headmin 0.2.1 → 0.2.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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/.nvmrc +1 -1
  3. data/Gemfile.lock +10 -10
  4. data/README.md +17 -7
  5. data/app/models/concerns/headmin/fieldable.rb +51 -23
  6. data/app/views/headmin/_blocks.html.erb +1 -1
  7. data/app/views/headmin/filters/_select.html.erb +3 -2
  8. data/app/views/headmin/filters/filter/_button.html.erb +0 -1
  9. data/app/views/headmin/forms/_repeater.html.erb +1 -2
  10. data/dist/js/headmin.js +1 -1
  11. data/docs/blocks-and-fields.md +54 -0
  12. data/docs/blocks.md +1 -47
  13. data/docs/devise.md +40 -2
  14. data/docs/fields.md +31 -9
  15. data/headmin.gemspec +1 -1
  16. data/lib/generators/headmin/devise_generator.rb +12 -0
  17. data/lib/generators/headmin/fields_generator.rb +1 -0
  18. data/lib/generators/templates/migrations/create_field_hierarchies.rb +16 -0
  19. data/lib/generators/templates/views/auth/confirmations/new.html.erb +1 -0
  20. data/lib/generators/templates/views/auth/mailer/confirmation_instructions.html.erb +1 -0
  21. data/lib/generators/templates/views/auth/mailer/email_changed.html.erb +1 -0
  22. data/lib/generators/templates/views/auth/mailer/password_change.html.erb +1 -0
  23. data/lib/generators/templates/views/auth/mailer/reset_password_instructions.html.erb +1 -0
  24. data/lib/generators/templates/views/auth/mailer/unlock_instructions.html.erb +1 -0
  25. data/lib/generators/templates/views/auth/passwords/edit.html.erb +1 -0
  26. data/lib/generators/templates/views/auth/passwords/new.html.erb +1 -0
  27. data/lib/generators/templates/views/auth/registrations/edit.html.erb +1 -0
  28. data/lib/generators/templates/views/auth/registrations/new.html.erb +1 -0
  29. data/lib/generators/templates/views/auth/sessions/new.html.erb +1 -0
  30. data/lib/generators/templates/views/auth/unlocks/new.html.erb +1 -0
  31. data/lib/generators/templates/views/layouts/auth.html.erb +20 -0
  32. data/lib/headmin/engine.rb +2 -0
  33. data/lib/headmin/version.rb +1 -1
  34. data/package.json +2 -2
  35. data/src/js/headmin/controllers/repeater_controller.js +0 -1
  36. data/yarn.lock +268 -228
  37. metadata +22 -7
  38. data/docs/README.md +0 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2beac2f2363cac213446d71615d6829e4e1a0f58445b9c542be5ec5d475b75f6
4
- data.tar.gz: 32230b06aab4e66fd34a84a502bbd59f511322d8120687c43fb891c89a055861
3
+ metadata.gz: a6df97c97a47fa81c3fa3a026c302819a6e98d745a03fa8e6923d0348f9fae3d
4
+ data.tar.gz: 88eacc6b2437171234af28230e9d57969e03e6d45f762d46a161d1f4d4bc422f
5
5
  SHA512:
6
- metadata.gz: 5f0b932aea5d61b7be3c00962a04f61ae894d6b6d375879db2c45bd55e82c7577f84b18f1d8e6f53afb8fa38cca7878271be261907c5540963b1f2d8dd6b1efe
7
- data.tar.gz: 8377deeba15704ada3b66370d296fb5b410eb40fcce4d3583afebf096f18011d2adda004803047f053dc34bd9b97a92bb8650edc5aafc5d759f53613d5f4ae46
6
+ metadata.gz: 551d05acba9042a11ad4cee3d886ce20945928977efb08aff7a82c74bcb94db6a23fb56923bd74189e92aab92788f4c874fdbfebe376cd9326e872c55d1fb9ca
7
+ data.tar.gz: 258da734682ffe276a47067fe9fdae811f2319e87057f78ed89c76a9d3ddea0c6372535c7065ebf52a62176001205feda5f8764d6bbe1c60c04399e301c4f515
data/.nvmrc CHANGED
@@ -1 +1 @@
1
- lts/*
1
+ lts/gallium
data/Gemfile.lock CHANGED
@@ -1,25 +1,25 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- headmin (0.2.0)
5
- closure_tree (~> 7.3)
4
+ headmin (0.2.4)
5
+ closure_tree
6
6
 
7
7
  GEM
8
8
  remote: https://rubygems.org/
9
9
  specs:
10
- activemodel (6.1.4)
11
- activesupport (= 6.1.4)
12
- activerecord (6.1.4)
13
- activemodel (= 6.1.4)
14
- activesupport (= 6.1.4)
15
- activesupport (6.1.4)
10
+ activemodel (6.1.4.1)
11
+ activesupport (= 6.1.4.1)
12
+ activerecord (6.1.4.1)
13
+ activemodel (= 6.1.4.1)
14
+ activesupport (= 6.1.4.1)
15
+ activesupport (6.1.4.1)
16
16
  concurrent-ruby (~> 1.0, >= 1.0.2)
17
17
  i18n (>= 1.6, < 2)
18
18
  minitest (>= 5.1)
19
19
  tzinfo (~> 2.0)
20
20
  zeitwerk (~> 2.3)
21
21
  ast (2.4.2)
22
- closure_tree (7.3.0)
22
+ closure_tree (7.4.0)
23
23
  activerecord (>= 4.2.10)
24
24
  with_advisory_lock (>= 4.0.0)
25
25
  concurrent-ruby (1.1.9)
@@ -50,7 +50,7 @@ GEM
50
50
  unicode-display_width (2.0.0)
51
51
  with_advisory_lock (4.6.0)
52
52
  activerecord (>= 4.2)
53
- zeitwerk (2.4.2)
53
+ zeitwerk (2.5.1)
54
54
 
55
55
  PLATFORMS
56
56
  x86_64-darwin-19
data/README.md CHANGED
@@ -1,10 +1,8 @@
1
1
  # Headmin
2
2
  A complete library of commonly used components to build an admin interface in your Ruby on Rails project.
3
3
 
4
- ## Interesting links
5
- - [Full documentation](docs/README.md)
6
- - [Rubygems](https://rubygems.org/gems/headmin)
7
- - [NPM](https://www.npmjs.com/package/headmin)
4
+ [![Gem Version](https://badge.fury.io/rb/headmin.svg)](https://rubygems.org/gems/headmin)
5
+ [![npm version](https://badge.fury.io/js/headmin.svg)](https://www.npmjs.com/package/headmin)
8
6
 
9
7
  ## Installation
10
8
 
@@ -47,6 +45,12 @@ Finally import Headmin in your stylesheet.
47
45
  @import '~headmin/src/scss/headmin.scss';
48
46
  ```
49
47
 
48
+ ### Integrations
49
+ - [Blocks](docs/blocks.md)
50
+ - [Fields](docs/fields.md)
51
+ - [Blocks + Fields = Magic](docs/blocks-and-fields.md)
52
+ - [Devise](docs/devise.md)
53
+
50
54
  ## Development
51
55
  For development purposes it's helpful to have both the test project and Headmin located in the same directory.
52
56
 
@@ -59,7 +63,7 @@ In package.json
59
63
  ```json
60
64
  {
61
65
  "dependencies": {
62
- "bar": "link:../headmin"
66
+ "bar": "file:../headmin"
63
67
  }
64
68
  }
65
69
  ```
@@ -94,8 +98,14 @@ $ yarn build
94
98
  Update the version number of the gem
95
99
 
96
100
  ```shell
97
- $ gem bump -v {patch,minor,major,...} -p
98
- $ gem tag -p
101
+ # First bundle if new runtime dependencies were added
102
+ $ bundle
103
+ $ git push
104
+
105
+ # Update the version number and tag the release
106
+ $ gem bump -v {patch,minor,major,...} --push --tag
107
+
108
+ # Release to Rubygems
99
109
  $ gem release
100
110
  ```
101
111
 
@@ -7,37 +7,65 @@ module Headmin
7
7
  accepts_nested_attributes_for :fields, allow_destroy: true
8
8
 
9
9
  def fields_hash
10
- save_pair(fields.hash_tree)
10
+ parse_hash_tree(fields.hash_tree)
11
+ end
12
+
13
+ def fields_hash=(hash)
14
+ parse_fields_hash(hash)
11
15
  end
12
16
 
13
17
  private
14
18
 
15
- def save_pair(my_hash, array = false)
16
- if array
17
- my_hash.map { |key, value|
18
- save_pair(value)
19
- }
20
- else
21
- Hash[my_hash.map { |key, value|
22
- if value.empty?
23
- case key.field_type
24
- when 'group'
25
- [key.name, save_pair(key.hash_tree[key])]
26
- when 'list'
27
- [key.name, save_pair(key.hash_tree[key], true)]
28
- else
29
- [key.name, return_field_output(key)]
19
+ def parse_fields_hash(hash)
20
+ hash.map do |key, value|
21
+ case value
22
+ when Hash
23
+ fields.build(
24
+ name: key,
25
+ field_type: 'group',
26
+ fields: parse_fields_hash(value)
27
+ )
28
+ when Array
29
+ fields.build(
30
+ name: key,
31
+ field_type: 'list',
32
+ fields: value.map do |item|
33
+ fields.new(name: 'item', field_type: 'group', fields: parse_fields_hash(item))
30
34
  end
31
- else
32
- key.field_type == 'list' ? [key.name, save_pair(value, true)] : [key.name, save_pair(value)]
33
- end
34
- }]
35
+ )
36
+ when String
37
+ fields.build(
38
+ name: key,
39
+ field_type: 'text',
40
+ value: value
41
+ )
42
+ else
43
+ fields.build(
44
+ name: key,
45
+ field_type: 'file',
46
+ file: value
47
+ )
48
+ end
35
49
  end
36
50
  end
37
51
 
38
- # returns what a field_type should return, e.g. its value, the file, the object ...
39
- def return_field_output(key)
40
- key.field_type == 'image' ? key.file : key.value
52
+ def parse_hash_tree(hash_tree)
53
+ Hash[hash_tree.map do |field, children|
54
+ case field.field_type.to_sym
55
+ when :group
56
+ children = children.any? ? children : field.hash_tree
57
+ [field.name, parse_hash_tree(children)]
58
+ when :list
59
+ children = children.any? ? children : field.hash_tree
60
+ [field.name, children.map { |child, grand_children| parse_hash_tree(grand_children) }]
61
+ when :image
62
+ [field.name, field.file]
63
+ when :file
64
+ [field.name, field.file]
65
+ else
66
+ [field.name, field.value]
67
+ end
68
+ end]
41
69
  end
42
70
  end
43
71
  end
@@ -18,7 +18,7 @@
18
18
 
19
19
  <% if blockable && blockable.respond_to?(:blocks) %>
20
20
  <% blockable.blocks.order(:position).each do |block| %>
21
- <% view_path = BlockService.frontend_view(block.name, path: path).gsub('/_', '/') %>
21
+ <% view_path = BlockService.frontend_view(block.name, path: path).gsub('/_', '/').split('.').first %>
22
22
  <%= render view_path, block: block %>
23
23
  <% end %>
24
24
  <% end %>
@@ -36,8 +36,9 @@
36
36
 
37
37
  <%= content_for :filters_buttons do %>
38
38
  <% (params[name] || []).each_with_index do |param, index| %>
39
- <% value = options.detect { |value, key, config| key == param } %>
40
- <%= render 'headmin/filters/filter/button', name: name, label: label, value: value ? value.first : nil, id: "#{name}_#{index}" do %>
39
+ <% selected_option = options.detect { |value, key, config| (key.present? ? key : value) == param } %>
40
+ <% selected_value = selected_option.is_a?(Array) ? selected_option.first : selected_option %>
41
+ <%= render 'headmin/filters/filter/button', name: name, label: label, value: selected_value, id: "#{name}_#{index}" do %>
41
42
  <%= select_tag("#{name}[]", options_for_select(options, param), select_options) %>
42
43
  <% end %>
43
44
  <% end %>
@@ -12,7 +12,6 @@
12
12
  <% value = local_assigns.has_key?(:value) ? value : nil %>
13
13
 
14
14
  <div class="h-filter me-1 my-1" data-filter-name="<%= name %>">
15
-
16
15
  <button
17
16
  type="button"
18
17
  class="h-filter-button btn h-btn-outline-transparent"
@@ -39,14 +39,13 @@
39
39
 
40
40
  template_names = templates.map { |template| File.basename(template, '.html.erb') }
41
41
  template_names = template_names.any? ? template_names : ['new']
42
- multiple_templates = template_names.count > 1
43
42
  object_model = form.object.class
44
43
  association_model = object_model.reflect_on_association(attribute).class_name.constantize
45
44
  with_positions = association_model.new.attributes.keys.include?('position')
46
45
  associations = form.object.send(attribute)
47
46
  associations = with_positions ? associations.order(:position) : associations
48
47
  repeater_id = form.object_id
49
- pass_thru = multiple_templates ? nil : '[data-template-name="new"]'
48
+ pass_thru = template_names.count == 1 ? "[data-template-name=\"#{template_names.first}\"]" : nil
50
49
  show_label = label != false
51
50
  %>
52
51
 
data/dist/js/headmin.js CHANGED
@@ -1505,7 +1505,7 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) *
1505
1505
  /***/ (function(module, __webpack_exports__, __webpack_require__) {
1506
1506
 
1507
1507
  "use strict";
1508
- eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"default\", function() { return _default; });\n/* harmony import */ var stimulus__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! stimulus */ \"./node_modules/stimulus/index.js\");\n/* harmony import */ var sortablejs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! sortablejs */ \"./node_modules/sortablejs/modular/sortable.esm.js\");\nfunction _typeof(obj) { \"@babel/helpers - typeof\"; if (typeof Symbol === \"function\" && typeof Symbol.iterator === \"symbol\") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === \"function\" && obj.constructor === Symbol && obj !== Symbol.prototype ? \"symbol\" : typeof obj; }; } return _typeof(obj); }\n\nfunction _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError(\"Cannot call a class as a function\"); } }\n\nfunction _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if (\"value\" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }\n\nfunction _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }\n\nfunction _inherits(subClass, superClass) { if (typeof superClass !== \"function\" && superClass !== null) { throw new TypeError(\"Super expression must either be null or a function\"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); }\n\nfunction _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }\n\nfunction _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; }\n\nfunction _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === \"object\" || typeof call === \"function\")) { return call; } return _assertThisInitialized(self); }\n\nfunction _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError(\"this hasn't been initialised - super() hasn't been called\"); } return self; }\n\nfunction _isNativeReflectConstruct() { if (typeof Reflect === \"undefined\" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === \"function\") return true; try { Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); return true; } catch (e) { return false; } }\n\nfunction _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }\n\n\n\n\nvar _default = /*#__PURE__*/function (_Controller) {\n _inherits(_default, _Controller);\n\n var _super = _createSuper(_default);\n\n function _default() {\n _classCallCheck(this, _default);\n\n return _super.apply(this, arguments);\n }\n\n _createClass(_default, [{\n key: \"connect\",\n value: function connect() {\n var _this = this;\n\n new sortablejs__WEBPACK_IMPORTED_MODULE_1__[\"default\"](this.listTarget, {\n animation: 150,\n ghostClass: 'list-group-item-dark',\n draggable: '.repeater-row',\n handle: '.repeater-row-handle',\n onEnd: function onEnd() {\n _this.resetIndices();\n\n _this.resetPositions();\n }\n });\n this.toggleEmpty();\n }\n }, {\n key: \"resetButtonIndices\",\n value: function resetButtonIndices(event) {\n var row = event.target.closest('.repeater-row');\n var index = this.containsRow(row) ? row.dataset.rowIndex : '';\n this.updatePopupButtonIndices(index);\n }\n }, {\n key: \"containsRow\",\n value: function containsRow(row) {\n return this.rowTargets.includes(row);\n }\n }, {\n key: \"updatePopupButtonIndices\",\n value: function updatePopupButtonIndices(index) {\n console.log(index);\n var popup = document.querySelector(\"[data-popup-target=\\\"popup\\\"][data-popup-id=\\\"repeater-buttons-\".concat(this.idValue, \"\\\"]\"));\n var buttons = popup.querySelectorAll('a');\n buttons.forEach(function (button) {\n button.dataset.rowIndex = index;\n });\n }\n }, {\n key: \"addRow\",\n value: function addRow(event) {\n event.preventDefault();\n var button = event.target;\n var templateName = button.dataset.templateName;\n var rowIndex = button.dataset.rowIndex; // Prepare html from template\n\n var html = this.getTemplateHTML(templateName);\n html = this.replaceIdsWithTimestamps(html); // Fallback to last row if no index is set\n\n if (rowIndex) {\n // Insert new row after defined row\n var row = this.rowTargets[rowIndex];\n row.insertAdjacentHTML('afterend', html);\n } else {\n // Insert before footer\n this.footerTarget.insertAdjacentHTML('beforebegin', html);\n } // Dispatch an event\n\n\n document.dispatchEvent(new CustomEvent('headmin:reinit', {\n bubbles: true\n }));\n this.resetIndices();\n this.resetPositions();\n this.toggleEmpty();\n }\n }, {\n key: \"removeRow\",\n value: function removeRow(event) {\n event.preventDefault();\n var row = event.target.closest(\".repeater-row\");\n\n if (row.dataset.newRecord === \"true\") {\n // New records are simply removed from the page\n row.remove();\n } else {\n // Existing records are hidden and flagged for deletion\n row.querySelector(\"input[name*='_destroy']\").value = 1;\n row.style.display = 'none';\n }\n\n this.resetIndices();\n this.resetPositions();\n this.toggleEmpty();\n }\n }, {\n key: \"getTemplateHTML\",\n value: function getTemplateHTML(name) {\n var template = this.templateTargets.filter(function (template) {\n return template.dataset.templateName === name;\n })[0];\n return template.innerHTML;\n }\n }, {\n key: \"replaceIdsWithTimestamps\",\n value: function replaceIdsWithTimestamps(html) {\n var regex = new RegExp('template_id', \"g\");\n return html.replace(regex, new Date().getTime());\n }\n }, {\n key: \"visibleRowsCount\",\n value: function visibleRowsCount() {\n return this.visibleRows().length;\n }\n }, {\n key: \"visibleRows\",\n value: function visibleRows() {\n var rows = this.rowTargets;\n return rows.filter(function (row) {\n return row.querySelector(\"input[name*='_destroy']\").value !== '1';\n });\n }\n }, {\n key: \"toggleEmpty\",\n value: function toggleEmpty() {\n if (this.visibleRowsCount() > 0) {\n this.emptyTarget.classList.add('invisible');\n } else {\n this.emptyTarget.classList.remove('invisible');\n }\n }\n }, {\n key: \"resetPositions\",\n value: function resetPositions() {\n this.visibleRows().forEach(function (row, index) {\n var positionInput = row.querySelector(\"input[name*='position']\");\n\n if (positionInput) {\n positionInput.value = index;\n }\n });\n }\n }, {\n key: \"resetIndices\",\n value: function resetIndices() {\n this.visibleRows().forEach(function (row, index) {\n row.dataset.rowIndex = index;\n });\n }\n }], [{\n key: \"values\",\n get: function get() {\n return {\n id: String\n };\n }\n }, {\n key: \"targets\",\n get: function get() {\n return [\"repeater\", \"footer\", \"template\", \"row\", \"list\", \"empty\", \"addButton\"];\n }\n }]);\n\n return _default;\n}(stimulus__WEBPACK_IMPORTED_MODULE_0__[\"Controller\"]);\n\n\n\n//# sourceURL=webpack:///./src/js/headmin/controllers/repeater_controller.js?");
1508
+ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"default\", function() { return _default; });\n/* harmony import */ var stimulus__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! stimulus */ \"./node_modules/stimulus/index.js\");\n/* harmony import */ var sortablejs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! sortablejs */ \"./node_modules/sortablejs/modular/sortable.esm.js\");\nfunction _typeof(obj) { \"@babel/helpers - typeof\"; if (typeof Symbol === \"function\" && typeof Symbol.iterator === \"symbol\") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === \"function\" && obj.constructor === Symbol && obj !== Symbol.prototype ? \"symbol\" : typeof obj; }; } return _typeof(obj); }\n\nfunction _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError(\"Cannot call a class as a function\"); } }\n\nfunction _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if (\"value\" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }\n\nfunction _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }\n\nfunction _inherits(subClass, superClass) { if (typeof superClass !== \"function\" && superClass !== null) { throw new TypeError(\"Super expression must either be null or a function\"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); }\n\nfunction _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }\n\nfunction _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; }\n\nfunction _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === \"object\" || typeof call === \"function\")) { return call; } return _assertThisInitialized(self); }\n\nfunction _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError(\"this hasn't been initialised - super() hasn't been called\"); } return self; }\n\nfunction _isNativeReflectConstruct() { if (typeof Reflect === \"undefined\" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === \"function\") return true; try { Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); return true; } catch (e) { return false; } }\n\nfunction _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }\n\n\n\n\nvar _default = /*#__PURE__*/function (_Controller) {\n _inherits(_default, _Controller);\n\n var _super = _createSuper(_default);\n\n function _default() {\n _classCallCheck(this, _default);\n\n return _super.apply(this, arguments);\n }\n\n _createClass(_default, [{\n key: \"connect\",\n value: function connect() {\n var _this = this;\n\n new sortablejs__WEBPACK_IMPORTED_MODULE_1__[\"default\"](this.listTarget, {\n animation: 150,\n ghostClass: 'list-group-item-dark',\n draggable: '.repeater-row',\n handle: '.repeater-row-handle',\n onEnd: function onEnd() {\n _this.resetIndices();\n\n _this.resetPositions();\n }\n });\n this.toggleEmpty();\n }\n }, {\n key: \"resetButtonIndices\",\n value: function resetButtonIndices(event) {\n var row = event.target.closest('.repeater-row');\n var index = this.containsRow(row) ? row.dataset.rowIndex : '';\n this.updatePopupButtonIndices(index);\n }\n }, {\n key: \"containsRow\",\n value: function containsRow(row) {\n return this.rowTargets.includes(row);\n }\n }, {\n key: \"updatePopupButtonIndices\",\n value: function updatePopupButtonIndices(index) {\n var popup = document.querySelector(\"[data-popup-target=\\\"popup\\\"][data-popup-id=\\\"repeater-buttons-\".concat(this.idValue, \"\\\"]\"));\n var buttons = popup.querySelectorAll('a');\n buttons.forEach(function (button) {\n button.dataset.rowIndex = index;\n });\n }\n }, {\n key: \"addRow\",\n value: function addRow(event) {\n event.preventDefault();\n var button = event.target;\n var templateName = button.dataset.templateName;\n var rowIndex = button.dataset.rowIndex; // Prepare html from template\n\n var html = this.getTemplateHTML(templateName);\n html = this.replaceIdsWithTimestamps(html); // Fallback to last row if no index is set\n\n if (rowIndex) {\n // Insert new row after defined row\n var row = this.rowTargets[rowIndex];\n row.insertAdjacentHTML('afterend', html);\n } else {\n // Insert before footer\n this.footerTarget.insertAdjacentHTML('beforebegin', html);\n } // Dispatch an event\n\n\n document.dispatchEvent(new CustomEvent('headmin:reinit', {\n bubbles: true\n }));\n this.resetIndices();\n this.resetPositions();\n this.toggleEmpty();\n }\n }, {\n key: \"removeRow\",\n value: function removeRow(event) {\n event.preventDefault();\n var row = event.target.closest(\".repeater-row\");\n\n if (row.dataset.newRecord === \"true\") {\n // New records are simply removed from the page\n row.remove();\n } else {\n // Existing records are hidden and flagged for deletion\n row.querySelector(\"input[name*='_destroy']\").value = 1;\n row.style.display = 'none';\n }\n\n this.resetIndices();\n this.resetPositions();\n this.toggleEmpty();\n }\n }, {\n key: \"getTemplateHTML\",\n value: function getTemplateHTML(name) {\n var template = this.templateTargets.filter(function (template) {\n return template.dataset.templateName === name;\n })[0];\n return template.innerHTML;\n }\n }, {\n key: \"replaceIdsWithTimestamps\",\n value: function replaceIdsWithTimestamps(html) {\n var regex = new RegExp('template_id', \"g\");\n return html.replace(regex, new Date().getTime());\n }\n }, {\n key: \"visibleRowsCount\",\n value: function visibleRowsCount() {\n return this.visibleRows().length;\n }\n }, {\n key: \"visibleRows\",\n value: function visibleRows() {\n var rows = this.rowTargets;\n return rows.filter(function (row) {\n return row.querySelector(\"input[name*='_destroy']\").value !== '1';\n });\n }\n }, {\n key: \"toggleEmpty\",\n value: function toggleEmpty() {\n if (this.visibleRowsCount() > 0) {\n this.emptyTarget.classList.add('invisible');\n } else {\n this.emptyTarget.classList.remove('invisible');\n }\n }\n }, {\n key: \"resetPositions\",\n value: function resetPositions() {\n this.visibleRows().forEach(function (row, index) {\n var positionInput = row.querySelector(\"input[name*='position']\");\n\n if (positionInput) {\n positionInput.value = index;\n }\n });\n }\n }, {\n key: \"resetIndices\",\n value: function resetIndices() {\n this.visibleRows().forEach(function (row, index) {\n row.dataset.rowIndex = index;\n });\n }\n }], [{\n key: \"values\",\n get: function get() {\n return {\n id: String\n };\n }\n }, {\n key: \"targets\",\n get: function get() {\n return [\"repeater\", \"footer\", \"template\", \"row\", \"list\", \"empty\", \"addButton\"];\n }\n }]);\n\n return _default;\n}(stimulus__WEBPACK_IMPORTED_MODULE_0__[\"Controller\"]);\n\n\n\n//# sourceURL=webpack:///./src/js/headmin/controllers/repeater_controller.js?");
1509
1509
 
1510
1510
  /***/ }),
1511
1511
 
@@ -0,0 +1,54 @@
1
+ # Blocks + fields = Magic
2
+
3
+ ## Installation
4
+ Run the following generators to generate the migration files.
5
+ ```
6
+ rails generate headmin:blocks
7
+ rails generate headmin:fields
8
+ rails db:migrate
9
+ ```
10
+
11
+ ## Getting Started
12
+ ### Setup model
13
+
14
+ Setup a block model by including `Headmin::Fieldable`
15
+
16
+ ```ruby
17
+ class Block < ApplicationRecord
18
+ include Headmin::Block
19
+ include Headmin::Fieldable
20
+ end
21
+ ```
22
+
23
+ ### Setup forms
24
+
25
+ ```erb
26
+ # app/views/admin/blocks/_contact.html.erb
27
+ <%= form.hidden_field :name, value: :contact %>
28
+
29
+ <!-- Title -->
30
+ <%= render 'headmin/forms/fields/text', form: form, name: :title do |field, attribute, label| %>
31
+ <%= render 'headmin/forms/text', form: field, attribute: attribute, label: label %>
32
+ <% end %>
33
+
34
+ <!-- People list -->
35
+ <%= render 'headmin/forms/fields/list', form: form, name: :people do |item| %>
36
+ <%= render 'headmin/forms/fields/text', form: item, name: :name do |field, attribute, label| %>
37
+ <%= render 'headmin/forms/text', form: field, attribute: attribute, label: label %>
38
+ <% end %>
39
+ <% end %>
40
+ ```
41
+
42
+ ### Usage in website
43
+
44
+ ```erb
45
+ # app/views/website/blocks/_contact.html.erb
46
+ <% fields = block.fields_hash %>
47
+
48
+ <h1><%= fields["title"] %></h1>
49
+ <ul>
50
+ <% fields["people"].each do |person| %>
51
+ <li><%= person["name"] %></h2></li>
52
+ <% end %>
53
+ </ul>
54
+ ```
data/docs/blocks.md CHANGED
@@ -38,7 +38,7 @@ Make sure to include a hidden field to store the name of the block.
38
38
  ...
39
39
  ```
40
40
 
41
- ### Render blocks in frontend
41
+ ### Usage in frontend
42
42
 
43
43
  ```erb
44
44
  # app/views/website/pages/show.html.erb
@@ -53,49 +53,3 @@ For each block in the admin, you'll need to create a corresponding template in y
53
53
  # app/views/website/blocks/_contact.html.erb
54
54
  <%= block.name %>
55
55
  ```
56
-
57
- ## Blocks + fields = Magic
58
-
59
- ### Add fields to blocks
60
-
61
- Setup a block model by including `Headmin::Fieldable`
62
-
63
- ```ruby
64
- class Block < ApplicationRecord
65
- include Headmin::Block
66
- include Headmin::Fieldable
67
- end
68
- ```
69
-
70
- ### Use fields in admin blocks
71
-
72
- ```erb
73
- # app/views/admin/blocks/_contact.html.erb
74
- <%= form.hidden_field :name, value: :contact %>
75
-
76
- <!-- Title -->
77
- <%= render 'headmin/forms/fields/text', form: form, name: :title do |field, attribute, label| %>
78
- <%= render 'headmin/forms/text', form: field, attribute: attribute, label: label %>
79
- <% end %>
80
-
81
- <!-- People list -->
82
- <%= render 'headmin/forms/fields/list', form: form, name: :people do |item| %>
83
- <%= render 'headmin/forms/fields/text', form: item, name: :name do |field, attribute, label| %>
84
- <%= render 'headmin/forms/text', form: field, attribute: attribute, label: label %>
85
- <% end %>
86
- <% end %>
87
- ```
88
-
89
- ### Render blocks in your frontend
90
-
91
- ```erb
92
- # app/views/website/blocks/_contact.html.erb
93
- <% fields = block.fields_hash %>
94
-
95
- <h1><%= fields["title"] %></h1>
96
- <ul>
97
- <% fields["people"].each do |person| %>
98
- <li><%= person["name"] %></h2></li>
99
- <% end %>
100
- </ul>
101
- ```
data/docs/devise.md CHANGED
@@ -1,3 +1,41 @@
1
- # Authentication
1
+ # Devise
2
+ Headmin comes with some default views for all devise related pages.
3
+ To make use of them, follow the guidelines below.
2
4
 
3
- TODO: rewrite
5
+ ## Installation
6
+ Add this line to your application's Gemfile:
7
+
8
+ ```ruby
9
+ gem 'devise'
10
+ ```
11
+
12
+ Run the install generator
13
+ ```sh
14
+ $ rails generate devise:install
15
+ ```
16
+
17
+ Create a devise model
18
+ ```sh
19
+ $ rails generate devise User
20
+ $ rails db:migrate
21
+ ```
22
+
23
+ Generate the default auth views
24
+ ```sh
25
+ $ rails generate headmin:devise
26
+ ```
27
+
28
+ ## Getting Started
29
+
30
+ ### Change routes
31
+ Change the default `devise_for` routes with the code below
32
+
33
+ ```ruby
34
+ devise_for :users, controllers: {
35
+ sessions: 'auth/sessions',
36
+ registrations: 'auth/registrations',
37
+ passwords: 'auth/passwords',
38
+ unlocks: 'auth/unlocks',
39
+ confirmations: 'auth/confirmations'
40
+ }
41
+ ```
data/docs/fields.md CHANGED
@@ -1,10 +1,12 @@
1
- # Fields
1
+ # Fields
2
2
 
3
3
  ## Installation
4
+
4
5
  Run the following generators to generate the migration files.
5
- ```
6
- rails generate headmin:fields
7
- rails db:migrate
6
+
7
+ ```sh
8
+ $ rails generate headmin:fields
9
+ $ rails db:migrate
8
10
  ```
9
11
 
10
12
  ## Getting Started
@@ -12,6 +14,7 @@ rails db:migrate
12
14
  ### Setup model
13
15
 
14
16
  ```ruby
17
+
15
18
  class Settings < ApplicationRecord
16
19
  include Headmin::Fieldable
17
20
  end
@@ -19,7 +22,7 @@ end
19
22
 
20
23
  ### Setup forms
21
24
 
22
- ```erb
25
+ ```html
23
26
  # app/views/admin/settings/_form.html.erb
24
27
 
25
28
  <!-- Company name -->
@@ -36,22 +39,41 @@ end
36
39
  ```
37
40
 
38
41
  #### Type of fields
42
+
39
43
  - Text: `headmin/forms/fields/text`
40
44
  - File: `headmin/forms/fields/file`
41
45
  - Image: `headmin/forms/fields/image`
42
46
  - List: `headmin/forms/fields/list`
43
47
  - Group: `headmin/forms/fields/group`
44
48
 
45
- ### Render fields in frontend
49
+ ### Seed database
50
+
51
+ Use the `fields_hash` setter method to set the data, the way you want to retrieve it.
46
52
 
47
- ```erb
53
+ ```ruby
54
+ # In db/seeds.rb
55
+ Setting.create!(
56
+ name: 'Website settings',
57
+ fields_hash: {
58
+ company_name: 'Insiting BV',
59
+ people: [
60
+ { name: 'Jef Vlamings' },
61
+ { name: 'Gert-Jan Peeters' }
62
+ ]
63
+ }
64
+ )
65
+ ```
66
+
67
+ ### Usage in frontend
68
+
69
+ ```html
48
70
  # app/views/website/settings/show.html.erb
49
71
  <% fields = @setting.fields_hash %>
50
72
 
51
73
  <h1><%= fields["company_name"] %></h1>
52
74
  <ul>
53
- <% fields["people"].each do |person| %>
75
+ <% fields["people"].each do |person| %>
54
76
  <li><%= person["name"] %></h2></li>
55
- <% end %>
77
+ <% end %>
56
78
  </ul>
57
79
  ```
data/headmin.gemspec CHANGED
@@ -29,7 +29,7 @@ Gem::Specification.new do |spec|
29
29
 
30
30
  # Uncomment to register a new dependency of your gem
31
31
  # spec.add_dependency "example-gem", "~> 1.0"
32
- spec.add_runtime_dependency 'closure_tree', '~> 7.3'
32
+ spec.add_runtime_dependency 'closure_tree'
33
33
 
34
34
  # For more information and examples about making a new gem, checkout our
35
35
  # guide at: https://bundler.io/guides/creating_gem.html
@@ -0,0 +1,12 @@
1
+ module Headmin
2
+ class DeviseGenerator < Rails::Generators::Base
3
+ include Rails::Generators::Migration
4
+
5
+ source_root File.expand_path('../../templates', __FILE__)
6
+
7
+ def blocks
8
+ directory 'views/auth', 'app/views/auth'
9
+ copy_file 'views/layouts/auth.html.erb', 'app/views/layouts/auth.html.erb'
10
+ end
11
+ end
12
+ end
@@ -7,6 +7,7 @@ module Headmin
7
7
  def blocks
8
8
  template 'models/field.rb', 'app/models/field.rb'
9
9
  migration_template 'migrations/create_fields.rb', 'db/migrate/create_fields.rb'
10
+ migration_template 'migrations/create_field_hierarchies.rb', 'db/migrate/create_field_hierarchies.rb'
10
11
  end
11
12
 
12
13
  private
@@ -0,0 +1,16 @@
1
+ class CreateFieldHierarchies < ActiveRecord::Migration[6.1]
2
+ def change
3
+ create_table :field_hierarchies, id: false do |t|
4
+ t.integer :ancestor_id, null: false
5
+ t.integer :descendant_id, null: false
6
+ t.integer :generations, null: false
7
+ end
8
+
9
+ add_index :field_hierarchies, [:ancestor_id, :descendant_id, :generations],
10
+ unique: true,
11
+ name: "field_anc_desc_idx"
12
+
13
+ add_index :field_hierarchies, [:descendant_id],
14
+ name: "field_desc_idx"
15
+ end
16
+ end
@@ -0,0 +1 @@
1
+ <%= render "headmin/views/devise/confirmations/new" %>
@@ -0,0 +1 @@
1
+ <%= render "headmin/views/devise/mailer/confirmation_instructions" %>
@@ -0,0 +1 @@
1
+ <%= render "headmin/views/devise/mailer/email_changed" %>
@@ -0,0 +1 @@
1
+ <%= render "headmin/views/devise/mailer/password_change" %>
@@ -0,0 +1 @@
1
+ <%= render "headmin/views/devise/mailer/reset_password_instructions" %>
@@ -0,0 +1 @@
1
+ <%= render "headmin/views/devise/mailer/unlock_instructions" %>
@@ -0,0 +1 @@
1
+ <%= render "headmin/views/devise/passwords/edit" %>
@@ -0,0 +1 @@
1
+ <%= render "headmin/views/devise/passwords/new" %>
@@ -0,0 +1 @@
1
+ <%= render "headmin/views/devise/registrations/edit" %>
@@ -0,0 +1 @@
1
+ <%= render "headmin/views/devise/registrations/new" %>
@@ -0,0 +1 @@
1
+ <%= render "headmin/views/devise/sessions/new" %>
@@ -0,0 +1 @@
1
+ <%= render "headmin/views/devise/unlocks/new" %>
@@ -0,0 +1,20 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Authentication</title>
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <%= csrf_meta_tags %>
7
+ <%= csp_meta_tag %>
8
+ <%= stylesheet_pack_tag 'admin', media: 'all', 'data-turbolinks-track': 'reload' %>
9
+ <%= javascript_pack_tag 'admin', 'data-turbolinks-track': 'reload' %>
10
+ </head>
11
+ <body class="bg-light w-100 vh-100">
12
+
13
+ <%= render 'headmin/notifications' %>
14
+
15
+ <main class="d-flex justify-content-center align-items-center w-100 vh-100 p-3">
16
+ <%= yield %>
17
+ </main>
18
+
19
+ </body>
20
+ </html>