plok 0.2.12 → 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/app/assets/javascripts/jquery-ui.autocomplete.html.js +40 -0
- data/app/assets/javascripts/plok/searchable.js +26 -0
- data/app/assets/javascripts/plok/sidebar.js +66 -0
- data/app/assets/stylesheets/plok/_autocomplete.scss +43 -0
- data/app/assets/stylesheets/plok/_sidebar.scss +86 -0
- data/app/assets/stylesheets/plok/_sidebar_compact.scss +66 -0
- data/app/models/concerns/plok/searchable.rb +96 -0
- data/app/models/queued_task.rb +7 -0
- data/app/models/search_index.rb +5 -0
- data/app/models/search_module.rb +11 -0
- data/config/initializers/module.rb +17 -0
- data/db/migrate/20220923162300_create_search_indices.rb +19 -0
- data/db/migrate/20220923164100_create_search_modules.rb +16 -0
- data/lib/generators/plok/sidebar/USAGE +8 -0
- data/lib/generators/plok/sidebar/sidebar_generator.rb +114 -0
- data/lib/generators/plok/sidebar/templates/_menu_item.html.erb +16 -0
- data/lib/generators/plok/sidebar/templates/_menu_items.html.erb +9 -0
- data/lib/generators/plok/sidebar/templates/_offcanvas_menu.html.erb +11 -0
- data/lib/generators/plok/sidebar/templates/_wrapper.html.erb +19 -0
- data/lib/plok/engine.rb +5 -2
- data/lib/plok/search/backend.rb +22 -0
- data/lib/plok/search/base.rb +71 -0
- data/lib/plok/search/result_objects/base.rb +74 -0
- data/lib/plok/search/term.rb +23 -0
- data/lib/plok/version.rb +1 -1
- data/lib/tasks/plok_tasks.rake +11 -4
- data/spec/factories/search_indices.rb +6 -0
- data/spec/factories/search_modules.rb +5 -0
- data/spec/support/fake_ar_model.rb +95 -0
- data/spec/support/plok/searchable.rb +100 -0
- metadata +29 -4
- data/config/plok.yml +0 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: caeb408fa4077fd24c19a153f273eb83f21d1da02a601035838e054080b80f85
|
4
|
+
data.tar.gz: e9ebfd53e89fdb4adddfb9498e2c7411ce994caf53039e0136459e90dd4a838e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 728e58a7153d8bb7bc4e19fde5b676ebec49d671b93d724b0c895d261e07e77bd7c6c347bf97c0dbf6dbc80e9c510e0c3f0bdf161713cf38b407a758f3da6ad0
|
7
|
+
data.tar.gz: d14e3d051253c34406c8736d4785b2ea7a1fba11f9e1dc46302b4ace6965d56f599b847ea604838aef90822595b7a526bcf18958f1e86a395e6a39a0c186cb18
|
@@ -0,0 +1,40 @@
|
|
1
|
+
/*
|
2
|
+
* jQuery UI Autocomplete HTML Extension
|
3
|
+
*
|
4
|
+
* Copyright 2010, Scott González (http://scottgonzalez.com)
|
5
|
+
* Dual licensed under the MIT or GPL Version 2 licenses.
|
6
|
+
*
|
7
|
+
* http://github.com/scottgonzalez/jquery-ui-extensions
|
8
|
+
*/
|
9
|
+
(function( $ ) {
|
10
|
+
|
11
|
+
var proto = $.ui.autocomplete.prototype,
|
12
|
+
initSource = proto._initSource;
|
13
|
+
|
14
|
+
function filter( array, term ) {
|
15
|
+
var matcher = new RegExp( $.ui.autocomplete.escapeRegex(term), "i" );
|
16
|
+
return $.grep( array, function(value) {
|
17
|
+
return matcher.test( $( "<div>" ).html( value.label || value.value || value ).text() );
|
18
|
+
});
|
19
|
+
}
|
20
|
+
|
21
|
+
$.extend( proto, {
|
22
|
+
_initSource: function() {
|
23
|
+
if ( this.options.html && $.isArray(this.options.source) ) {
|
24
|
+
this.source = function( request, response ) {
|
25
|
+
response( filter( this.options.source, request.term ) );
|
26
|
+
};
|
27
|
+
} else {
|
28
|
+
initSource.call( this );
|
29
|
+
}
|
30
|
+
},
|
31
|
+
|
32
|
+
_renderItem: function( ul, item) {
|
33
|
+
return $( "<li></li>" )
|
34
|
+
.data( "item.autocomplete", item )
|
35
|
+
.append( $( "<a></a>" )[ this.options.html ? "html" : "text" ]( item.label ) )
|
36
|
+
.appendTo( ul );
|
37
|
+
}
|
38
|
+
});
|
39
|
+
|
40
|
+
})( jQuery );
|
@@ -0,0 +1,26 @@
|
|
1
|
+
var search = search || {
|
2
|
+
init: function() {
|
3
|
+
this.target().autocomplete({
|
4
|
+
minLength: 2,
|
5
|
+
source: search.target().parents('form').attr('action'),
|
6
|
+
select: search.select,
|
7
|
+
html: true
|
8
|
+
}).on('keypress', this.keypress_listener);
|
9
|
+
},
|
10
|
+
|
11
|
+
keypress_listener: function(e) {
|
12
|
+
var code = (e.keyCode ? e.keyCode : e.which);
|
13
|
+
if(code == 13) return false;
|
14
|
+
},
|
15
|
+
|
16
|
+
select: function(event, ui) {
|
17
|
+
window.location = ui.item.value;
|
18
|
+
return false;
|
19
|
+
},
|
20
|
+
|
21
|
+
target: function() {
|
22
|
+
return $('#search input');
|
23
|
+
}
|
24
|
+
};
|
25
|
+
|
26
|
+
$(function(){ search.init(); });
|
@@ -0,0 +1,66 @@
|
|
1
|
+
var sidebar = {
|
2
|
+
init: () => {
|
3
|
+
sidebar.bind_mouse_events();
|
4
|
+
$('#hamburger').on('click', sidebar.toggle_hamburger_click_listener)
|
5
|
+
$(document).on('click', sidebar.document_click_listener)
|
6
|
+
},
|
7
|
+
|
8
|
+
bind_mouse_events: () => {
|
9
|
+
$('.wrapper.compact ul li.top-level')
|
10
|
+
.on('mouseenter', sidebar.list_item_mouseenter_listener)
|
11
|
+
.on('mouseleave', sidebar.list_item_mouseleave_listener)
|
12
|
+
},
|
13
|
+
|
14
|
+
close_all_submenus: () => {
|
15
|
+
$('[id^="nav-"]').each((_i, item) => { $(item).removeClass('show') })
|
16
|
+
$('a.top-level-anchor').each((_i, item) => { $(item).removeClass('bg-secondary') })
|
17
|
+
},
|
18
|
+
|
19
|
+
document_click_listener: (e) => {
|
20
|
+
if($('.compact:visible').length == 0) return
|
21
|
+
if($(e.target).closest('.sidebar-menu').length === 0)
|
22
|
+
sidebar.close_all_submenus();
|
23
|
+
},
|
24
|
+
|
25
|
+
list_item_mouseenter_listener: (e) => {
|
26
|
+
sidebar.list_item_mouseleave_listener(e)
|
27
|
+
|
28
|
+
let anchor = $(e.target)
|
29
|
+
if(!anchor.hasClass('top-level-anchor')) return
|
30
|
+
anchor.addClass('bg-secondary')
|
31
|
+
$(anchor.attr('href')).addClass('show')
|
32
|
+
},
|
33
|
+
|
34
|
+
list_item_mouseleave_listener: (e) => {
|
35
|
+
let anchor = $(e.target)
|
36
|
+
if(!anchor.hasClass('top-level-anchor')) return
|
37
|
+
sidebar.close_all_submenus();
|
38
|
+
},
|
39
|
+
|
40
|
+
toggle_hamburger_click_listener: e => {
|
41
|
+
e.preventDefault()
|
42
|
+
$('.wrapper').toggleClass('compact')
|
43
|
+
|
44
|
+
$('[id^="nav-"]').each((index, item) => {
|
45
|
+
if($('.wrapper').hasClass('compact')) {
|
46
|
+
$(item).removeClass('show')
|
47
|
+
}
|
48
|
+
})
|
49
|
+
|
50
|
+
if($('.wrapper').hasClass('compact')) {
|
51
|
+
sidebar.bind_mouse_events()
|
52
|
+
$('.sidebar-menu').removeClass('overflow-auto')
|
53
|
+
} else {
|
54
|
+
sidebar.unbind_mouse_events()
|
55
|
+
$('.sidebar-menu').addClass('overflow-auto')
|
56
|
+
}
|
57
|
+
},
|
58
|
+
|
59
|
+
unbind_mouse_events: () => {
|
60
|
+
$('.wrapper ul li.top-level')
|
61
|
+
.off('mouseenter')
|
62
|
+
.off('mouseleave')
|
63
|
+
}
|
64
|
+
}
|
65
|
+
|
66
|
+
$(() => { sidebar.init() })
|
@@ -0,0 +1,43 @@
|
|
1
|
+
.ui-autocomplete {
|
2
|
+
width: 24rem !important;
|
3
|
+
padding: 0;
|
4
|
+
z-index: 9999;
|
5
|
+
filter: drop-shadow(5px 5px 5px #666);
|
6
|
+
}
|
7
|
+
|
8
|
+
// Shows up during autocompletion sometimes.
|
9
|
+
.ui-helper-hidden-accessible {
|
10
|
+
display: none;
|
11
|
+
}
|
12
|
+
|
13
|
+
li.ui-menu-item {
|
14
|
+
list-style: none;
|
15
|
+
cursor: pointer;
|
16
|
+
border-bottom: 1px solid #ccc;
|
17
|
+
padding: 5px;
|
18
|
+
|
19
|
+
&:last-child {
|
20
|
+
border: none;
|
21
|
+
margin-bottom: 0;
|
22
|
+
padding-bottom: 0;
|
23
|
+
}
|
24
|
+
|
25
|
+
&:hover {
|
26
|
+
background-color: $primary;
|
27
|
+
color: white !important;
|
28
|
+
}
|
29
|
+
|
30
|
+
a.ui-menu-item-wrapper {
|
31
|
+
border: none;
|
32
|
+
|
33
|
+
&.ui-state-active {
|
34
|
+
background: none;
|
35
|
+
color: inherit;
|
36
|
+
text-decoration: none;
|
37
|
+
|
38
|
+
.text-muted {
|
39
|
+
color: white !important;
|
40
|
+
}
|
41
|
+
}
|
42
|
+
}
|
43
|
+
}
|
@@ -0,0 +1,86 @@
|
|
1
|
+
.sidebar-menu {
|
2
|
+
bottom: 0;
|
3
|
+
position: fixed;
|
4
|
+
top: 0;
|
5
|
+
width: var(--sidebar-width);
|
6
|
+
transition: .5s;
|
7
|
+
z-index: 10;
|
8
|
+
|
9
|
+
a.logo {
|
10
|
+
display: block;
|
11
|
+
letter-spacing: 0.1em;
|
12
|
+
line-height: 45px;
|
13
|
+
text-decoration: none;
|
14
|
+
text-transform: uppercase;
|
15
|
+
}
|
16
|
+
|
17
|
+
a.home-icon {
|
18
|
+
display: none !important;
|
19
|
+
}
|
20
|
+
|
21
|
+
ul {
|
22
|
+
list-style-type: none;
|
23
|
+
padding-left: 0;
|
24
|
+
|
25
|
+
li {
|
26
|
+
cursor: pointer;
|
27
|
+
|
28
|
+
a {
|
29
|
+
color: #fff; // $color-contrast-light from the Bootstrap gem.
|
30
|
+
display: block;
|
31
|
+
font-size: 1rem;
|
32
|
+
padding: 10px;
|
33
|
+
padding-left: var(--sidebar-menu-item-left-margin);
|
34
|
+
text-decoration: none !important;
|
35
|
+
|
36
|
+
&:hover {
|
37
|
+
background-color: #6c757d; // $secondary from the Bootstrap gem.
|
38
|
+
}
|
39
|
+
}
|
40
|
+
|
41
|
+
&.top-level {
|
42
|
+
letter-spacing: .05em;
|
43
|
+
position: relative;
|
44
|
+
|
45
|
+
.fa {
|
46
|
+
font-size: 14px;
|
47
|
+
width: var(--sidebar-icon-width);
|
48
|
+
}
|
49
|
+
|
50
|
+
.text {
|
51
|
+
cursor: default;
|
52
|
+
font-size: 0.8rem;
|
53
|
+
padding: 16px 0 12px 0;
|
54
|
+
pointer-events: none;
|
55
|
+
text-transform: uppercase;
|
56
|
+
}
|
57
|
+
|
58
|
+
div[id^=nav-] {
|
59
|
+
h4 {
|
60
|
+
display: none;
|
61
|
+
}
|
62
|
+
|
63
|
+
ul li {
|
64
|
+
position: relative;
|
65
|
+
|
66
|
+
&::before {
|
67
|
+
content: '\25A1';
|
68
|
+
font-size: 0.4rem;
|
69
|
+
left: calc(var(--sidebar-menu-item-left-margin) + 15px);
|
70
|
+
position: absolute;
|
71
|
+
top: 15px;
|
72
|
+
}
|
73
|
+
|
74
|
+
a {
|
75
|
+
font-size: 0.8rem;
|
76
|
+
padding-left: calc(var(--sidebar-menu-item-left-margin) + var(--sidebar-icon-width) + 10px);
|
77
|
+
text-transform: uppercase;
|
78
|
+
}
|
79
|
+
}
|
80
|
+
}
|
81
|
+
|
82
|
+
}
|
83
|
+
}
|
84
|
+
}
|
85
|
+
|
86
|
+
}
|
@@ -0,0 +1,66 @@
|
|
1
|
+
.wrapper.compact {
|
2
|
+
.content {
|
3
|
+
margin-left: var(--sidebar-compact-width);
|
4
|
+
|
5
|
+
@include media-breakpoint-down(md) {
|
6
|
+
margin-left: 0;
|
7
|
+
}
|
8
|
+
}
|
9
|
+
|
10
|
+
.sidebar-menu {
|
11
|
+
width: var(--sidebar-compact-width);
|
12
|
+
|
13
|
+
@include media-breakpoint-down(md) {
|
14
|
+
width: 0;
|
15
|
+
}
|
16
|
+
|
17
|
+
a.logo {
|
18
|
+
display: none !important;
|
19
|
+
}
|
20
|
+
|
21
|
+
a.home-icon {
|
22
|
+
display: block !important;
|
23
|
+
}
|
24
|
+
|
25
|
+
ul li.top-level {
|
26
|
+
> a { // The icons in the compact view.
|
27
|
+
padding: 10px 0;
|
28
|
+
text-align: center;
|
29
|
+
}
|
30
|
+
|
31
|
+
h4 {
|
32
|
+
cursor: default;
|
33
|
+
display: inline-block;
|
34
|
+
font-size: .9rem;
|
35
|
+
padding: 13px 0 0 5px;
|
36
|
+
vertical-align: middle;
|
37
|
+
text-transform: uppercase;
|
38
|
+
}
|
39
|
+
|
40
|
+
ul li {
|
41
|
+
&::before {
|
42
|
+
left: calc(var(--sidebar-icon-width) - 5px);
|
43
|
+
}
|
44
|
+
|
45
|
+
a {
|
46
|
+
padding-left: calc(var(--sidebar-icon-width) + 10px);
|
47
|
+
|
48
|
+
&:hover {
|
49
|
+
background-color: $primary;
|
50
|
+
}
|
51
|
+
}
|
52
|
+
}
|
53
|
+
|
54
|
+
.fa { font-size: 20px; }
|
55
|
+
.text { display: none; }
|
56
|
+
|
57
|
+
.collapse {
|
58
|
+
background-color: $secondary;
|
59
|
+
left: var(--sidebar-compact-width);
|
60
|
+
position: absolute;
|
61
|
+
top: 0;
|
62
|
+
width: 240px;
|
63
|
+
}
|
64
|
+
}
|
65
|
+
}
|
66
|
+
}
|
@@ -0,0 +1,96 @@
|
|
1
|
+
module Plok::Searchable
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
included do
|
5
|
+
has_many :search_indices, as: :searchable, dependent: :destroy
|
6
|
+
|
7
|
+
# The after_save block creates or saves indices for every indicated
|
8
|
+
# searchable field. Takes both translations and flexible content into
|
9
|
+
# account.
|
10
|
+
#
|
11
|
+
# Translation support was relatively painless, but FlexibleContent
|
12
|
+
# required more thought. See #save_flexible_content_indices!
|
13
|
+
after_save do
|
14
|
+
trigger_indices_save!
|
15
|
+
end
|
16
|
+
|
17
|
+
def trigger_indices_save!
|
18
|
+
self.class.searchable_fields_list.each do |key|
|
19
|
+
if key == :flexible_content
|
20
|
+
save_flexible_content_search_indices! && next
|
21
|
+
end
|
22
|
+
|
23
|
+
if respond_to?(:translatable?) && self.class.translatable_fields_list.include?(key)
|
24
|
+
save_translatable_search_index!(key) && next
|
25
|
+
end
|
26
|
+
|
27
|
+
save_search_index!(key)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# I save all ContentText#content values as indices linked to the parent
|
32
|
+
# object. The Plok::Search::Base#indices method already groups by
|
33
|
+
# searchable resource, so there are never any duplicates.
|
34
|
+
# Only additional matches for the searchable because it takes
|
35
|
+
# ContentText#content sources into account.
|
36
|
+
#
|
37
|
+
# ContentColumn and ContentText have after_{save,destroy} callbacks
|
38
|
+
# to help facilitate searchable management. Note that said code was
|
39
|
+
# initially present in this class, and it was such a mess that it became
|
40
|
+
# unpractical to maintain.
|
41
|
+
def save_flexible_content_search_indices!
|
42
|
+
content_rows.each do |row|
|
43
|
+
row.columns.each do |column|
|
44
|
+
next unless column.content.is_a?(ContentText)
|
45
|
+
key = "flexible_content:#{column.content_id}"
|
46
|
+
save_index!(key, value: column.content.content, locale: row.locale)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def save_index!(key, value: nil, locale: nil)
|
52
|
+
value = read_attribute(key) if value.blank? && respond_to?(key)
|
53
|
+
return if value.blank?
|
54
|
+
|
55
|
+
search_indices
|
56
|
+
.find_or_create_by!(name: key, locale: locale)
|
57
|
+
.update_column(value: value)
|
58
|
+
end
|
59
|
+
|
60
|
+
# This exists so we can use #save_search_index! as the main method
|
61
|
+
# to override, and then be able to call #save_index! in the overridden
|
62
|
+
# method to accommodate further defaults.
|
63
|
+
def save_search_index!(key)
|
64
|
+
value = read_attribute(key)
|
65
|
+
save_index!(key, value: value)
|
66
|
+
end
|
67
|
+
|
68
|
+
def save_translatable_search_index!(key)
|
69
|
+
# TODO: locales can't be hardcoded
|
70
|
+
%w(nl fr).each do |locale|
|
71
|
+
value = translation(locale.to_sym).send(key)
|
72
|
+
save_index!(key, value: value, locale: locale)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def searchable?
|
77
|
+
true
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
module ClassMethods
|
82
|
+
def searchable_field(key)
|
83
|
+
unless searchable_fields_list.include?(key.to_sym)
|
84
|
+
searchable_fields_list << key.to_sym
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def searchable_fields(*args)
|
89
|
+
args.each { |key| searchable_field(key) }
|
90
|
+
end
|
91
|
+
|
92
|
+
def searchable_fields_list
|
93
|
+
@searchable_fields_list ||= []
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
data/app/models/queued_task.rb
CHANGED
@@ -55,6 +55,13 @@ class QueuedTask < ActiveRecord::Base
|
|
55
55
|
destroy
|
56
56
|
end
|
57
57
|
|
58
|
+
# You want this separate from #unlock so the ensure block in #process! does
|
59
|
+
# not keep resetting attempts when it shouldn't. This should be called
|
60
|
+
# from the controller where you manually unlock tasks in your backend.
|
61
|
+
def reset_attempts!
|
62
|
+
update_column(:attempts, 0)
|
63
|
+
end
|
64
|
+
|
58
65
|
def increase_attempts!
|
59
66
|
update_column(:attempts, attempts + 1)
|
60
67
|
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
class SearchModule < ActiveRecord::Base
|
2
|
+
scope :weighted, -> { order('weight DESC') }
|
3
|
+
|
4
|
+
validates :name, presence: true
|
5
|
+
|
6
|
+
def indices
|
7
|
+
SearchIndex
|
8
|
+
.joins('INNER JOIN search_modules ON search_indices.searchable_type = search_modules.name')
|
9
|
+
.where('search_modules.name = ?', name)
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
class Module
|
2
|
+
def takes(*arg_names)
|
3
|
+
define_method(:initialize) do |*arg_values|
|
4
|
+
arg_names.zip(arg_values).each do |name, value|
|
5
|
+
if name.is_a?(Hash)
|
6
|
+
name, default_value = name.to_a.flatten
|
7
|
+
value = default_value if value.blank?
|
8
|
+
end
|
9
|
+
|
10
|
+
instance_variable_set(:"@#{name}", value)
|
11
|
+
singleton_class.instance_eval { attr_reader name.to_sym }
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
alias_method :let, :define_method
|
17
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# This migration originally comes from udongo_engine
|
2
|
+
class CreateSearchIndices < ActiveRecord::Migration[6.1]
|
3
|
+
def change
|
4
|
+
return if table_exists?(:search_indices)
|
5
|
+
|
6
|
+
create_table :search_indices do |t|
|
7
|
+
t.string :searchable_type
|
8
|
+
t.integer :searchable_id
|
9
|
+
t.string :locale, index: true
|
10
|
+
t.string :name
|
11
|
+
t.text :value
|
12
|
+
|
13
|
+
t.timestamps
|
14
|
+
end
|
15
|
+
|
16
|
+
add_index :search_indices, [:searchable_type, :searchable_id]
|
17
|
+
add_index :search_indices, [:locale, :name]
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# This migration originally comes from udongo_engine
|
2
|
+
class CreateSearchModules < ActiveRecord::Migration[6.1]
|
3
|
+
def change
|
4
|
+
return if table_exists?(:search_modules)
|
5
|
+
|
6
|
+
create_table :search_modules do |t|
|
7
|
+
t.string :name
|
8
|
+
t.boolean :searchable
|
9
|
+
t.integer :weight
|
10
|
+
|
11
|
+
t.timestamps
|
12
|
+
end
|
13
|
+
|
14
|
+
add_index :search_modules, [:name, :searchable]
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
require 'rails/generators/base'
|
2
|
+
|
3
|
+
class Plok::SidebarGenerator < Rails::Generators::Base
|
4
|
+
source_root File.expand_path('templates', __dir__)
|
5
|
+
class_option :css_framework, type: :string, default: 'bs5'
|
6
|
+
|
7
|
+
def install
|
8
|
+
copy_sidebar_files('wrapper', 'menu_items', 'menu_item', 'offcanvas_menu')
|
9
|
+
add_scss_imports_to_application
|
10
|
+
add_js_imports_to_application
|
11
|
+
inject_wrapper_block_into_application_layout
|
12
|
+
say("\nAll done! Remember to reboot your server so the new assets can load.\n\n")
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def add_js_imports_to_application
|
18
|
+
if application_file(:js)
|
19
|
+
unless file_contains?(application_file(:js), /\/\/= require plok\/sidebar/)
|
20
|
+
append_to_file application_file(:js), "//= require plok/sidebar"
|
21
|
+
end
|
22
|
+
else
|
23
|
+
say("\nWARNING: No suitable application.js file found.\n")
|
24
|
+
say("Please add the following import to your backend application.js file:\n\n")
|
25
|
+
say("//= require plok/sidebar\n\n")
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def add_scss_imports_to_application
|
30
|
+
if application_file(:scss)
|
31
|
+
unless file_contains?(application_file(:scss), /@import 'plok\/sidebar'/)
|
32
|
+
append_to_file application_file(:scss), "@import 'plok/sidebar';\n"
|
33
|
+
end
|
34
|
+
|
35
|
+
unless file_contains?(application_file(:scss), /@import 'plok\/sidebar_compact'/)
|
36
|
+
append_to_file application_file(:scss), "@import 'plok/sidebar_compact';\n"
|
37
|
+
end
|
38
|
+
else
|
39
|
+
say("\nWARNING: No suitable application.scss file found.\n")
|
40
|
+
say("Please add the following imports to your backend application.scss file:\n\n")
|
41
|
+
say("@import 'plok/sidebar';\n")
|
42
|
+
say("@import 'plok/sidebar_compact';\n")
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def inject_wrapper_block_into_application_layout
|
47
|
+
# The sidebar wrapper already exists, so stop here.
|
48
|
+
return if file_contains?(application_layout_file, /sidebar\/wrapper/)
|
49
|
+
|
50
|
+
# The wrapper is missing, but we *need* a suitable spot for a closing tag.
|
51
|
+
unless file_contains?(application_layout_file, /yield\(:javascripts_early\)/)
|
52
|
+
say("\nWARNING: The generator could not inject the sidebar wrapper.\n")
|
53
|
+
say("You will have to wrap your backend application markup in this block:\n\n")
|
54
|
+
say("# #{application_layout_file}\n")
|
55
|
+
say("<%= render 'backend/#{options.css_framework}/sidebar/wrapper', brand_name: '#{app_name}' do %>\n")
|
56
|
+
say(" # ...your backend application markup here...\n")
|
57
|
+
say("<% end %>\n")
|
58
|
+
return
|
59
|
+
end
|
60
|
+
|
61
|
+
gsub_file(
|
62
|
+
application_layout_file,
|
63
|
+
/<body(.*)>\n/,
|
64
|
+
"<body\\1>\n <%= render 'backend/#{options.css_framework}/sidebar/wrapper', brand_name: '#{app_name}' do %>\n"
|
65
|
+
)
|
66
|
+
|
67
|
+
gsub_file(
|
68
|
+
application_layout_file,
|
69
|
+
/\n(.*)<%= yield\(:javascripts_early\) %>/,
|
70
|
+
" <% end %>\n\n\\1<%= yield(:javascripts_early) %>"
|
71
|
+
)
|
72
|
+
end
|
73
|
+
|
74
|
+
def app_name
|
75
|
+
Rails.application.class.name.split('::').first
|
76
|
+
end
|
77
|
+
|
78
|
+
def copy_sidebar_files(*partials)
|
79
|
+
partials.each do |partial_name|
|
80
|
+
copy_file "_#{partial_name}.html.erb", sidebar_partial_path(partial_name)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def sidebar_partial_path(partial_name)
|
85
|
+
"app/views/backend/#{options.css_framework}/sidebar/_#{partial_name}.html.erb"
|
86
|
+
end
|
87
|
+
|
88
|
+
def file_contains?(file, content)
|
89
|
+
File.readlines(file).grep(content).any?
|
90
|
+
end
|
91
|
+
|
92
|
+
def application_layout_file
|
93
|
+
if File.exists?("app/views/layouts/backend/#{options.css_framework}/application.html.erb")
|
94
|
+
return "app/views/layouts/backend/#{options.css_framework}/application.html.erb"
|
95
|
+
end
|
96
|
+
|
97
|
+
if File.exists?('app/views/layouts/backend/application.html.erb')
|
98
|
+
return 'app/views/layouts/backend/application.html.erb'
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def application_file(type)
|
103
|
+
namespace = 'stylesheets'
|
104
|
+
namespace = 'javascripts' if type.to_s == 'js'
|
105
|
+
|
106
|
+
if File.exists?("app/assets/#{namespace}/backend/#{options.css_framework}/application.#{type}")
|
107
|
+
return "app/assets/#{namespace}/backend/#{options.css_framework}/application.#{type}"
|
108
|
+
end
|
109
|
+
|
110
|
+
if File.exists?("app/assets/#{namespace}/backend/application.#{type}")
|
111
|
+
return "app/assets/#{namespace}/backend/application.#{type}"
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
<% lbl ||= t("b.#{name}") %>
|
2
|
+
<% href ||= defined?(path) ? path : "#nav-#{name}" %>
|
3
|
+
|
4
|
+
<%= content_tag :li, class: 'top-level' do %>
|
5
|
+
<%= link_to href, class: 'top-level-anchor link-light', data: { bs_toggle: (defined?(href) ? 'tooltip' : 'collapse') }, title: lbl do %>
|
6
|
+
<%= fa_icon icon, class: 'fa-fw' %>
|
7
|
+
<%= content_tag :span, lbl, class: 'text' %>
|
8
|
+
<% end %>
|
9
|
+
|
10
|
+
<% if defined?(path) %>
|
11
|
+
<%= content_tag :div, id: path, class: 'collapse' do %>
|
12
|
+
<h4><%= lbl %></h4>
|
13
|
+
<%= yield %>
|
14
|
+
<% end %>
|
15
|
+
<% end %>
|
16
|
+
<% end %>
|
@@ -0,0 +1,11 @@
|
|
1
|
+
<div class="offcanvas offcanvas-start bg-dark text-light" tabindex="-1" id="navigation-offcanvas">
|
2
|
+
<div class="offcanvas-header">
|
3
|
+
<h5 class="offcanvas-title"><%= brand_name %></h5>
|
4
|
+
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="offcanvas" aria-label="Close"></button>
|
5
|
+
</div>
|
6
|
+
<div class="offcanvas-body">
|
7
|
+
<div class="sidebar-menu position-static">
|
8
|
+
<%= render 'backend/bs5/sidebar/menu_items' %>
|
9
|
+
</div>
|
10
|
+
</div>
|
11
|
+
</div>
|
@@ -0,0 +1,19 @@
|
|
1
|
+
<div class="wrapper">
|
2
|
+
<div class="sidebar-menu text-light bg-dark overflow-auto">
|
3
|
+
<%= link_to backend_path, class: 'text-center navbar-brand logo text-light me-0' do %>
|
4
|
+
<%= brand_name %>
|
5
|
+
<% end %>
|
6
|
+
|
7
|
+
<%= link_to fa_icon(:'home'), backend_path, class: 'text-center navbar-brand home-icon text-light fs-2 me-0' %>
|
8
|
+
|
9
|
+
<nav class="mt-3">
|
10
|
+
<%= render 'backend/bs5/sidebar/menu_items' %>
|
11
|
+
</nav>
|
12
|
+
</div>
|
13
|
+
|
14
|
+
<div class="content">
|
15
|
+
<%= yield %>
|
16
|
+
</div>
|
17
|
+
</div>
|
18
|
+
|
19
|
+
<%= render 'backend/bs5/sidebar/offcanvas_menu', brand_name: brand_name %>
|
data/lib/plok/engine.rb
CHANGED
@@ -19,14 +19,17 @@ module Plok
|
|
19
19
|
g.test_framework :rspec
|
20
20
|
end
|
21
21
|
|
22
|
-
|
22
|
+
# Autoload classes from the lib dir
|
23
|
+
config.autoload_paths << File.expand_path('../..', __FILE__)
|
24
|
+
|
25
|
+
def self.class_exists?(class_name)
|
23
26
|
klass = Module.const_get(class_name.to_s)
|
24
27
|
klass.is_a?(Class)
|
25
28
|
rescue NameError
|
26
29
|
return false
|
27
30
|
end
|
28
31
|
|
29
|
-
def module_exists?(module_name)
|
32
|
+
def self.module_exists?(module_name)
|
30
33
|
# By casting to a string and making a constant, we can assume module_name
|
31
34
|
# can be either one without it being a problem.
|
32
35
|
module_name.to_s.constantize.is_a?(Module)
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# Due to the load order of classes, Backend precedes the required Base class.
|
2
|
+
require_relative 'base'
|
3
|
+
|
4
|
+
module Plok::Search
|
5
|
+
# The goal of this class is to provide a manipulated version of the filtered
|
6
|
+
# index data that we can use in the result set of an autocomplete-triggered
|
7
|
+
# search query. See Plok::Search::Base for more information on how this
|
8
|
+
# search functionality is designed.
|
9
|
+
class Backend < Plok::Search::Base
|
10
|
+
# This translates the filtered indices into meaningful result objects.
|
11
|
+
# These require a { label: ... value: ... } to accommodate jquery-ui.
|
12
|
+
#
|
13
|
+
# Note that the result_object#url method is defined in
|
14
|
+
# Plok::Search::ResultObjects::Backend::Page.
|
15
|
+
def search
|
16
|
+
indices.map do |index|
|
17
|
+
result = result_object(index)
|
18
|
+
{ label: result.build_html, value: result.url }
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module Plok::Search
|
2
|
+
# The goal of the Base class is to filter our indices on the given search
|
3
|
+
# term. Further manipulation of individual index data into a meaningful
|
4
|
+
# result set (think autocomplete results) is done by extending this class.
|
5
|
+
#
|
6
|
+
# Examples of class extensions could be:
|
7
|
+
# Plok::Search::Backend - included in Plok
|
8
|
+
# Plok::Search::Frontend
|
9
|
+
# Plok::Search::Api
|
10
|
+
#
|
11
|
+
# The primary benefit in having these namespaced search interfaces is to
|
12
|
+
# provide a way for the developer to have different result objects for
|
13
|
+
# each resource.
|
14
|
+
#
|
15
|
+
# Example #1: A search request for a specific Page instance in the frontend
|
16
|
+
# will typically return a link to said page. However, in search requests made
|
17
|
+
# in the backend for the same Page instance, you'd expect a link to a form in
|
18
|
+
# the backend Page module where you can edit the page's contents.
|
19
|
+
#
|
20
|
+
# Example #2: Some autocompletes in a frontend namespace might require an
|
21
|
+
# image or a price to be included in its body.
|
22
|
+
#
|
23
|
+
# However these result objects are structured are also up to the developer.
|
24
|
+
class Base
|
25
|
+
attr_reader :term, :controller
|
26
|
+
|
27
|
+
def initialize(term, controller: nil, namespace: nil)
|
28
|
+
@term = Plok::Search::Term.new(term, controller: controller)
|
29
|
+
@controller = controller
|
30
|
+
@namespace = namespace
|
31
|
+
end
|
32
|
+
|
33
|
+
def indices
|
34
|
+
# Having the searchmodules sorted by weight returns indices in the
|
35
|
+
# correct order.
|
36
|
+
@indices ||= SearchModule.weighted.inject([]) do |stack, m|
|
37
|
+
# The group happens to make sure we end up with just 1 copy of
|
38
|
+
# a searchable result. Otherwise matches from both an indexed
|
39
|
+
# Page#title and Page#description would be in the result set.
|
40
|
+
stack << m.indices
|
41
|
+
.where(
|
42
|
+
'(searchable_id = ? OR search_indices.value LIKE ?)',
|
43
|
+
term.value,
|
44
|
+
"%#{term.value}%"
|
45
|
+
)
|
46
|
+
.group([:searchable_type, :searchable_id])
|
47
|
+
end.flatten
|
48
|
+
end
|
49
|
+
|
50
|
+
def namespace
|
51
|
+
# This looks daft, but it gives us a foot in the door for when a frontend
|
52
|
+
# search is triggered in the backend.
|
53
|
+
return @namespace unless @namespace.nil?
|
54
|
+
return 'Frontend' if controller.nil?
|
55
|
+
controller.class.module_parent.to_s
|
56
|
+
end
|
57
|
+
|
58
|
+
# In order to provide a good result set in a search autocomplete, we have
|
59
|
+
# to translate the raw index to a class that makes an index adhere
|
60
|
+
# to a certain interface (that can include links).
|
61
|
+
def result_object(index)
|
62
|
+
klass = "Plok::Search::ResultObjects::#{namespace}::#{index.searchable_type}"
|
63
|
+
klass = 'Plok::Search::ResultObjects::Base' unless result_object_exists?(klass)
|
64
|
+
klass.constantize.new(index, search_context: self)
|
65
|
+
end
|
66
|
+
|
67
|
+
def result_object_exists?(name)
|
68
|
+
Plok::Engine.class_exists?(name) && name.constantize.method_defined?(:build_html)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module Plok
|
2
|
+
class InterfaceNotImplementedError < NoMethodError
|
3
|
+
end
|
4
|
+
|
5
|
+
class Search::ResultObjects::Base
|
6
|
+
attr_reader :index, :search_context
|
7
|
+
|
8
|
+
delegate :searchable, to: :index
|
9
|
+
|
10
|
+
def initialize(index, search_context: nil)
|
11
|
+
@index = index
|
12
|
+
@search_context = search_context
|
13
|
+
end
|
14
|
+
|
15
|
+
# Typically, an autocomplete requires 3 things:
|
16
|
+
#
|
17
|
+
# * A title indicating a resource name.
|
18
|
+
# Examples: Page#title, Product#name,...
|
19
|
+
# * A truncated summary providing a glimpse of the resource's contents.
|
20
|
+
# Examples: Page#subtitle, Product#description,...
|
21
|
+
# * A link to the resource.
|
22
|
+
# Examples: edit_backend_page_path(37), product_path(37),...
|
23
|
+
#
|
24
|
+
# However, this seems very restrictive to me. If I narrow down the data
|
25
|
+
# a dev can use in an autocomplete, it severely reduces options he/she has
|
26
|
+
# in how the autocomplete results look like. Think of autocompletes in a
|
27
|
+
# shop that require images or prices to be included in their result bodies.
|
28
|
+
#
|
29
|
+
# This is why I chose to let ApplicationController.render work around the
|
30
|
+
# problem by letting the dev decide how the row should look.
|
31
|
+
#
|
32
|
+
def build_html
|
33
|
+
ApplicationController.render(partial: partial, locals: locals)
|
34
|
+
end
|
35
|
+
|
36
|
+
def hidden?
|
37
|
+
searchable.respond_to?(:visible?) && searchable.hidden?
|
38
|
+
end
|
39
|
+
|
40
|
+
def label
|
41
|
+
# We want to grab the name of the index from ContentText whenever
|
42
|
+
# we're dealing with FlexibleContent stuff.
|
43
|
+
if index.name.include?('flexible_content')
|
44
|
+
id = index.name.split(':')[1].to_i
|
45
|
+
return ContentText.find(id).content
|
46
|
+
end
|
47
|
+
|
48
|
+
searchable.send(index.name)
|
49
|
+
end
|
50
|
+
|
51
|
+
def locals
|
52
|
+
{ "#{partial_target}": index.searchable, index: index }
|
53
|
+
end
|
54
|
+
|
55
|
+
def partial
|
56
|
+
"#{partial_path}/#{partial_target}"
|
57
|
+
end
|
58
|
+
|
59
|
+
def partial_path
|
60
|
+
"#{search_context.namespace.to_s.underscore}/search"
|
61
|
+
end
|
62
|
+
|
63
|
+
def partial_target
|
64
|
+
index.searchable_type.underscore
|
65
|
+
end
|
66
|
+
|
67
|
+
def unpublished?
|
68
|
+
searchable.respond_to?(:published?) && searchable.unpublished?
|
69
|
+
end
|
70
|
+
|
71
|
+
def url
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Plok::Search
|
2
|
+
class Term
|
3
|
+
attr_reader :controller, :string
|
4
|
+
|
5
|
+
def initialize(string, controller: nil)
|
6
|
+
@string = string
|
7
|
+
@controller = controller
|
8
|
+
end
|
9
|
+
|
10
|
+
def locale
|
11
|
+
return controller.locale if controller.present?
|
12
|
+
:nl
|
13
|
+
end
|
14
|
+
|
15
|
+
def valid?
|
16
|
+
@string.present?
|
17
|
+
end
|
18
|
+
|
19
|
+
def value
|
20
|
+
string
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/lib/plok/version.rb
CHANGED
data/lib/tasks/plok_tasks.rake
CHANGED
@@ -1,4 +1,11 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
1
|
+
namespace 'plok:search' do
|
2
|
+
desc 'Rebuild all search indices'
|
3
|
+
task rebuild_indices: :environment do
|
4
|
+
SearchIndex.destroy_all
|
5
|
+
SearchModule.where(searchable: true).each do |m|
|
6
|
+
puts "Rebuilding #{m.name} indices..."
|
7
|
+
m.name.constantize.all.each(&:trigger_indices_save!)
|
8
|
+
end
|
9
|
+
puts "Done."
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# This exists so one is able to mock actual ActiveRecord model classes that are
|
2
|
+
# not tied to a database. This is useful if you want to write tests for concerns
|
3
|
+
# without tying them to project-specific models.
|
4
|
+
#
|
5
|
+
# * It allows you to not mock simple ActiveRecord DSL like find/find_by/where
|
6
|
+
# calls.
|
7
|
+
# * Projects using Plok can also use this, provided they call
|
8
|
+
# Plok::Engine.load_spec_supports in their spec/rails_helper.rb file.
|
9
|
+
# * Even if you're required to mock additional business logic onto your classes,
|
10
|
+
# you can just extend from Plok::FakeArModel to fill in gaps.
|
11
|
+
#
|
12
|
+
# Example:
|
13
|
+
#
|
14
|
+
# class Product < Plok::FakeArModel
|
15
|
+
# def visible?
|
16
|
+
# true
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# def hidden?
|
20
|
+
# !visible?
|
21
|
+
# end
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
class Plok::FakeArModel
|
25
|
+
extend ActiveModel::Naming
|
26
|
+
extend ActiveModel::Translation
|
27
|
+
include ActiveModel::Validations
|
28
|
+
include ActiveModel::Conversion
|
29
|
+
include ActiveModel::Dirty
|
30
|
+
|
31
|
+
def initialize(attributes = {})
|
32
|
+
attributes.each do |key, value|
|
33
|
+
instance_variable_set(:"@#{key}", value)
|
34
|
+
self.class.send(:attr_accessor, key)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.collection(list)
|
39
|
+
@collection = list.to_a.map { |values| new(values) }
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.primary_key
|
43
|
+
:id
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.polymorphic_name
|
47
|
+
:fake_ar_model
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.current_scope
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.find(id)
|
54
|
+
@collection.to_a.detect { |o| o.id == id }
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.find_by(arguments)
|
58
|
+
where(arguments)&.first
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.where(arguments = nil)
|
62
|
+
@collection.select do |o|
|
63
|
+
if arguments.is_a?(Hash)
|
64
|
+
!!arguments.detect { |k, v| o.send(k) == v }
|
65
|
+
elsif(arguments.is_a?(String))
|
66
|
+
# NOTE: This will require intensive tinkering if we ever need it,
|
67
|
+
# so lets leave it for now.
|
68
|
+
[]
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def destroyed?
|
74
|
+
false
|
75
|
+
end
|
76
|
+
|
77
|
+
def new_record?
|
78
|
+
true
|
79
|
+
end
|
80
|
+
|
81
|
+
def save(options = {})
|
82
|
+
true
|
83
|
+
end
|
84
|
+
|
85
|
+
def _read_attribute(a)
|
86
|
+
a
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
def collection
|
92
|
+
self.class.instance_variable_get(:@collection)&.to_a || []
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
require 'rails_helper'
|
2
|
+
|
3
|
+
shared_examples_for :searchable do
|
4
|
+
let(:klass) { described_class.to_s.underscore.to_sym }
|
5
|
+
|
6
|
+
describe 'after_save' do
|
7
|
+
context 'non-translatable model' do
|
8
|
+
before do
|
9
|
+
# Because this is a spec/support included through it_behaves_like,
|
10
|
+
# we have to mock the entry points of the data used in
|
11
|
+
# Plok::Searchable.
|
12
|
+
allow(described_class).to receive(:searchable_fields_list) { [:foo] }
|
13
|
+
if described_class.respond_to?(:translatable_fields_list)
|
14
|
+
allow(described_class).to receive(:translatable_fields_list) { [] }
|
15
|
+
end
|
16
|
+
described_class.define_method(:foo) { 'bar' }
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'default' do
|
20
|
+
subject = build(klass)
|
21
|
+
allow(subject).to receive(:foo) { 'bar' }
|
22
|
+
expect(subject.search_indices).to eq []
|
23
|
+
end
|
24
|
+
|
25
|
+
# NOTE: See if you can dynamically test the creation of indices on save.
|
26
|
+
end
|
27
|
+
|
28
|
+
if described_class.respond_to?(:translatable_fields)
|
29
|
+
before do
|
30
|
+
allow(described_class).to receive(:searchable_fields_list) { [:foo] }
|
31
|
+
end
|
32
|
+
|
33
|
+
context 'translatable model' do
|
34
|
+
before do
|
35
|
+
described_class.translatable_fields :foo, :bar
|
36
|
+
end
|
37
|
+
|
38
|
+
let(:create_subject!) do
|
39
|
+
subject = build(klass)
|
40
|
+
%w(nl fr).each do |l|
|
41
|
+
t = subject.translation(l.to_sym)
|
42
|
+
t.foo = "baz #{l}"
|
43
|
+
t.bar = 'bak'
|
44
|
+
end
|
45
|
+
subject.save
|
46
|
+
subject
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'does not copy translatable fields that are not in searchable fields' do
|
50
|
+
subject = create_subject!
|
51
|
+
expect(subject.search_indices.find_by(locale: :nl, name: 'bar')).to be nil
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
if described_class.respond_to?(:content_rows)
|
57
|
+
context 'model with flexible_content' do
|
58
|
+
let(:subject) { create(klass) }
|
59
|
+
let(:content) { create(:content_text, content: 'Lorem ipsum') }
|
60
|
+
|
61
|
+
before do
|
62
|
+
# Because this is a spec/support included through it_behaves_like,
|
63
|
+
# we have to mock the entry points of the data used in
|
64
|
+
# Plok::Searchable.
|
65
|
+
allow(described_class).to receive(:searchable_fields_list) { [:foo] }
|
66
|
+
allow_any_subject_of(described_class).to receive(:foo) { 'bar' }
|
67
|
+
allow_any_subject_of(Concerns::Storable::Collection).to receive(:foo) { 'bar' }
|
68
|
+
|
69
|
+
row = create(:content_row, locale: 'nl', rowable: subject)
|
70
|
+
row.columns << create(:content_column, content: content)
|
71
|
+
subject.content_rows << row
|
72
|
+
end
|
73
|
+
|
74
|
+
it 'saves index when updating searchable subject' do
|
75
|
+
subject.save!
|
76
|
+
key = "flexible_content:#{content.id}"
|
77
|
+
expect(subject.search_indices.find_by(locale: :nl, name: key).value).to eq 'Lorem ipsum'
|
78
|
+
end
|
79
|
+
|
80
|
+
it 'saves index when updating flexible content' do
|
81
|
+
subject.save!
|
82
|
+
content.content = 'Dolor sit amet'
|
83
|
+
content.save!
|
84
|
+
key = "flexible_content:#{content.id}"
|
85
|
+
expect(subject.search_indices.find_by(locale: :nl, name: key).value).to eq 'Dolor sit amet'
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
it '.respond_to?' do
|
92
|
+
expect(described_class).to respond_to(
|
93
|
+
:searchable_field, :searchable_fields, :searchable_fields_list
|
94
|
+
)
|
95
|
+
end
|
96
|
+
|
97
|
+
it '#respond_to?' do
|
98
|
+
expect(subject).to respond_to(:search_indices)
|
99
|
+
end
|
100
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: plok
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Davy Hellemans
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2022-
|
12
|
+
date: 2022-10-14 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rails
|
@@ -78,12 +78,21 @@ files:
|
|
78
78
|
- MIT-LICENSE
|
79
79
|
- Rakefile
|
80
80
|
- app/assets/config/plok_manifest.js
|
81
|
+
- app/assets/javascripts/jquery-ui.autocomplete.html.js
|
82
|
+
- app/assets/javascripts/plok/searchable.js
|
83
|
+
- app/assets/javascripts/plok/sidebar.js
|
84
|
+
- app/assets/stylesheets/plok/_autocomplete.scss
|
85
|
+
- app/assets/stylesheets/plok/_sidebar.scss
|
86
|
+
- app/assets/stylesheets/plok/_sidebar_compact.scss
|
81
87
|
- app/controllers/catch_all_controller.rb
|
82
88
|
- app/controllers/plok/version_controller.rb
|
83
89
|
- app/models/concerns/plok/loggable.rb
|
90
|
+
- app/models/concerns/plok/searchable.rb
|
84
91
|
- app/models/log.rb
|
85
92
|
- app/models/queued_task.rb
|
86
|
-
-
|
93
|
+
- app/models/search_index.rb
|
94
|
+
- app/models/search_module.rb
|
95
|
+
- config/initializers/module.rb
|
87
96
|
- config/routes.rb
|
88
97
|
- db/migrate/20211008130809_create_plok_logs.rb
|
89
98
|
- db/migrate/20211015141837_create_plok_queued_tasks.rb
|
@@ -93,13 +102,29 @@ files:
|
|
93
102
|
- db/migrate/20211203103118_add_file_to_logs.rb
|
94
103
|
- db/migrate/20220512141814_add_log_indexes_to_category_type_and_id.rb
|
95
104
|
- db/migrate/20220512142356_add_index_to_queued_task_weight.rb
|
105
|
+
- db/migrate/20220923162300_create_search_indices.rb
|
106
|
+
- db/migrate/20220923164100_create_search_modules.rb
|
107
|
+
- lib/generators/plok/sidebar/USAGE
|
108
|
+
- lib/generators/plok/sidebar/sidebar_generator.rb
|
109
|
+
- lib/generators/plok/sidebar/templates/_menu_item.html.erb
|
110
|
+
- lib/generators/plok/sidebar/templates/_menu_items.html.erb
|
111
|
+
- lib/generators/plok/sidebar/templates/_offcanvas_menu.html.erb
|
112
|
+
- lib/generators/plok/sidebar/templates/_wrapper.html.erb
|
96
113
|
- lib/plok.rb
|
97
114
|
- lib/plok/engine.rb
|
115
|
+
- lib/plok/search/backend.rb
|
116
|
+
- lib/plok/search/base.rb
|
117
|
+
- lib/plok/search/result_objects/base.rb
|
118
|
+
- lib/plok/search/term.rb
|
98
119
|
- lib/plok/version.rb
|
99
120
|
- lib/tasks/plok_tasks.rake
|
100
121
|
- spec/factories/logs.rb
|
101
122
|
- spec/factories/queued_tasks.rb
|
123
|
+
- spec/factories/search_indices.rb
|
124
|
+
- spec/factories/search_modules.rb
|
125
|
+
- spec/support/fake_ar_model.rb
|
102
126
|
- spec/support/plok/loggable.rb
|
127
|
+
- spec/support/plok/searchable.rb
|
103
128
|
homepage: https://plok.blimp.be
|
104
129
|
licenses:
|
105
130
|
- MIT
|
@@ -120,7 +145,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
120
145
|
- !ruby/object:Gem::Version
|
121
146
|
version: '0'
|
122
147
|
requirements: []
|
123
|
-
rubygems_version: 3.
|
148
|
+
rubygems_version: 3.3.9
|
124
149
|
signing_key:
|
125
150
|
specification_version: 4
|
126
151
|
summary: CMS basics
|