headmin 0.2.0 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.nvmrc +1 -1
- data/Gemfile +0 -1
- data/Gemfile.lock +10 -10
- data/README.md +20 -16
- data/app/models/concerns/headmin/fieldable.rb +51 -23
- data/app/views/headmin/filters/_select.html.erb +3 -2
- data/app/views/headmin/filters/filter/_button.html.erb +0 -1
- data/app/views/headmin/forms/_repeater.html.erb +1 -2
- data/dist/js/headmin.js +1 -1
- data/docs/blocks-and-fields.md +54 -0
- data/docs/blocks.md +1 -47
- data/docs/devise.md +40 -2
- data/docs/fields.md +31 -9
- data/headmin.gemspec +1 -0
- data/lib/generators/headmin/devise_generator.rb +12 -0
- data/lib/generators/headmin/fields_generator.rb +1 -0
- data/lib/generators/templates/migrations/create_field_hierarchies.rb +16 -0
- data/lib/generators/templates/views/auth/confirmations/new.html.erb +1 -0
- data/lib/generators/templates/views/auth/mailer/confirmation_instructions.html.erb +1 -0
- data/lib/generators/templates/views/auth/mailer/email_changed.html.erb +1 -0
- data/lib/generators/templates/views/auth/mailer/password_change.html.erb +1 -0
- data/lib/generators/templates/views/auth/mailer/reset_password_instructions.html.erb +1 -0
- data/lib/generators/templates/views/auth/mailer/unlock_instructions.html.erb +1 -0
- data/lib/generators/templates/views/auth/passwords/edit.html.erb +1 -0
- data/lib/generators/templates/views/auth/passwords/new.html.erb +1 -0
- data/lib/generators/templates/views/auth/registrations/edit.html.erb +1 -0
- data/lib/generators/templates/views/auth/registrations/new.html.erb +1 -0
- data/lib/generators/templates/views/auth/sessions/new.html.erb +1 -0
- data/lib/generators/templates/views/auth/unlocks/new.html.erb +1 -0
- data/lib/generators/templates/views/layouts/auth.html.erb +20 -0
- data/lib/headmin/engine.rb +2 -0
- data/lib/headmin/version.rb +1 -1
- data/package.json +2 -2
- data/src/js/headmin/controllers/repeater_controller.js +0 -1
- data/yarn.lock +268 -228
- metadata +33 -4
- data/docs/README.md +0 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ade465655c1e8e80dd864856e0f0cb26ab1f731bfd518b3ad976bbb317420aa1
|
4
|
+
data.tar.gz: 2dc85e50e4840a186b996ec165f296dd8b234292172f58e1be74cf378adf7f3f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bfa03386ea283e4d45180d69d04c226459ce494ed6a004215d1b4ba028dd5e1eb4c87e6027016743deab25baa103a239ed2933b0786dbdb0af727a1d16e90de2
|
7
|
+
data.tar.gz: a86486191d491de2585fe647644812f6c2e813473821d89bea6be44c0a7d71b42cdc0293d2117094946ba5bbd4415dea85e7cd5dc3c7249a72d68b2e8a15a9bd
|
data/.nvmrc
CHANGED
@@ -1 +1 @@
|
|
1
|
-
lts
|
1
|
+
lts/gallium
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,24 +1,25 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
headmin (0.2.
|
4
|
+
headmin (0.2.3)
|
5
|
+
closure_tree
|
5
6
|
|
6
7
|
GEM
|
7
8
|
remote: https://rubygems.org/
|
8
9
|
specs:
|
9
|
-
activemodel (6.1.4)
|
10
|
-
activesupport (= 6.1.4)
|
11
|
-
activerecord (6.1.4)
|
12
|
-
activemodel (= 6.1.4)
|
13
|
-
activesupport (= 6.1.4)
|
14
|
-
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)
|
15
16
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
16
17
|
i18n (>= 1.6, < 2)
|
17
18
|
minitest (>= 5.1)
|
18
19
|
tzinfo (~> 2.0)
|
19
20
|
zeitwerk (~> 2.3)
|
20
21
|
ast (2.4.2)
|
21
|
-
closure_tree (7.
|
22
|
+
closure_tree (7.4.0)
|
22
23
|
activerecord (>= 4.2.10)
|
23
24
|
with_advisory_lock (>= 4.0.0)
|
24
25
|
concurrent-ruby (1.1.9)
|
@@ -49,13 +50,12 @@ GEM
|
|
49
50
|
unicode-display_width (2.0.0)
|
50
51
|
with_advisory_lock (4.6.0)
|
51
52
|
activerecord (>= 4.2)
|
52
|
-
zeitwerk (2.
|
53
|
+
zeitwerk (2.5.1)
|
53
54
|
|
54
55
|
PLATFORMS
|
55
56
|
x86_64-darwin-19
|
56
57
|
|
57
58
|
DEPENDENCIES
|
58
|
-
closure_tree (~> 7.3)
|
59
59
|
headmin!
|
60
60
|
minitest (~> 5.0)
|
61
61
|
rake (~> 13.0)
|
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
|
-
|
5
|
-
|
6
|
-
- [Rubygems](https://rubygems.org/gems/headmin)
|
7
|
-
- [NPM](https://www.npmjs.com/package/headmin)
|
4
|
+
[](https://rubygems.org/gems/headmin)
|
5
|
+
[](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": "
|
66
|
+
"bar": "file:../headmin"
|
63
67
|
}
|
64
68
|
}
|
65
69
|
```
|
@@ -94,21 +98,21 @@ $ yarn build
|
|
94
98
|
Update the version number of the gem
|
95
99
|
|
96
100
|
```shell
|
97
|
-
|
98
|
-
$
|
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
|
|
102
|
-
|
112
|
+
Update the node module
|
103
113
|
```
|
104
|
-
|
105
|
-
|
106
|
-
{
|
107
|
-
"name": "headmin",
|
108
|
-
"version": "{new version number here}",
|
109
|
-
...
|
110
|
-
}
|
111
|
-
|
114
|
+
# Manually update the version number in package.json
|
115
|
+
$ npm publish
|
112
116
|
```
|
113
117
|
|
114
118
|
## Deployment
|
@@ -7,37 +7,65 @@ module Headmin
|
|
7
7
|
accepts_nested_attributes_for :fields, allow_destroy: true
|
8
8
|
|
9
9
|
def fields_hash
|
10
|
-
|
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
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
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
|
-
|
32
|
-
|
33
|
-
|
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
|
-
|
39
|
-
|
40
|
-
|
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
|
@@ -36,8 +36,9 @@
|
|
36
36
|
|
37
37
|
<%= content_for :filters_buttons do %>
|
38
38
|
<% (params[name] || []).each_with_index do |param, index| %>
|
39
|
-
<%
|
40
|
-
|
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 %>
|
@@ -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 =
|
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
|
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
|
-
###
|
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
|
-
#
|
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
|
-
|
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
|
-
|
7
|
-
rails
|
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
|
-
```
|
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
|
-
###
|
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
|
-
```
|
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
|
-
|
75
|
+
<% fields["people"].each do |person| %>
|
54
76
|
<li><%= person["name"] %></h2></li>
|
55
|
-
|
77
|
+
<% end %>
|
56
78
|
</ul>
|
57
79
|
```
|
data/headmin.gemspec
CHANGED
@@ -29,6 +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'
|
32
33
|
|
33
34
|
# For more information and examples about making a new gem, checkout our
|
34
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/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" %>
|