adminterface 0.2.0 → 0.2.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.
- checksums.yaml +4 -4
- data/README.md +9 -1
- data/app/javascript/adminterface/initializers/header_toggler.js +1 -1
- data/app/javascript/adminterface/initializers/plugins/flatpickr.js +1 -1
- data/app/javascript/adminterface/initializers/plugins/input_counter.js +1 -1
- data/app/javascript/adminterface/initializers/plugins/password_visibility.js +1 -1
- data/app/javascript/adminterface/initializers/plugins/tom_select.js +1 -1
- data/app/javascript/adminterface/lib/__tests__/checkbox_toggler.html +5 -0
- data/app/javascript/adminterface/lib/__tests__/checkbox_toggler.spec.js +110 -0
- data/app/javascript/adminterface/lib/__tests__/detached_dropdown.spec.js +0 -1
- data/app/javascript/adminterface/lib/__tests__/filters.spec.js +0 -1
- data/app/javascript/adminterface/lib/__tests__/has_many.html +14 -0
- data/app/javascript/adminterface/lib/__tests__/has_many.spec.js +224 -0
- data/app/javascript/adminterface/lib/__tests__/utils.spec.js +0 -1
- data/app/javascript/adminterface/lib/has_many.js +41 -38
- data/lib/adminterface/version.rb +1 -1
- data/lib/generators/adminterface/comments/comments_generator.rb +4 -0
- data/lib/generators/adminterface/configs/configs_generator.rb +2 -2
- data/lib/generators/adminterface/install/install_generator.rb +21 -23
- data/lib/generators/adminterface/views/views_generator.rb +2 -2
- data/lib/generators/adminterface/webpacker/webpacker_generator.rb +20 -6
- metadata +9 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7aa4367f85794395d6b92b141c12488c2158b99f0585f4a76f201bb080fb167c
|
4
|
+
data.tar.gz: 88a7bb2ecafe63f80145908a0cd4d1289011a71352ee9960f2870a80c91fc08f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 92dfa1e53f319043ee355df3d0d70e9888f070727c5828851ee1e10d6e2aa590af464dc7ad75ef871d8e375676dfc68ed4c97decc0fc3f0492868d642f2f2667
|
7
|
+
data.tar.gz: 650a23ef8b8f323f6f792fa591aec71d60701c7b5c32d3d2d311cb51ed73a42f9ef8bf0d988498d60aba2cfe0b8a87e82bd8aa7c8e5162b209ac6f72e5d60004
|
data/README.md
CHANGED
@@ -1,7 +1,11 @@
|
|
1
1
|
# Adminterface <!-- omit in toc -->
|
2
2
|
[](https://github.com/CMDBrew/adminterface/actions/workflows/ci.yml)
|
3
|
+
[](https://codeclimate.com/github/CMDBrew/adminterface/maintainability)
|
4
|
+
[](https://codeclimate.com/github/CMDBrew/adminterface/test_coverage)
|
5
|
+
[](https://rubygems.org/gems/adminterface)
|
6
|
+
[](https://www.npmjs.com/package/@cmdbrew/adminterface)
|
3
7
|
|
4
|
-
|
8
|
+
A gem that brings Bootstrap 5, advanced customizability, and other goodies into ActiveAdmin
|
5
9
|
|
6
10
|
[](https://adminterface.io)
|
7
11
|
|
@@ -26,6 +30,10 @@ An add-on that brings Bootstrap 5 and other goodies into ActiveAdmin.
|
|
26
30
|
$ rails g active_admin:install --use_webpacker
|
27
31
|
```
|
28
32
|
4. Install Adminterface
|
33
|
+
|
34
|
+
> This command will ask for permissions to overwrite files generated by ActiveAdmin. Make sure to review the files carefully and accept them.
|
35
|
+
> We recommend you commit your files before running this command to ensure you are aware of the changes made.
|
36
|
+
|
29
37
|
```bash
|
30
38
|
$ rails g adminterface:install
|
31
39
|
```
|
@@ -3,7 +3,7 @@ import HeaderToggler from '../lib/header_toggler'
|
|
3
3
|
document.addEventListener('DOMContentLoaded', () => {
|
4
4
|
const togglerTriggerList = [].slice.call(document.querySelectorAll('[data-aa-header-toggler]'))
|
5
5
|
togglerTriggerList.map((el) => {
|
6
|
-
const options = JSON.parse(el.dataset.aaHeaderToggler || {})
|
6
|
+
const options = JSON.parse(el.dataset.aaHeaderToggler || '{}')
|
7
7
|
|
8
8
|
return new HeaderToggler(el, options)
|
9
9
|
})
|
@@ -5,7 +5,7 @@ const initFlatpickr = (element) => {
|
|
5
5
|
const flatpickrTriggerList = [].slice.call(element.querySelectorAll('[data-aa-flatpickr]'))
|
6
6
|
|
7
7
|
flatpickrTriggerList.map((el) => {
|
8
|
-
const options = JSON.parse(el.dataset.aaFlatpickr || {})
|
8
|
+
const options = JSON.parse(el.dataset.aaFlatpickr || '{}')
|
9
9
|
const instance = flatpickr(el, options)
|
10
10
|
|
11
11
|
adminterface.addObserver(el, instance, 'flatpickr')
|
@@ -4,7 +4,7 @@ const initInputCounter = function (element) {
|
|
4
4
|
const inputCounterTriggerList = [].slice.call(element.querySelectorAll('[data-aa-input-counter]'))
|
5
5
|
|
6
6
|
inputCounterTriggerList.map((el) => {
|
7
|
-
const options = JSON.parse(el.dataset.aaInputCounter || {})
|
7
|
+
const options = JSON.parse(el.dataset.aaInputCounter || '{}')
|
8
8
|
|
9
9
|
return new InputCounter(el, options)
|
10
10
|
})
|
@@ -4,7 +4,7 @@ import initBootstrap from '../bootstrap'
|
|
4
4
|
const initPasswordVisibility = function (element) {
|
5
5
|
const visibilityTriggerList = [].slice.call(element.querySelectorAll('[data-aa-password-visibility]'))
|
6
6
|
visibilityTriggerList.map((el) => {
|
7
|
-
const options = JSON.parse(el.dataset.aaPasswordVisibility || {})
|
7
|
+
const options = JSON.parse(el.dataset.aaPasswordVisibility || '{}')
|
8
8
|
const instance = new PasswordVisibility(el, options)
|
9
9
|
|
10
10
|
initBootstrap(instance.eventTarget)
|
@@ -5,7 +5,7 @@ const initTomSelect = function (element) {
|
|
5
5
|
const tomSelectTriggerList = [].slice.call(element.querySelectorAll('[data-aa-tom-select]'))
|
6
6
|
|
7
7
|
tomSelectTriggerList.map((el) => {
|
8
|
-
const options = JSON.parse(el.dataset.aaTomSelect || {})
|
8
|
+
const options = JSON.parse(el.dataset.aaTomSelect || '{}')
|
9
9
|
const instance = new TomSelect(el, options)
|
10
10
|
|
11
11
|
adminterface.addObserver(el, instance, instance.constructor.name)
|
@@ -0,0 +1,110 @@
|
|
1
|
+
import fs from 'fs'
|
2
|
+
import path from 'path'
|
3
|
+
import CheckboxTogglerClass from '../checkbox_toggler'
|
4
|
+
|
5
|
+
describe('CheckboxToggler', () => {
|
6
|
+
const html = fs.readFileSync(path.resolve(__dirname, './checkbox_toggler.html'))
|
7
|
+
const container = '#find-me'
|
8
|
+
const options = 'options'
|
9
|
+
|
10
|
+
beforeEach(() => {
|
11
|
+
document.documentElement.innerHTML = html.toString()
|
12
|
+
})
|
13
|
+
|
14
|
+
afterEach(() => {
|
15
|
+
jest.restoreAllMocks()
|
16
|
+
})
|
17
|
+
|
18
|
+
test('constructor initiates correctly', () => {
|
19
|
+
const spyOnInit = jest.spyOn(CheckboxTogglerClass.prototype, '_init')
|
20
|
+
const spyOnBind = jest.spyOn(CheckboxTogglerClass.prototype, '_bind')
|
21
|
+
|
22
|
+
const CheckboxToggler = new CheckboxTogglerClass(container, options)
|
23
|
+
|
24
|
+
expect(CheckboxToggler.options).toStrictEqual(options)
|
25
|
+
expect(CheckboxToggler.container).toStrictEqual(container)
|
26
|
+
|
27
|
+
expect(spyOnInit).toHaveBeenCalledTimes(1)
|
28
|
+
expect(spyOnBind).toHaveBeenCalledTimes(1)
|
29
|
+
})
|
30
|
+
|
31
|
+
test('init throws error if container is null', () => {
|
32
|
+
expect(() => { new CheckboxTogglerClass(null, options) }).toThrow( // eslint-disable-line no-new
|
33
|
+
'Container element not found'
|
34
|
+
)
|
35
|
+
})
|
36
|
+
|
37
|
+
test('init throws error if toggle checkbox missing', () => {
|
38
|
+
const element = document.createElement('div')
|
39
|
+
element.id = 'find-me'
|
40
|
+
document.documentElement.innerHTML = element.outerHTML
|
41
|
+
|
42
|
+
expect(() => { new CheckboxTogglerClass(container, options) }).toThrow( // eslint-disable-line no-new
|
43
|
+
'"toggle all" checkbox not found'
|
44
|
+
)
|
45
|
+
})
|
46
|
+
|
47
|
+
test('init sets $container, $toggleAllCheckbox, $checkboxes', () => {
|
48
|
+
const $container = document.querySelector(container)
|
49
|
+
const $toggleAllCheckbox = $container.querySelector('.toggle_all')
|
50
|
+
const $checkboxes = $container.querySelectorAll('input[type="checkbox"]:not(.toggle_all)')
|
51
|
+
const CheckboxToggler = new CheckboxTogglerClass(container, options)
|
52
|
+
|
53
|
+
expect(CheckboxToggler.$container).toStrictEqual($container)
|
54
|
+
expect(CheckboxToggler.$toggleAllCheckbox).toStrictEqual($toggleAllCheckbox)
|
55
|
+
expect(CheckboxToggler.$checkboxes).toStrictEqual($checkboxes)
|
56
|
+
})
|
57
|
+
|
58
|
+
test('bind triggers addEventListener', () => {
|
59
|
+
const CheckboxToggler = new CheckboxTogglerClass(container, options)
|
60
|
+
const spyOnCheckboxesAddEvent = jest.spyOn(
|
61
|
+
CheckboxToggler.$checkboxes[0], 'addEventListener'
|
62
|
+
)
|
63
|
+
const spyOnToggleAllCheckboxAddEvent = jest.spyOn(
|
64
|
+
CheckboxToggler.$toggleAllCheckbox, 'addEventListener'
|
65
|
+
)
|
66
|
+
|
67
|
+
CheckboxToggler._bind()
|
68
|
+
|
69
|
+
expect(spyOnCheckboxesAddEvent).toHaveBeenCalledTimes(1)
|
70
|
+
expect(spyOnCheckboxesAddEvent).toHaveBeenCalledWith(
|
71
|
+
'change',
|
72
|
+
expect.any(Function)
|
73
|
+
)
|
74
|
+
|
75
|
+
expect(spyOnToggleAllCheckboxAddEvent).toHaveBeenCalledTimes(1)
|
76
|
+
expect(spyOnToggleAllCheckboxAddEvent).toHaveBeenCalledWith(
|
77
|
+
'change',
|
78
|
+
expect.any(Function)
|
79
|
+
)
|
80
|
+
})
|
81
|
+
|
82
|
+
test('didChangeCheckbox sets toggleAllCheckbox checked to true when all checked', () => {
|
83
|
+
const CheckboxToggler = new CheckboxTogglerClass(container, options)
|
84
|
+
CheckboxToggler._didChangeCheckbox()
|
85
|
+
|
86
|
+
expect(CheckboxToggler.$toggleAllCheckbox.checked).toBeTruthy()
|
87
|
+
expect(CheckboxToggler.$toggleAllCheckbox.indeterminate).toBeFalsy()
|
88
|
+
})
|
89
|
+
|
90
|
+
test('didChangeCheckbox sets toggleAllCheckbox checked to false when some checked', () => {
|
91
|
+
const inputElementToggleTwo = document.querySelector('input.toggle_two')
|
92
|
+
inputElementToggleTwo.checked = false
|
93
|
+
|
94
|
+
const CheckboxToggler = new CheckboxTogglerClass(container, options)
|
95
|
+
CheckboxToggler._didChangeCheckbox()
|
96
|
+
|
97
|
+
expect(CheckboxToggler.$toggleAllCheckbox.checked).toBeFalsy()
|
98
|
+
expect(CheckboxToggler.$toggleAllCheckbox.indeterminate).toBeTruthy()
|
99
|
+
})
|
100
|
+
|
101
|
+
test('didChangeToggleAllCheckbox', () => {
|
102
|
+
const CheckboxToggler = new CheckboxTogglerClass(container, options)
|
103
|
+
const setting = CheckboxToggler._didChangeToggleAllCheckbox()
|
104
|
+
|
105
|
+
expect(setting).toBeTruthy()
|
106
|
+
CheckboxToggler.$checkboxes.forEach(el => {
|
107
|
+
expect(el.checked).toStrictEqual(setting)
|
108
|
+
})
|
109
|
+
})
|
110
|
+
})
|
@@ -0,0 +1,14 @@
|
|
1
|
+
<div data-sortable="address">
|
2
|
+
<form class="has-many-list">
|
3
|
+
<fieldset class="has_many_fields">
|
4
|
+
<input type="checkbox" name="[_destroy]" checked>
|
5
|
+
<input type="text" name="[address]" value="Tokyo">
|
6
|
+
<a class="button has_many_remove"></a>
|
7
|
+
<a
|
8
|
+
class="button has_many_add"
|
9
|
+
data-html="<div></div>"
|
10
|
+
data-placeholder="script"
|
11
|
+
></a>
|
12
|
+
</fieldset>
|
13
|
+
</form>
|
14
|
+
</div>
|
@@ -0,0 +1,224 @@
|
|
1
|
+
import fs from 'fs'
|
2
|
+
import path from 'path'
|
3
|
+
import Sortable from 'sortablejs'
|
4
|
+
import HasManyClass from '../has_many'
|
5
|
+
|
6
|
+
jest.mock('sortablejs')
|
7
|
+
|
8
|
+
describe('HasMany', () => {
|
9
|
+
const html = fs.readFileSync(path.resolve(__dirname, './has_many.html'))
|
10
|
+
let element
|
11
|
+
|
12
|
+
const options = {
|
13
|
+
list: '.has-many-list',
|
14
|
+
item: 'fieldset.has_many_fields',
|
15
|
+
addLink: 'a.button.has_many_add',
|
16
|
+
removeLink: 'a.button.has_many_remove',
|
17
|
+
destroyInput: 'input[name$="[_destroy]"]'
|
18
|
+
}
|
19
|
+
|
20
|
+
beforeEach(() => {
|
21
|
+
document.documentElement.innerHTML = html
|
22
|
+
element = document.querySelector('div')
|
23
|
+
global.adminterface = {
|
24
|
+
addObserver: jest.fn()
|
25
|
+
}
|
26
|
+
})
|
27
|
+
|
28
|
+
afterEach(() => {
|
29
|
+
jest.restoreAllMocks()
|
30
|
+
})
|
31
|
+
|
32
|
+
test('constructor initiates correctly', () => {
|
33
|
+
const spyOnBind = jest.spyOn(HasManyClass.prototype, '_bind')
|
34
|
+
spyOnBind.mockImplementation(() => {})
|
35
|
+
|
36
|
+
const HasMany = new HasManyClass(element)
|
37
|
+
expect(HasMany.element).toStrictEqual(element)
|
38
|
+
|
39
|
+
expect(spyOnBind).toHaveBeenCalledTimes(1)
|
40
|
+
})
|
41
|
+
|
42
|
+
test('bind calls bindEvents, initSortable and admininterface', () => {
|
43
|
+
const spyOnBindEvents = jest.spyOn(HasManyClass.prototype, '_bindEvents')
|
44
|
+
spyOnBindEvents.mockImplementation(() => {})
|
45
|
+
|
46
|
+
const spyOnInitSortable = jest.spyOn(HasManyClass.prototype, '_initSortable')
|
47
|
+
spyOnInitSortable.mockImplementation(() => {})
|
48
|
+
|
49
|
+
const HasMany = new HasManyClass(element)
|
50
|
+
|
51
|
+
expect(spyOnBindEvents).toHaveBeenCalledWith(element)
|
52
|
+
expect(spyOnInitSortable).toHaveBeenCalledTimes(1)
|
53
|
+
expect(global.adminterface.addObserver).toHaveBeenCalledTimes(1)
|
54
|
+
expect(global.adminterface.addObserver).toHaveBeenCalledWith(
|
55
|
+
element,
|
56
|
+
HasMany,
|
57
|
+
HasMany.constructor.name,
|
58
|
+
[]
|
59
|
+
)
|
60
|
+
})
|
61
|
+
|
62
|
+
test('initSortable assigns new instance of Sortable to sortable property', () => {
|
63
|
+
const spyOnBindEvents = jest.spyOn(HasManyClass.prototype, '_bindEvents')
|
64
|
+
spyOnBindEvents.mockImplementation(() => {})
|
65
|
+
|
66
|
+
const HasMany = new HasManyClass(element)
|
67
|
+
|
68
|
+
const MockSortableArgument = {
|
69
|
+
handle: '.handle',
|
70
|
+
animation: 150,
|
71
|
+
ghostClass: 'sortable-placeholder',
|
72
|
+
dragClass: 'sortable-drag',
|
73
|
+
onUpdate: expect.any(Function)
|
74
|
+
}
|
75
|
+
|
76
|
+
expect(Sortable).toHaveBeenCalledTimes(1)
|
77
|
+
expect(Sortable).toHaveBeenCalledWith(
|
78
|
+
document.querySelector(HasMany.options.list),
|
79
|
+
MockSortableArgument
|
80
|
+
)
|
81
|
+
const sortable = new Sortable(
|
82
|
+
document.querySelector(HasMany.options.list),
|
83
|
+
MockSortableArgument
|
84
|
+
)
|
85
|
+
expect(HasMany.sortable.toString()).toStrictEqual(sortable.toString())
|
86
|
+
})
|
87
|
+
|
88
|
+
test('recomputePosition changes sortableInput value', () => {
|
89
|
+
const query = 'input[name$="[address]"]'
|
90
|
+
const sortableInput = element.querySelector(query)
|
91
|
+
|
92
|
+
expect(sortableInput.value.length).not.toEqual(0)
|
93
|
+
|
94
|
+
const HasMany = new HasManyClass(element)
|
95
|
+
HasMany._recomputePosition()
|
96
|
+
|
97
|
+
expect(sortableInput.value.length).toEqual(0)
|
98
|
+
})
|
99
|
+
|
100
|
+
test('bindDestroyEvent called with element and change event added', () => {
|
101
|
+
const spyOnBindDestroyEvent = jest.spyOn(
|
102
|
+
HasManyClass.prototype,
|
103
|
+
'_bindDestroyEvent'
|
104
|
+
)
|
105
|
+
const destroyInput = document.querySelector(options.destroyInput)
|
106
|
+
destroyInput.addEventListener = jest.fn()
|
107
|
+
|
108
|
+
new HasManyClass(element) // eslint-disable-line no-new
|
109
|
+
|
110
|
+
expect(spyOnBindDestroyEvent).toHaveBeenCalledTimes(1)
|
111
|
+
expect(spyOnBindDestroyEvent).toHaveBeenCalledWith(destroyInput)
|
112
|
+
|
113
|
+
expect(destroyInput.addEventListener).toHaveBeenCalledTimes(1)
|
114
|
+
expect(destroyInput.addEventListener).toHaveBeenCalledWith(
|
115
|
+
'change',
|
116
|
+
expect.any(Function)
|
117
|
+
)
|
118
|
+
})
|
119
|
+
|
120
|
+
test('bindRemoveEvent called with element and click event added', () => {
|
121
|
+
const spyOnBindRemoveEvent = jest.spyOn(
|
122
|
+
HasManyClass.prototype,
|
123
|
+
'_bindRemoveEvent'
|
124
|
+
)
|
125
|
+
const removeLinks = document.querySelector(options.removeLink)
|
126
|
+
removeLinks.addEventListener = jest.fn()
|
127
|
+
|
128
|
+
new HasManyClass(element) // eslint-disable-line no-new
|
129
|
+
|
130
|
+
expect(spyOnBindRemoveEvent).toHaveBeenCalledTimes(1)
|
131
|
+
expect(spyOnBindRemoveEvent).toHaveBeenCalledWith(removeLinks)
|
132
|
+
|
133
|
+
expect(removeLinks.addEventListener).toHaveBeenCalledTimes(1)
|
134
|
+
expect(removeLinks.addEventListener).toHaveBeenCalledWith(
|
135
|
+
'click',
|
136
|
+
expect.any(Function)
|
137
|
+
)
|
138
|
+
})
|
139
|
+
|
140
|
+
test('bindRemoveEventCallBack', () => {
|
141
|
+
const mockEvent = {
|
142
|
+
preventDefault: () => {},
|
143
|
+
target: document.body.querySelector('fieldset')
|
144
|
+
}
|
145
|
+
const spyOnRecomputePosition = jest.spyOn(
|
146
|
+
HasManyClass.prototype,
|
147
|
+
'_recomputePosition'
|
148
|
+
)
|
149
|
+
const HasMany = new HasManyClass(element)
|
150
|
+
const spyOnDispatch = jest.spyOn(HasMany.element, 'dispatchEvent')
|
151
|
+
|
152
|
+
const fieldSet = mockEvent.target.closest(HasMany.options.item)
|
153
|
+
fieldSet.remove = jest.fn()
|
154
|
+
|
155
|
+
const parent = HasMany.element
|
156
|
+
const removeBeforeEvent = HasMany.events.removeBefore
|
157
|
+
removeBeforeEvent.detail = { fieldSet, parent }
|
158
|
+
const removeAfterEvent = HasMany.events.removeAfter
|
159
|
+
removeAfterEvent.detail = { fieldSet, parent }
|
160
|
+
|
161
|
+
const output = HasMany._bindRemoveEventCallBack(mockEvent)
|
162
|
+
|
163
|
+
expect(spyOnRecomputePosition).toHaveBeenCalledTimes(1)
|
164
|
+
expect(spyOnDispatch).toHaveBeenCalledTimes(2)
|
165
|
+
expect(spyOnDispatch.mock.calls).toMatchObject([
|
166
|
+
[removeBeforeEvent],
|
167
|
+
[removeAfterEvent]
|
168
|
+
])
|
169
|
+
expect(fieldSet.remove).toHaveBeenCalledTimes(1)
|
170
|
+
expect(output).toBeTruthy()
|
171
|
+
})
|
172
|
+
|
173
|
+
test('bindAddEvent called with correctly element and click event added', () => {
|
174
|
+
const spyOnBindAddEvent = jest.spyOn(
|
175
|
+
HasManyClass.prototype,
|
176
|
+
'_bindAddEvent'
|
177
|
+
)
|
178
|
+
|
179
|
+
const addLinks = document.querySelector(options.addLink)
|
180
|
+
addLinks.addEventListener = jest.fn()
|
181
|
+
|
182
|
+
new HasManyClass(element) // eslint-disable-line no-new
|
183
|
+
|
184
|
+
expect(spyOnBindAddEvent).toHaveBeenCalledTimes(1)
|
185
|
+
expect(spyOnBindAddEvent).toHaveBeenCalledWith(addLinks)
|
186
|
+
|
187
|
+
expect(addLinks.addEventListener).toHaveBeenCalledTimes(1)
|
188
|
+
expect(addLinks.addEventListener).toHaveBeenCalledWith(
|
189
|
+
'click',
|
190
|
+
expect.any(Function)
|
191
|
+
)
|
192
|
+
})
|
193
|
+
|
194
|
+
test('bindAddEventCallBack', () => {
|
195
|
+
const mockEvent = {
|
196
|
+
preventDefault: () => {},
|
197
|
+
target: document.body.querySelector(options.addLink)
|
198
|
+
}
|
199
|
+
const HasMany = new HasManyClass(element)
|
200
|
+
const spyOnDispatchEvent = jest.spyOn(HasMany.element, 'dispatchEvent')
|
201
|
+
const $list = HasMany.element.querySelector(HasMany.options.list)
|
202
|
+
const spyOnListAppendChild = jest.spyOn($list, 'appendChild')
|
203
|
+
const spyOnBindEvents = jest.spyOn(HasMany, '_bindEvents')
|
204
|
+
const spyOnRecomputePosition = jest.spyOn(HasMany, '_recomputePosition')
|
205
|
+
const beforeAdd = HasMany.events.addBefore
|
206
|
+
const parent = HasMany.element
|
207
|
+
const datasetHTML = document.createElement('div')
|
208
|
+
|
209
|
+
const output = HasMany._bindAddEventCallBack(mockEvent)
|
210
|
+
|
211
|
+
expect(spyOnDispatchEvent).toHaveBeenCalledTimes(2)
|
212
|
+
expect(spyOnDispatchEvent.mock.calls).toMatchObject([
|
213
|
+
[beforeAdd, [parent]],
|
214
|
+
[HasMany.events.addAfter]
|
215
|
+
])
|
216
|
+
|
217
|
+
expect(spyOnListAppendChild).toHaveBeenCalledTimes(1)
|
218
|
+
expect(spyOnListAppendChild).toHaveBeenCalledWith(datasetHTML)
|
219
|
+
expect(spyOnBindEvents).toHaveBeenCalledTimes(1)
|
220
|
+
expect(spyOnBindEvents).toHaveBeenCalledWith(datasetHTML)
|
221
|
+
expect(spyOnRecomputePosition).toHaveBeenCalledTimes(1)
|
222
|
+
expect(output).toBeTruthy()
|
223
|
+
})
|
224
|
+
})
|
@@ -51,54 +51,57 @@ class HasMany {
|
|
51
51
|
}
|
52
52
|
|
53
53
|
_bindAddEvent (el) {
|
54
|
-
el.addEventListener('click', (e) =>
|
55
|
-
|
56
|
-
const el = e.target
|
57
|
-
const parent = this.element
|
54
|
+
el.addEventListener('click', (e) => this._bindAddEventCallBack(e))
|
55
|
+
}
|
58
56
|
|
59
|
-
|
60
|
-
|
57
|
+
_bindAddEventCallBack (e) {
|
58
|
+
let beforeAdd
|
59
|
+
const el = e.target
|
60
|
+
const parent = this.element
|
61
|
+
e.preventDefault()
|
62
|
+
parent.dispatchEvent(beforeAdd = this.events.addBefore, [parent])
|
61
63
|
|
62
|
-
|
63
|
-
|
64
|
-
|
64
|
+
if (!beforeAdd.defaultPrevented) {
|
65
|
+
let index = parent.dataset.hasManyIndex || parent.querySelectorAll(this.options.item).length - 1
|
66
|
+
parent.setAttribute('data-has-many-index', ++index)
|
65
67
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
68
|
+
const regex = new RegExp(el.dataset.placeholder, 'g')
|
69
|
+
const html = el.dataset.html.replace(regex, index)
|
70
|
+
const newNode = document.createElement('div')
|
71
|
+
newNode.innerHTML = html
|
70
72
|
|
71
|
-
|
72
|
-
|
73
|
+
const fieldset = newNode.firstElementChild
|
74
|
+
const $list = this.element.querySelector(this.options.list)
|
73
75
|
|
74
|
-
|
75
|
-
|
76
|
-
|
76
|
+
$list.appendChild(fieldset)
|
77
|
+
this._bindEvents(fieldset)
|
78
|
+
this._recomputePosition()
|
77
79
|
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
})
|
80
|
+
const addAfterEvent = this.events.addAfter
|
81
|
+
addAfterEvent.detail = { fieldset, parent }
|
82
|
+
return parent.dispatchEvent(this.events.addAfter)
|
83
|
+
}
|
83
84
|
}
|
84
85
|
|
85
86
|
_bindRemoveEvent (el) {
|
86
|
-
el.addEventListener('click', (e) =>
|
87
|
-
|
88
|
-
const parent = this.element
|
89
|
-
const fieldset = el.closest(this.options.item)
|
90
|
-
const removeBeforeEvent = this.events.removeBefore
|
91
|
-
const removeAfterEvent = this.events.removeAfter
|
92
|
-
|
93
|
-
e.preventDefault()
|
94
|
-
this._recomputePosition()
|
87
|
+
el.addEventListener('click', (e) => this._bindRemoveEventCallBack(e))
|
88
|
+
}
|
95
89
|
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
90
|
+
_bindRemoveEventCallBack (e) {
|
91
|
+
const el = e.target
|
92
|
+
const parent = this.element
|
93
|
+
const fieldset = el.closest(this.options.item)
|
94
|
+
const removeBeforeEvent = this.events.removeBefore
|
95
|
+
const removeAfterEvent = this.events.removeAfter
|
96
|
+
|
97
|
+
e.preventDefault()
|
98
|
+
this._recomputePosition()
|
99
|
+
|
100
|
+
removeBeforeEvent.detail = { fieldset, parent }
|
101
|
+
removeAfterEvent.detail = { fieldset, parent }
|
102
|
+
parent.dispatchEvent(removeBeforeEvent)
|
103
|
+
fieldset.remove()
|
104
|
+
return parent.dispatchEvent(removeAfterEvent)
|
102
105
|
}
|
103
106
|
|
104
107
|
_bindEvents (el) {
|
data/lib/adminterface/version.rb
CHANGED
@@ -11,6 +11,10 @@ module Adminterface
|
|
11
11
|
ActiveRecord::Migration.next_migration_number(next_migration_number)
|
12
12
|
end
|
13
13
|
|
14
|
+
def install_action_text
|
15
|
+
rails_command "action_text:install"
|
16
|
+
end
|
17
|
+
|
14
18
|
def create_initializer
|
15
19
|
template "active_admin_comment_action_text.rb",
|
16
20
|
"config/initializers/active_admin_comment_action_text.rb"
|
@@ -2,10 +2,10 @@ module Adminterface
|
|
2
2
|
module Generators
|
3
3
|
class ConfigsGenerator < Rails::Generators::Base
|
4
4
|
desc "Copies config files into a directory for customization"
|
5
|
-
argument :namespace, type: :string, default: "Admin"
|
6
|
-
|
7
5
|
source_root File.expand_path("templates", __dir__)
|
8
6
|
|
7
|
+
argument :namespace, type: :string, default: "Admin"
|
8
|
+
|
9
9
|
def copy
|
10
10
|
directory Adminterface::Engine.root.join("lib/adminterface/fixtures"),
|
11
11
|
"config/adminterface/#{namespace.underscore}/"
|
@@ -4,37 +4,35 @@ module Adminterface
|
|
4
4
|
desc "Installs Adminterface and generates necessary files and migrations"
|
5
5
|
source_root File.expand_path("templates", __dir__)
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
|
7
|
+
class_option :skip_comments,
|
8
|
+
type: :boolean, default: false,
|
9
|
+
desc: "Skip installation of ActionText for comments"
|
10
|
+
class_option :skip_examples,
|
11
|
+
type: :boolean, default: false,
|
12
|
+
desc: "Skip adding sample admin files"
|
13
|
+
class_option :use_webpacker,
|
14
|
+
type: :boolean, default: true,
|
15
|
+
desc: "Install assets with webpacker"
|
16
|
+
class_option :version,
|
17
|
+
aliases: "-v", type: :string, default: Adminterface::VERSION,
|
18
|
+
desc: "Install with a specific version"
|
10
19
|
|
11
20
|
def install_assets
|
12
|
-
|
13
|
-
generate "adminterface:webpacker"
|
14
|
-
end
|
15
|
-
end
|
21
|
+
return unless options[:use_webpacker]
|
16
22
|
|
17
|
-
|
18
|
-
generate "adminterface:comments"
|
23
|
+
generate "adminterface:webpacker -v #{options[:version]}"
|
19
24
|
end
|
20
25
|
|
21
|
-
def
|
22
|
-
|
23
|
-
end
|
26
|
+
def install_comments
|
27
|
+
return if options[:skip_comments]
|
24
28
|
|
25
|
-
|
29
|
+
generate "adminterface:comments"
|
30
|
+
end
|
26
31
|
|
27
|
-
def
|
28
|
-
return
|
32
|
+
def add_examples
|
33
|
+
return if options[:skip_examples]
|
29
34
|
|
30
|
-
|
31
|
-
if yes?("Install webpacker and continue?")
|
32
|
-
gem "webpacker"
|
33
|
-
rails_command "webpacker:install"
|
34
|
-
yield
|
35
|
-
else
|
36
|
-
puts Rainbow("Installation aborted").red
|
37
|
-
end
|
35
|
+
template "dashboard.rb", "app/admin/dashboard.rb"
|
38
36
|
end
|
39
37
|
end
|
40
38
|
end
|
@@ -2,13 +2,13 @@ module Adminterface
|
|
2
2
|
module Generators
|
3
3
|
class ViewsGenerator < Rails::Generators::Base
|
4
4
|
desc "Copies view templates for customization"
|
5
|
-
|
6
5
|
source_root Adminterface::Engine.root.join("app/views")
|
7
6
|
|
8
7
|
def copy
|
9
8
|
directory "active_admin", "app/views/active_admin"
|
10
9
|
directory "kaminari/active_admin", "app/views/kaminari/active_admin"
|
11
|
-
directory "layouts/active_admin_logged_out.html.erb",
|
10
|
+
directory "layouts/active_admin_logged_out.html.erb",
|
11
|
+
"app/views/layouts/active_admin_logged_out.html.erb"
|
12
12
|
end
|
13
13
|
end
|
14
14
|
end
|
@@ -2,11 +2,24 @@ module Adminterface
|
|
2
2
|
module Generators
|
3
3
|
class WebpackerGenerator < Rails::Generators::Base
|
4
4
|
desc "Install Stylesheets and JavaScripts using Webpacker"
|
5
|
+
source_root File.expand_path("templates", __dir__)
|
6
|
+
|
5
7
|
class_option :version,
|
6
8
|
aliases: "-v", type: :string, default: Adminterface::VERSION,
|
7
9
|
desc: "Install with a specific npm package version"
|
8
10
|
|
9
|
-
|
11
|
+
def install_webpacker
|
12
|
+
return if webpacker_installed?
|
13
|
+
|
14
|
+
puts Rainbow("Adminterface requires webpacker:").yellow
|
15
|
+
if yes?("Install webpacker and continue?")
|
16
|
+
gem "webpacker"
|
17
|
+
rails_command "webpacker:install"
|
18
|
+
else
|
19
|
+
puts Rainbow("Installation aborted").red
|
20
|
+
abort
|
21
|
+
end
|
22
|
+
end
|
10
23
|
|
11
24
|
def install_packages
|
12
25
|
insert_into_file "config/webpack/environment.js", after: /require\(('|")@rails\/webpacker\1\);?\n/ do
|
@@ -18,7 +31,7 @@ module Adminterface
|
|
18
31
|
EOF
|
19
32
|
end
|
20
33
|
|
21
|
-
run "yarn add @cmdbrew/adminterface
|
34
|
+
run "yarn add @cmdbrew/adminterface@#{npm_version}"
|
22
35
|
end
|
23
36
|
|
24
37
|
def install_assets
|
@@ -44,11 +57,12 @@ module Adminterface
|
|
44
57
|
|
45
58
|
private
|
46
59
|
|
47
|
-
def
|
48
|
-
|
49
|
-
|
60
|
+
def webpacker_installed?
|
61
|
+
defined?(Webpacker)
|
62
|
+
end
|
50
63
|
|
51
|
-
|
64
|
+
def npm_version
|
65
|
+
options[:version].gsub(".rc", "-rc")
|
52
66
|
end
|
53
67
|
end
|
54
68
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: adminterface
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- I-Lung Lee
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-10-
|
11
|
+
date: 2021-10-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activeadmin
|
@@ -80,7 +80,8 @@ dependencies:
|
|
80
80
|
- - ">="
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: '0.18'
|
83
|
-
description:
|
83
|
+
description: A gem that brings Bootstrap 5, advanced customizability, and other goodies
|
84
|
+
into ActiveAdmin
|
84
85
|
email:
|
85
86
|
- ilung@hey.com
|
86
87
|
executables: []
|
@@ -174,8 +175,12 @@ files:
|
|
174
175
|
- app/javascript/adminterface/initializers/plugins/input_counter.js
|
175
176
|
- app/javascript/adminterface/initializers/plugins/password_visibility.js
|
176
177
|
- app/javascript/adminterface/initializers/plugins/tom_select.js
|
178
|
+
- app/javascript/adminterface/lib/__tests__/checkbox_toggler.html
|
179
|
+
- app/javascript/adminterface/lib/__tests__/checkbox_toggler.spec.js
|
177
180
|
- app/javascript/adminterface/lib/__tests__/detached_dropdown.spec.js
|
178
181
|
- app/javascript/adminterface/lib/__tests__/filters.spec.js
|
182
|
+
- app/javascript/adminterface/lib/__tests__/has_many.html
|
183
|
+
- app/javascript/adminterface/lib/__tests__/has_many.spec.js
|
179
184
|
- app/javascript/adminterface/lib/__tests__/utils.spec.js
|
180
185
|
- app/javascript/adminterface/lib/batch_actions.js
|
181
186
|
- app/javascript/adminterface/lib/checkbox_toggler.js
|
@@ -377,5 +382,5 @@ requirements: []
|
|
377
382
|
rubygems_version: 3.2.22
|
378
383
|
signing_key:
|
379
384
|
specification_version: 4
|
380
|
-
summary:
|
385
|
+
summary: Build functional and beautiful web apps faster with ActiveAdmin
|
381
386
|
test_files: []
|