headmin 0.6.1 → 0.6.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/app/assets/javascripts/headmin/controllers/filter_controller.js +15 -3
- data/app/assets/javascripts/headmin/controllers/filter_row_controller.js +75 -47
- data/app/assets/javascripts/headmin/controllers/infinite_scroller_controller.js +30 -0
- data/app/assets/javascripts/headmin/controllers/media_controller.js +24 -8
- data/app/assets/javascripts/headmin/controllers/media_modal_controller.js +142 -105
- data/app/assets/javascripts/headmin/index.js +2 -0
- data/app/assets/javascripts/headmin.js +122 -19
- data/app/assets/stylesheets/headmin.css +1 -1
- data/app/controllers/headmin/media_controller.rb +11 -0
- data/app/helpers/headmin/form_helper.rb +0 -11
- data/app/models/headmin/filter/association.rb +0 -12
- data/app/models/headmin/filter/association_count.rb +50 -0
- data/app/models/headmin/filter/association_count_view.rb +78 -0
- data/app/models/headmin/filter/base.rb +10 -0
- data/app/models/headmin/filter/date.rb +8 -1
- data/app/models/headmin/filter/date_view.rb +1 -1
- data/app/models/headmin/filter/number.rb +0 -12
- data/app/models/headmin/filter/operator_view.rb +3 -1
- data/app/views/headmin/_filters.html.erb +1 -1
- data/app/views/headmin/filters/_association_count.html.erb +28 -0
- data/app/views/headmin/filters/_date.html.erb +1 -0
- data/app/views/headmin/media/_modal.html.erb +8 -3
- data/app/views/headmin/media/_thumbnail.html.erb +20 -0
- data/app/views/headmin/media/create.turbo_stream.erb +1 -1
- data/app/views/headmin/media/index.turbo_stream.erb +11 -0
- data/app/views/headmin/media/thumbnail.html.erb +3 -0
- data/app/views/headmin/pagination/_infinite.html.erb +7 -0
- data/config/locales/headmin/filters/en.yml +2 -0
- data/config/locales/headmin/filters/nl.yml +2 -0
- data/config/locales/headmin/pagination/en.yml +2 -0
- data/config/locales/headmin/pagination/nl.yml +2 -0
- data/config/routes.rb +2 -1
- data/lib/headmin/version.rb +1 -1
- data/package.json +1 -1
- metadata +10 -3
- data/app/views/headmin/media/_item.html.erb +0 -16
@@ -7718,7 +7718,6 @@ var filter_controller_default = class extends Controller {
|
|
7718
7718
|
}
|
7719
7719
|
connect() {
|
7720
7720
|
this.element.controller = this;
|
7721
|
-
this.updateHiddenValue();
|
7722
7721
|
}
|
7723
7722
|
toggle(event) {
|
7724
7723
|
const expanded = this.buttonTarget.getAttribute("aria-expanded") === "true";
|
@@ -7776,7 +7775,14 @@ var filter_controller_default = class extends Controller {
|
|
7776
7775
|
for (const row of this.rowTargets) {
|
7777
7776
|
const conditional = row.previousElementSibling ? row.previousElementSibling.querySelector('[data-filter-target="conditional"]').value : null;
|
7778
7777
|
const operator = row.querySelector('[data-filter-target="operator"]').value;
|
7779
|
-
|
7778
|
+
let values = Array.from(row.querySelectorAll('[data-filter-target="value"]'));
|
7779
|
+
values = values.filter((element) => {
|
7780
|
+
return element.style.display;
|
7781
|
+
});
|
7782
|
+
values = values.map((element) => {
|
7783
|
+
return element.value;
|
7784
|
+
});
|
7785
|
+
const value = values.join(",");
|
7780
7786
|
string += `${conditional || ""}${operator}:${value}`;
|
7781
7787
|
}
|
7782
7788
|
return string;
|
@@ -7799,16 +7805,25 @@ var filter_row_controller_default = class extends Controller {
|
|
7799
7805
|
handleOperatorChange() {
|
7800
7806
|
if (this.operatorTarget.value === "is_null" || this.operatorTarget.value === "is_not_null") {
|
7801
7807
|
this.toggleNullInput();
|
7808
|
+
} else if (this.operatorTarget.value === "between" || this.operatorTarget.value === "not_between") {
|
7809
|
+
this.toggleSecondaryInput();
|
7802
7810
|
} else {
|
7803
7811
|
this.toggleOriginalInput();
|
7804
7812
|
}
|
7805
7813
|
}
|
7806
7814
|
toggleNullInput() {
|
7807
7815
|
this.hideOriginal();
|
7816
|
+
this.hideSecondary();
|
7808
7817
|
this.showNull();
|
7809
7818
|
}
|
7810
7819
|
toggleOriginalInput() {
|
7811
7820
|
this.showOriginal();
|
7821
|
+
this.hideSecondary();
|
7822
|
+
this.hideNull();
|
7823
|
+
}
|
7824
|
+
toggleSecondaryInput() {
|
7825
|
+
this.showSecondary();
|
7826
|
+
this.hideOriginal();
|
7812
7827
|
this.hideNull();
|
7813
7828
|
}
|
7814
7829
|
hideOriginal() {
|
@@ -7819,6 +7834,22 @@ var filter_row_controller_default = class extends Controller {
|
|
7819
7834
|
this.originalTarget.style.display = "block";
|
7820
7835
|
this.originalTarget.setAttribute("data-filter-target", "value");
|
7821
7836
|
}
|
7837
|
+
hideSecondary() {
|
7838
|
+
for (const [index2, value] of this.originalTargets.entries()) {
|
7839
|
+
if (index2 != 0) {
|
7840
|
+
value.style.display = "none";
|
7841
|
+
value.setAttribute("data-filter-target", "value_original");
|
7842
|
+
}
|
7843
|
+
}
|
7844
|
+
}
|
7845
|
+
showSecondary() {
|
7846
|
+
for (const [index2, value] of this.originalTargets.entries()) {
|
7847
|
+
if (index2 != 0) {
|
7848
|
+
value.style.display = "block";
|
7849
|
+
value.setAttribute("data-filter-target", "value");
|
7850
|
+
}
|
7851
|
+
}
|
7852
|
+
}
|
7822
7853
|
hideNull() {
|
7823
7854
|
this.nullTarget.style.display = "none";
|
7824
7855
|
this.nullTarget.setAttribute("data-filter-target", "value_null");
|
@@ -10093,6 +10124,29 @@ var hello_controller_default = class extends Controller {
|
|
10093
10124
|
}
|
10094
10125
|
};
|
10095
10126
|
|
10127
|
+
// app/assets/javascripts/headmin/controllers/infinite_scroller_controller.js
|
10128
|
+
var infinite_scroller_controller_default = class extends Controller {
|
10129
|
+
connect() {
|
10130
|
+
this.clickWhenInViewport();
|
10131
|
+
document.querySelector(".modal-body").addEventListener("scroll", () => {
|
10132
|
+
this.clickWhenInViewport();
|
10133
|
+
});
|
10134
|
+
}
|
10135
|
+
clickWhenInViewport() {
|
10136
|
+
if (!this.isLoading() && this.isInViewport()) {
|
10137
|
+
this.element.setAttribute("clicked", 1);
|
10138
|
+
this.element.click();
|
10139
|
+
}
|
10140
|
+
}
|
10141
|
+
isLoading() {
|
10142
|
+
return this.element.hasAttribute("clicked");
|
10143
|
+
}
|
10144
|
+
isInViewport() {
|
10145
|
+
const rect = this.element.getBoundingClientRect();
|
10146
|
+
return rect.top >= 0 && rect.left >= 0 && rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && rect.right <= (window.innerWidth || document.documentElement.clientWidth);
|
10147
|
+
}
|
10148
|
+
};
|
10149
|
+
|
10096
10150
|
// app/assets/javascripts/headmin/controllers/media_controller.js
|
10097
10151
|
var media_controller_default = class extends Controller {
|
10098
10152
|
static get targets() {
|
@@ -10140,8 +10194,8 @@ var media_controller_default = class extends Controller {
|
|
10140
10194
|
this.postProcess();
|
10141
10195
|
}
|
10142
10196
|
selectItems(items) {
|
10143
|
-
this.
|
10144
|
-
this.
|
10197
|
+
this.removeAllDeselectedItems(items);
|
10198
|
+
this.addNewItems(items);
|
10145
10199
|
this.postProcess();
|
10146
10200
|
}
|
10147
10201
|
postProcess() {
|
@@ -10184,8 +10238,16 @@ var media_controller_default = class extends Controller {
|
|
10184
10238
|
}
|
10185
10239
|
});
|
10186
10240
|
}
|
10187
|
-
|
10188
|
-
|
10241
|
+
addNewItems(items) {
|
10242
|
+
const itemTargetIds = this.itemTargets.map((i) => {
|
10243
|
+
return parseInt(i.querySelectorAll("input")[1].value);
|
10244
|
+
});
|
10245
|
+
items.forEach((item) => {
|
10246
|
+
if (itemTargetIds.includes(item.blobId)) {
|
10247
|
+
return;
|
10248
|
+
}
|
10249
|
+
this.addItem(item);
|
10250
|
+
});
|
10189
10251
|
}
|
10190
10252
|
addItem(item) {
|
10191
10253
|
const currentItem = this.itemByBlobId(item.blobId);
|
@@ -10230,11 +10292,18 @@ var media_controller_default = class extends Controller {
|
|
10230
10292
|
const randomNumber = Math.floor(1e8 + Math.random() * 9e8);
|
10231
10293
|
return template.innerHTML.replace(regex, randomNumber);
|
10232
10294
|
}
|
10233
|
-
|
10234
|
-
this.
|
10295
|
+
removeAllDeselectedItems(items) {
|
10296
|
+
this.removeDeselectedItems(items, this.itemTargets);
|
10235
10297
|
}
|
10236
|
-
|
10298
|
+
removeDeselectedItems(elements, items) {
|
10299
|
+
const returnedBlobIds = elements.map((e) => {
|
10300
|
+
return e.blobId;
|
10301
|
+
});
|
10237
10302
|
items.forEach((item) => {
|
10303
|
+
const blobId = parseInt(item.querySelectorAll("input")[1].value);
|
10304
|
+
if (returnedBlobIds.includes(blobId)) {
|
10305
|
+
return;
|
10306
|
+
}
|
10238
10307
|
this.removeItem(item);
|
10239
10308
|
});
|
10240
10309
|
}
|
@@ -10267,6 +10336,9 @@ var media_modal_controller_default = class extends Controller {
|
|
10267
10336
|
static get targets() {
|
10268
10337
|
return ["idCheckbox", "item", "form", "selectButton", "placeholder", "count"];
|
10269
10338
|
}
|
10339
|
+
static get values() {
|
10340
|
+
return { ids: Array };
|
10341
|
+
}
|
10270
10342
|
connect() {
|
10271
10343
|
this.validate();
|
10272
10344
|
this.updateCount();
|
@@ -10278,34 +10350,64 @@ var media_modal_controller_default = class extends Controller {
|
|
10278
10350
|
this.hidePlaceholder();
|
10279
10351
|
this.triggerFormSubmission();
|
10280
10352
|
}
|
10281
|
-
inputChange() {
|
10282
|
-
this.
|
10353
|
+
inputChange(event) {
|
10354
|
+
this.handleIdsUpdate(event.target);
|
10283
10355
|
this.updateCount();
|
10284
10356
|
}
|
10285
10357
|
hidePlaceholder() {
|
10286
10358
|
this.placeholderTarget.classList.add("d-none");
|
10287
10359
|
}
|
10288
|
-
|
10360
|
+
handleIdsUpdate(element) {
|
10361
|
+
if (element.checked) {
|
10362
|
+
let arr = this.idsValue;
|
10363
|
+
arr.push(element.value);
|
10364
|
+
this.idsValue = arr;
|
10365
|
+
} else {
|
10366
|
+
this.idsValue = this.idsValue.filter((value) => {
|
10367
|
+
return element.value !== value;
|
10368
|
+
});
|
10369
|
+
}
|
10370
|
+
}
|
10371
|
+
itemTargetConnected(element) {
|
10372
|
+
this.updateItem(element.querySelector("input"));
|
10373
|
+
}
|
10374
|
+
updateItem(element) {
|
10375
|
+
const arr = this.idsValue;
|
10376
|
+
if (arr.includes(element.value)) {
|
10377
|
+
element.checked = true;
|
10378
|
+
} else {
|
10379
|
+
element.checked = false;
|
10380
|
+
}
|
10381
|
+
}
|
10382
|
+
idsValueChanged() {
|
10383
|
+
for (const item of this.itemTargets) {
|
10384
|
+
this.updateItem(item.querySelector("input"));
|
10385
|
+
}
|
10289
10386
|
this.validate();
|
10290
10387
|
}
|
10291
10388
|
dispatchSelectionEvent() {
|
10292
10389
|
document.dispatchEvent(new CustomEvent("mediaSelectionSubmitted", {
|
10293
10390
|
detail: {
|
10294
10391
|
name: this.element.dataset.name,
|
10295
|
-
items: this.renderItemsForEvent(
|
10392
|
+
items: this.renderItemsForEvent()
|
10296
10393
|
}
|
10297
10394
|
}));
|
10298
10395
|
}
|
10299
10396
|
triggerFormSubmission() {
|
10300
10397
|
this.formTarget.requestSubmit();
|
10301
10398
|
}
|
10302
|
-
renderItemsForEvent(
|
10303
|
-
return
|
10399
|
+
renderItemsForEvent() {
|
10400
|
+
return this.idsValue.map((item) => this.renderItemForEvent(item)).filter((i) => {
|
10401
|
+
return i !== void 0;
|
10402
|
+
});
|
10304
10403
|
}
|
10305
10404
|
renderItemForEvent(item) {
|
10405
|
+
const id = parseInt(item);
|
10406
|
+
const blob_id = `#blob_${id}`;
|
10407
|
+
const element = this.element.querySelector(blob_id);
|
10306
10408
|
return {
|
10307
|
-
blobId:
|
10308
|
-
thumbnail:
|
10409
|
+
blobId: id,
|
10410
|
+
thumbnail: element ? element.querySelector(".h-thumbnail") : ""
|
10309
10411
|
};
|
10310
10412
|
}
|
10311
10413
|
selectedItems() {
|
@@ -10315,7 +10417,7 @@ var media_modal_controller_default = class extends Controller {
|
|
10315
10417
|
});
|
10316
10418
|
}
|
10317
10419
|
selectedItemsCount() {
|
10318
|
-
return this.
|
10420
|
+
return this.idsValue.length;
|
10319
10421
|
}
|
10320
10422
|
minSelectedItems() {
|
10321
10423
|
return parseInt(this.element.dataset.min, 10) || 0;
|
@@ -10341,7 +10443,7 @@ var media_modal_controller_default = class extends Controller {
|
|
10341
10443
|
return count >= this.minSelectedItems() && count <= this.maxSelectedItems();
|
10342
10444
|
}
|
10343
10445
|
updateCount() {
|
10344
|
-
this.countTarget.innerHTML = this.
|
10446
|
+
this.countTarget.innerHTML = this.selectedItemsCount();
|
10345
10447
|
}
|
10346
10448
|
};
|
10347
10449
|
|
@@ -15901,6 +16003,7 @@ var Headmin = class {
|
|
15901
16003
|
Stimulus.register("filters", filters_controller_default);
|
15902
16004
|
Stimulus.register("flatpickr", flatpickr_controller_default);
|
15903
16005
|
Stimulus.register("hello", hello_controller_default);
|
16006
|
+
Stimulus.register("infinite-scroller", infinite_scroller_controller_default);
|
15904
16007
|
Stimulus.register("media", media_controller_default);
|
15905
16008
|
Stimulus.register("media-modal", media_modal_controller_default);
|
15906
16009
|
Stimulus.register("notification", notification_controller_default);
|
@@ -1,7 +1,7 @@
|
|
1
1
|
@charset "UTF-8";
|
2
2
|
@import "https://cdn.jsdelivr.net/npm/bootstrap-icons@1.5.0/font/bootstrap-icons.css";
|
3
3
|
|
4
|
-
/* sass-plugin-0:/
|
4
|
+
/* sass-plugin-0:/usr/local/var/www/headmin/src/scss/headmin.scss */
|
5
5
|
:root {
|
6
6
|
--bs-blue: #0d6efd;
|
7
7
|
--bs-indigo: #6610f2;
|
@@ -1,4 +1,5 @@
|
|
1
1
|
class Headmin::MediaController < HeadminController
|
2
|
+
include Headmin::Pagination
|
2
3
|
layout false
|
3
4
|
|
4
5
|
def index
|
@@ -9,7 +10,13 @@ class Headmin::MediaController < HeadminController
|
|
9
10
|
.order(created_at: :desc)
|
10
11
|
.group(:id)
|
11
12
|
.all
|
13
|
+
@blobs = paginate(@blobs)
|
12
14
|
@mimetypes = media_params[:mimetype]
|
15
|
+
|
16
|
+
respond_to do |format|
|
17
|
+
format.html
|
18
|
+
format.turbo_stream
|
19
|
+
end
|
13
20
|
end
|
14
21
|
|
15
22
|
def create
|
@@ -38,6 +45,10 @@ class Headmin::MediaController < HeadminController
|
|
38
45
|
end
|
39
46
|
end
|
40
47
|
|
48
|
+
def thumbnail
|
49
|
+
@blob = ActiveStorage::Blob.find(params[:id])
|
50
|
+
end
|
51
|
+
|
41
52
|
private
|
42
53
|
|
43
54
|
def media_params
|
@@ -1,16 +1,5 @@
|
|
1
1
|
module Headmin
|
2
2
|
module FormHelper
|
3
|
-
# TODO: cleanup after input field refactoring
|
4
|
-
def form_field_validation_id(form, name)
|
5
|
-
[form.object_name, name.to_s, "validation"].join("_").parameterize.underscore
|
6
|
-
end
|
7
|
-
|
8
|
-
# TODO: cleanup after input field refactoring
|
9
|
-
def form_field_validation_class(form, name)
|
10
|
-
return nil if request.get?
|
11
|
-
form.object.errors.has_key?(name) ? "is-invalid" : "is-valid"
|
12
|
-
end
|
13
|
-
|
14
3
|
# Outputs currently present query parameters as hidden fields for a given form
|
15
4
|
#
|
16
5
|
# https://example.com/products?amount=1&type[]=food&type[]=beverage
|
@@ -69,18 +69,6 @@ module Headmin
|
|
69
69
|
def has_many?
|
70
70
|
macro == :has_many
|
71
71
|
end
|
72
|
-
|
73
|
-
private
|
74
|
-
|
75
|
-
def is_i?(value)
|
76
|
-
# Regex: this selects signed digits (\d) only, it is then checked to the value, e.g.:
|
77
|
-
# is_i?("3") = true
|
78
|
-
# is_i?("-3") = true
|
79
|
-
# is_i?("3a") = false
|
80
|
-
# is_i?("3.2") = false
|
81
|
-
|
82
|
-
/\A[-+]?\d+\z/.match(value)
|
83
|
-
end
|
84
72
|
end
|
85
73
|
end
|
86
74
|
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Headmin
|
2
|
+
module Filter
|
3
|
+
class AssociationCount < Headmin::Filter::Base
|
4
|
+
OPERATORS = %w[eq not_eq gt gteq lt lteq]
|
5
|
+
|
6
|
+
def cast_value(value)
|
7
|
+
is_i?(value) ? value.to_i : 0
|
8
|
+
end
|
9
|
+
|
10
|
+
def query(collection)
|
11
|
+
return collection unless @instructions.any?
|
12
|
+
|
13
|
+
# Store the collections' class for later use
|
14
|
+
@parent_class = collection.is_a?(Class) ? collection : collection.klass
|
15
|
+
|
16
|
+
# Join table and group on primary key if necessary
|
17
|
+
collection = collection.left_joins(reflection.name).group(primary_key)
|
18
|
+
|
19
|
+
# Build query and execute
|
20
|
+
query = nil
|
21
|
+
@instructions.each do |instruction|
|
22
|
+
query = build_query(query, collection, instruction)
|
23
|
+
end
|
24
|
+
collection.having(query)
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def build_query(query, collection, instruction)
|
30
|
+
query_operator = convert_to_query_operator(instruction[:operator])
|
31
|
+
query_value = convert_to_query_value(instruction[:value], instruction[:operator])
|
32
|
+
|
33
|
+
query_operator, query_value = process_null_operators(query_operator, query_value)
|
34
|
+
new_query = reflection.klass.arel_table[reflection.foreign_key.to_sym].count.send(query_operator, query_value)
|
35
|
+
query ? query.send(instruction[:conditional], new_query) : new_query
|
36
|
+
end
|
37
|
+
|
38
|
+
def reflection
|
39
|
+
reflection = @parent_class.reflect_on_association(attribute.to_s.split("_")[0].to_sym)
|
40
|
+
raise UnknownAssociation if reflection.nil?
|
41
|
+
|
42
|
+
reflection
|
43
|
+
end
|
44
|
+
|
45
|
+
def primary_key
|
46
|
+
"#{@parent_class.table_name}.#{@parent_class.primary_key}"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module Headmin
|
2
|
+
module Filter
|
3
|
+
class AssociationCountView < FilterView
|
4
|
+
def base_options
|
5
|
+
keys = %i[name label form]
|
6
|
+
options = to_h.slice(*keys)
|
7
|
+
default_base_options.merge(options)
|
8
|
+
end
|
9
|
+
|
10
|
+
def input_options
|
11
|
+
keys = %i[form]
|
12
|
+
options = to_h.slice(*keys)
|
13
|
+
default_input_options.merge(options)
|
14
|
+
end
|
15
|
+
|
16
|
+
def collection
|
17
|
+
@collection || association_model.all.map { |record| [record.to_s, record.id] }
|
18
|
+
end
|
19
|
+
|
20
|
+
def association_model
|
21
|
+
reflection.klass
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def id
|
27
|
+
"#{name}_value"
|
28
|
+
end
|
29
|
+
|
30
|
+
def name
|
31
|
+
@name || attribute
|
32
|
+
end
|
33
|
+
|
34
|
+
def attribute
|
35
|
+
"#{@association}_count"
|
36
|
+
end
|
37
|
+
|
38
|
+
def label
|
39
|
+
@label || I18n.t("attributes.#{attribute}", default: "#{I18n.t("attributes.count")} #{association_model.model_name.human(count: collection? ? 2 : 1)}")
|
40
|
+
end
|
41
|
+
|
42
|
+
def reflection
|
43
|
+
form.object.class.reflect_on_association(@association)
|
44
|
+
end
|
45
|
+
|
46
|
+
def collection?
|
47
|
+
reflection.collection?
|
48
|
+
end
|
49
|
+
|
50
|
+
def default_base_options
|
51
|
+
{
|
52
|
+
label: label,
|
53
|
+
name: attribute,
|
54
|
+
display_values: collection,
|
55
|
+
filter: Headmin::Filter::AssociationCount.new(name, @params),
|
56
|
+
allowed_operators: Headmin::Filter::AssociationCount::OPERATORS
|
57
|
+
}
|
58
|
+
end
|
59
|
+
|
60
|
+
def default_input_options
|
61
|
+
{
|
62
|
+
label: false,
|
63
|
+
wrapper: false,
|
64
|
+
name: nil,
|
65
|
+
id: id,
|
66
|
+
data: {
|
67
|
+
action: "change->filter#updateHiddenValue",
|
68
|
+
filter_target: "value",
|
69
|
+
filter_row_target: "original"
|
70
|
+
},
|
71
|
+
collection: collection,
|
72
|
+
selected: selected,
|
73
|
+
class: "form-control"
|
74
|
+
}
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -100,6 +100,16 @@ module Headmin
|
|
100
100
|
query ? query.send(instruction[:conditional], new_query) : new_query
|
101
101
|
end
|
102
102
|
|
103
|
+
def is_i?(value)
|
104
|
+
# Regex: this selects signed digits (\d) only, it is then checked to the value, e.g.:
|
105
|
+
# is_i?("3") = true
|
106
|
+
# is_i?("-3") = true
|
107
|
+
# is_i?("3a") = false
|
108
|
+
# is_i?("3.2") = false
|
109
|
+
|
110
|
+
/\A[-+]?\d+\z/.match(value)
|
111
|
+
end
|
112
|
+
|
103
113
|
private
|
104
114
|
|
105
115
|
def parse(string)
|
@@ -56,8 +56,15 @@ module Headmin
|
|
56
56
|
def display_value(value)
|
57
57
|
# This uses the default date format of headmin.
|
58
58
|
# Can be overwritten by setting default date format of the application.
|
59
|
-
|
59
|
+
|
60
|
+
current_operator = instructions.find { |instruction| instruction[:value] == value }[:operator]
|
61
|
+
|
62
|
+
# To make the operators eq and not_eq work, we pass a range.
|
63
|
+
# However, display value should return this as a date and not a range.
|
64
|
+
if value.class.to_s == "Range" && (current_operator == "eq" || current_operator == "not_eq")
|
60
65
|
I18n.l(value.last.to_date)
|
66
|
+
elsif values.class.to_s
|
67
|
+
"#{I18n.l(value.first.to_date)} - #{I18n.l(value.last.to_date)}"
|
61
68
|
else
|
62
69
|
I18n.l(value.to_date)
|
63
70
|
end
|
@@ -28,7 +28,7 @@ module Headmin
|
|
28
28
|
label: label,
|
29
29
|
name: attribute,
|
30
30
|
filter: Headmin::Filter::Date.new(name, @params),
|
31
|
-
allowed_operators: Headmin::Filter::Date::OPERATORS - %w[in not_in
|
31
|
+
allowed_operators: Headmin::Filter::Date::OPERATORS - %w[in not_in]
|
32
32
|
}
|
33
33
|
end
|
34
34
|
|
@@ -10,18 +10,6 @@ module Headmin
|
|
10
10
|
def to_s
|
11
11
|
string
|
12
12
|
end
|
13
|
-
|
14
|
-
private
|
15
|
-
|
16
|
-
def is_i?(value)
|
17
|
-
# Regex: this selects signed digits (\d) only, it is then checked to the value, e.g.:
|
18
|
-
# is_i?("3") = true
|
19
|
-
# is_i?("-3") = true
|
20
|
-
# is_i?("3a") = false
|
21
|
-
# is_i?("3.2") = false
|
22
|
-
|
23
|
-
/\A[-+]?\d+\z/.match(value)
|
24
|
-
end
|
25
13
|
end
|
26
14
|
end
|
27
15
|
end
|
@@ -24,7 +24,9 @@ module Headmin
|
|
24
24
|
is_null: "○ #{I18n.t("headmin.filters.operators.is_null")}",
|
25
25
|
is_not_null: "● #{I18n.t("headmin.filters.operators.is_not_null")}",
|
26
26
|
in: "∋ #{I18n.t("headmin.filters.operators.in")}",
|
27
|
-
not_in: "∌ #{I18n.t("headmin.filters.operators.not_in")}"
|
27
|
+
not_in: "∌ #{I18n.t("headmin.filters.operators.not_in")}",
|
28
|
+
between: "↔ #{I18n.t("headmin.filters.operators.between")}",
|
29
|
+
not_between: "⥈ #{I18n.t("headmin.filters.operators.not_between")}"
|
28
30
|
}
|
29
31
|
end
|
30
32
|
end
|
@@ -23,7 +23,7 @@
|
|
23
23
|
|
24
24
|
<!-- Default parameters (e.g. sorting, pagination) -->
|
25
25
|
<% default_params.except("page").each do |name, value| %>
|
26
|
-
<%=
|
26
|
+
<%= hidden_field_tag(name, value) %>
|
27
27
|
<% end %>
|
28
28
|
|
29
29
|
<div class="d-flex flex-column flex-md-row align-content-start align-items-md-start">
|
@@ -0,0 +1,28 @@
|
|
1
|
+
<%
|
2
|
+
# headmin/filters/association_count
|
3
|
+
#
|
4
|
+
# ==== Required parameters
|
5
|
+
# * +association+ - Name of the association that has to be counted
|
6
|
+
# * +form+ - Form object
|
7
|
+
#
|
8
|
+
# ==== Optional parameters
|
9
|
+
# * +label+ - Display label
|
10
|
+
# * +name+ - Name of the filter parameter
|
11
|
+
#
|
12
|
+
# ==== Examples
|
13
|
+
# Basic version (one-to-many)
|
14
|
+
# <%= render "headmin/filters", url: admin_orders_path do |form| %#>
|
15
|
+
# <%= render "headmin/filters/association_count", form: form, association: :beverages %#>
|
16
|
+
# <% end %#>
|
17
|
+
#
|
18
|
+
# Basic version (one-to-one)
|
19
|
+
# <%= render "headmin/filters", url: admin_orders_path do |form| %#>
|
20
|
+
# <%= render "headmin/filters/association_count", form: form, association: :beverage %#>
|
21
|
+
# <% end %#>
|
22
|
+
|
23
|
+
number = Headmin::Filter::AssociationCountView.new(local_assigns.merge(params: params))
|
24
|
+
%>
|
25
|
+
|
26
|
+
<%= render "headmin/filters/base", number.base_options do |value| %>
|
27
|
+
<%= render "headmin/forms/number", number.input_options.merge({value: value}) %>
|
28
|
+
<% end %>
|
@@ -20,4 +20,5 @@
|
|
20
20
|
|
21
21
|
<%= render "headmin/filters/base", date.base_options do |value| %>
|
22
22
|
<%= render "headmin/forms/date", date.input_options.merge({value: value}) %>
|
23
|
+
<%= render "headmin/forms/date_range", date.input_options.merge({start: {value: value.class.to_s == "Range" ? value.first : value}, end: {value: value.class.to_s == "Range" ? value.last : value}}) %>
|
23
24
|
<% end %>
|
@@ -1,4 +1,4 @@
|
|
1
|
-
<div class="media-modal modal fade" tabindex="-1" data-controller="remote-modal media-modal" data-name="<%= name %>" data-min="<%= min %>" data-max="<%= max %>">
|
1
|
+
<div class="media-modal modal fade" tabindex="-1" data-controller="remote-modal media-modal" data-media-modal-ids-value="<%= params[:ids] ? params[:ids] : [] %>" data-name="<%= name %>" data-min="<%= min %>" data-max="<%= max %>">
|
2
2
|
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
3
3
|
<div class="modal-content">
|
4
4
|
<div class="modal-header">
|
@@ -10,15 +10,20 @@
|
|
10
10
|
<div class="modal-body">
|
11
11
|
<%= turbo_frame_tag "thumbnails", class: "d-flex flex-wrap gap-2" do %>
|
12
12
|
<% @blobs.each do |blob| %>
|
13
|
-
<%=
|
13
|
+
<%= turbo_frame_tag blob, src: headmin_media_item_thumbnail_path(blob), loading: "lazy" do %>
|
14
|
+
<%= render "thumbnail" %>
|
15
|
+
<% end %>
|
14
16
|
<% end %>
|
15
17
|
<div data-media-modal-target="placeholder" class="<%= "d-none" if !@blobs.empty? %>">
|
16
18
|
<p><%= t(".placeholder") %></p>
|
17
19
|
</div>
|
18
20
|
<% end %>
|
21
|
+
<div class="mt-3">
|
22
|
+
<%= render "headmin/pagination/infinite", items: @blobs %>
|
23
|
+
</div>
|
19
24
|
</div>
|
20
25
|
<div class="modal-footer">
|
21
|
-
<%= form_with url:
|
26
|
+
<%= form_with url: headmin_new_media_path, multipart: true, data: {"media-modal-target": "form"}, class: "me-auto" do |form| %>
|
22
27
|
<%= form.label :files, class: "btn h-btn-outline-light" do %>
|
23
28
|
<%= bootstrap_icon("upload") %>
|
24
29
|
<%= t(".upload") %>
|
@@ -0,0 +1,20 @@
|
|
1
|
+
<% blob = local_assigns.has_key?(:blob) ? local_assigns[:blob] : nil %>
|
2
|
+
<% if blob.present? %>
|
3
|
+
<div data-media-modal-target="item" title="<%= "#{blob.filename} (#{l(blob.created_at, format: :long)})" %>">
|
4
|
+
<!-- Input -->
|
5
|
+
<input
|
6
|
+
id="media-item-<%= blob.id %>"
|
7
|
+
type="checkbox"
|
8
|
+
value="<%= blob.id %>"
|
9
|
+
data-action="change->media-modal#inputChange"
|
10
|
+
data-media-modal-target="idCheckbox"
|
11
|
+
hidden>
|
12
|
+
|
13
|
+
<!-- Label -->
|
14
|
+
<label for="media-item-<%= blob.id %>">
|
15
|
+
<%= render "headmin/thumbnail", file: blob %>
|
16
|
+
</label>
|
17
|
+
</div>
|
18
|
+
<% else %>
|
19
|
+
<%= render "headmin/thumbnail", file: nil %>
|
20
|
+
<% end %>
|
@@ -0,0 +1,11 @@
|
|
1
|
+
<%= turbo_stream.append "thumbnails" do %>
|
2
|
+
<% @blobs.each do |blob| %>
|
3
|
+
<%= turbo_frame_tag blob, src: headmin_media_item_thumbnail_path(blob), loading: "lazy" do %>
|
4
|
+
<%= render 'thumbnail' %>
|
5
|
+
<% end %>
|
6
|
+
<% end %>
|
7
|
+
<% end %>
|
8
|
+
|
9
|
+
<%= turbo_stream.replace "infinite" do %>
|
10
|
+
<%= render "headmin/pagination/infinite", items: @blobs %>
|
11
|
+
<% end %>
|