adminterface 0.2.0 → 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
[![CI](https://github.com/CMDBrew/adminterface/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/CMDBrew/adminterface/actions/workflows/ci.yml)
|
3
|
+
[![Maintainability](https://api.codeclimate.com/v1/badges/4dbedfdf21a2d675d2ca/maintainability)](https://codeclimate.com/github/CMDBrew/adminterface/maintainability)
|
4
|
+
[![Test Coverage](https://api.codeclimate.com/v1/badges/4dbedfdf21a2d675d2ca/test_coverage)](https://codeclimate.com/github/CMDBrew/adminterface/test_coverage)
|
5
|
+
[![gem](https://img.shields.io/gem/v/adminterface?color=%23E9573F&label=rubygems.org)](https://rubygems.org/gems/adminterface)
|
6
|
+
[![npm](https://img.shields.io/npm/v/@cmdbrew/adminterface?color=%230273B4&label=npmjs.com)](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
|
[![](preview.jpg)](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: []
|