diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index c51a8ef..4952cda 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -18,7 +18,7 @@ jobs:
matrix:
ruby: ['3.3', '3.4', '4.0']
rails: ['8.0.0', '8.1.0']
- activeadmin: ['3.2.0', '3.3.0', '3.4.0', '3.5.1']
+ activeadmin: ['3.2.0', '3.3.0', '3.4.0', '3.5.1', '4.0.0.beta23']
env:
RAILS: ${{ matrix.rails }}
AA: ${{ matrix.activeadmin }}
diff --git a/Gemfile b/Gemfile
index c1b79dc..ef01d33 100644
--- a/Gemfile
+++ b/Gemfile
@@ -5,9 +5,16 @@ default_rails_version = '8.0.0'
default_activeadmin_version = '3.2.0'
gem 'rails', "~> #{ENV['RAILS'] || default_rails_version}"
-gem 'activeadmin', "~> #{ENV['AA'] || default_activeadmin_version}"
-gem 'sprockets-rails'
-gem 'sass-rails'
+activeadmin_version = ENV['AA'] || default_activeadmin_version
+
+gem 'activeadmin', "~> #{activeadmin_version}"
+if Gem::Version.new(activeadmin_version) >= Gem::Version.new('4.0.0.beta1')
+ gem 'propshaft'
+ gem 'importmap-rails'
+else
+ gem 'sprockets-rails'
+ gem 'sass-rails'
+end
group :test do
gem 'simplecov', require: false
diff --git a/README.md b/README.md
index 9cfad58..1d6432f 100644
--- a/README.md
+++ b/README.md
@@ -8,6 +8,8 @@
Adds XDSoft's DateTime picker as a `date_time_picker` input for forms, and `date_time_range` for filters.
+On ActiveAdmin 4 the same inputs render the browser's native date and time controls instead; see [ActiveAdmin 4](#activeadmin-4).
+

## Installation
@@ -89,6 +91,40 @@ The `date_time_picker` input accepts many of the options available to the standa
end
```
+## ActiveAdmin 4
+
+ActiveAdmin 4 ships without jQuery and Sprockets, so on ActiveAdmin 4 the gem
+does not load the XDSoft picker. The `date_time_picker` input and the
+`date_time_range` filter render native `` fields
+(`type="date"` for a `date` column), styled by ActiveAdmin itself. There is no
+JavaScript or CSS to install: skip the Sprockets and NPM steps above.
+
+Values use the HTML format, `YYYY-MM-DDTHH:MM` (`YYYY-MM-DD` for date fields).
+`ActiveAdminDatetimepicker::Base.format` applies to ActiveAdmin 3 only.
+
+Only these `datepicker_options` (and `default_datetime_picker_options`) have a
+native equivalent and are applied on ActiveAdmin 4. Everything else is ignored.
+
+| Option | Native attribute |
+| --- | --- |
+| `min_date`, `min_date_time` | `min` |
+| `max_date`, `max_date_time` | `max` |
+| `step` (minutes) | `step` (seconds, `datetime-local` only) |
+
+`min`/`max` must be absolute dates (a `Date`, `Time` or a string like
+`"2024-01-31"`); relative values such as `"+3D"` are dropped. An explicit
+`input_html: { min: ... }` wins over the mapped value.
+
+Unlike the XDSoft picker, the browser enforces `min`, `max` and `step` when the
+form is submitted. A stored value outside the range, or off the step (10:07 with
+`step: 15`), blocks the save until it is changed.
+
+The markup carries `data-*` hooks for tests and other plugins:
+
+- every input: `data-datetimepicker-input`
+- the filter wrapper: `data-datetimepicker-range` (on ActiveAdmin 4 it also keeps the `filter_date_time_range` class)
+- the filter inputs: `data-datetimepicker-range-input="from"` and `"to"`
+
## Override behaviour in initializer
```ruby
diff --git a/active_admin_datetimepicker.gemspec b/active_admin_datetimepicker.gemspec
index 5889802..427744b 100644
--- a/active_admin_datetimepicker.gemspec
+++ b/active_admin_datetimepicker.gemspec
@@ -10,7 +10,7 @@ Gem::Specification.new do |spec|
spec.email = ["fedoronchuk@gmail.com"]
spec.summary = %q{datetimepicker extension for ActiveAdmin}
- spec.description = %q{Integrate jQuery xdan datetimepicker plugin to ActiveAdmin}
+ spec.description = %q{Integrate jQuery xdan datetimepicker plugin to ActiveAdmin 3, native date and time inputs on ActiveAdmin 4}
spec.homepage = "https://github.com/activeadmin-plugins/activeadmin_datetimepicker"
spec.license = "MIT"
@@ -25,5 +25,5 @@ Gem::Specification.new do |spec|
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
spec.require_paths = ["lib"]
- spec.add_dependency "activeadmin", ">= 3.0", "< 4.0"
+ spec.add_dependency "activeadmin", ">= 3.0", "< 5.0.0.a"
end
diff --git a/config/initializers/active_admin_datetimepicker.rb b/config/initializers/active_admin_datetimepicker.rb
index 65f6806..54e4c96 100644
--- a/config/initializers/active_admin_datetimepicker.rb
+++ b/config/initializers/active_admin_datetimepicker.rb
@@ -1,7 +1,16 @@
+# ActiveAdmin 4 submits native datetime-local values ("2024-01-31T10:00").
+# Typed columns cast them to times; for an untyped ransacker the value stays a
+# string, so the "T" is swapped for the space the database compares against.
+iso_to_db = lambda do |value|
+ value.is_a?(String) ? value.sub(/\A(\d{4}-\d{2}-\d{2})T(?=\d{2}:\d{2})/, '\1 ') : value
+end
+
Ransack.configure do |config|
config.add_predicate 'gteq_datetime_picker',
- arel_predicate: 'gteq'
+ arel_predicate: 'gteq',
+ formatter: iso_to_db
config.add_predicate 'lteq_datetime_picker',
- arel_predicate: 'lt'
+ arel_predicate: 'lt',
+ formatter: iso_to_db
end
diff --git a/lib/active_admin_datetimepicker.rb b/lib/active_admin_datetimepicker.rb
index 6d97cd5..5da2048 100644
--- a/lib/active_admin_datetimepicker.rb
+++ b/lib/active_admin_datetimepicker.rb
@@ -1,4 +1,15 @@
require 'activeadmin'
+
+module ActiveAdminDatetimepicker
+ # ActiveAdmin 4 drops jQuery and Sprockets, so the plugin renders native
+ # date/time inputs there instead of the xdsoft picker.
+ def self.aa_v4?
+ return @aa_v4 if defined?(@aa_v4)
+
+ @aa_v4 = Gem::Version.new(ActiveAdmin::VERSION) >= Gem::Version.new('4.0.0.beta1')
+ end
+end
+
require 'active_admin_datetimepicker/version'
require 'active_admin_datetimepicker/base'
require 'active_admin_datetimepicker/inputs/date_time_picker_input'
diff --git a/lib/active_admin_datetimepicker/base.rb b/lib/active_admin_datetimepicker/base.rb
index 4b29b66..b5c07dc 100644
--- a/lib/active_admin_datetimepicker/base.rb
+++ b/lib/active_admin_datetimepicker/base.rb
@@ -5,6 +5,12 @@ module Base
mattr_accessor :format
@@format = '%Y-%m-%d %H:%M'
+ # Value formats the browser accepts for native inputs (HTML spec), used on ActiveAdmin 4.
+ NATIVE_FORMATS = {
+ 'datetime-local' => '%Y-%m-%dT%H:%M',
+ 'date' => '%Y-%m-%d'
+ }.freeze
+
def html_class
'date-time-picker'
end
@@ -17,9 +23,14 @@ def input_html_options(input_name = nil, placeholder = nil)
super().tap do |options|
options[:class] = [self.options[:class], html_class].compact.join(' ')
options[:data] ||= input_html_data
- options[:data].merge!(datepicker_options: datetime_picker_options)
- options[:value] = input_value(input_name)
- options[:maxlength] = 19
+ if ActiveAdminDatetimepicker.aa_v4?
+ native_input_html_options(options, input_name)
+ else
+ options[:data].merge!(datepicker_options: datetime_picker_options)
+ options[:value] = input_value(input_name)
+ options[:maxlength] = 19
+ end
+ options[:data].merge!(datetimepicker_input: true)
options[:placeholder] = placeholder unless placeholder.nil?
end
end
@@ -45,8 +56,73 @@ def datetime_picker_options
end
end
+ # A date column gets a date-only control; datetime columns and
+ # ransackers without a column get a date and time control.
+ def native_input_type
+ date_column? ? 'date' : 'datetime-local'
+ end
+
+ # Covers DB columns and attributes declared with the Attributes API.
+ def date_column?
+ return column.type == :date if column&.type
+
+ object.class.respond_to?(:attribute_types) && object.class.attribute_types[method.to_s]&.type == :date
+ end
+
+ # Stored strings are parsed as leniently as input_value does on ActiveAdmin 3,
+ # so a non-ISO value is not blanked and wiped on save.
+ def native_input_value(input_name = nil)
+ format_native(object.public_send(input_name || method), strict: false)
+ end
+
protected
+ def native_input_html_options(options, input_name)
+ options[:type] = native_input_type
+ options[:value] = native_input_value(input_name)
+ options.delete(:maxlength)
+ options.delete(:size)
+ native_picker_attributes.each { |name, value| options[name] = value unless options.key?(name) }
+ end
+
+ # Only the xdsoft options with a native equivalent are mapped; the rest
+ # configure the jQuery widget, which ActiveAdmin 4 pages do not load.
+ def native_picker_attributes
+ picker_options = datetime_picker_options.transform_keys { |k| k.to_s.camelcase(:lower) }
+ attributes = {}
+ min = format_native(picker_options['minDateTime']) || native_day_bound(picker_options['minDate'], '00:00')
+ max = format_native(picker_options['maxDateTime']) || native_day_bound(picker_options['maxDate'], '23:59')
+ attributes[:min] = min if min
+ attributes[:max] = max if max
+ step = picker_options['step'].to_i
+ # xdsoft counts the step in minutes, the HTML attribute in seconds.
+ attributes[:step] = step * 60 if step > 0 && native_input_type == 'datetime-local'
+ attributes
+ end
+
+ # xdsoft's minDate and maxDate are whole days, so a datetime-local bound
+ # starts at 00:00 of the first day and ends at 23:59 of the last.
+ def native_day_bound(value, time)
+ bound = format_native(value)
+ bound && native_input_type == 'datetime-local' ? "#{bound[0, 10]}T#{time}" : bound
+ end
+
+ def format_native(value, strict: true)
+ value = value.call if value.respond_to?(:call)
+ return nil if value.blank?
+ if value.respond_to?(:strftime)
+ # Times are shown in the app's zone, like the record values.
+ value = value.in_time_zone if value.is_a?(Time) || value.is_a?(DateTime)
+ else
+ # Relative xdsoft values such as "+3D" have no native equivalent.
+ return nil if strict && !value.to_s.match?(%r{\A\d{4}[-/]\d{1,2}[-/]\d{1,2}})
+ value = DateTime.parse(value.to_s)
+ end
+ value.strftime(NATIVE_FORMATS.fetch(native_input_type))
+ rescue ArgumentError
+ nil
+ end
+
def _default_datetime_picker_options
res = default_datetime_picker_options.map do |k, v|
if v.respond_to?(:call) || v.is_a?(Proc)
@@ -59,4 +135,3 @@ def _default_datetime_picker_options
end
end
end
-
diff --git a/lib/active_admin_datetimepicker/inputs/filters/date_time_range_input.rb b/lib/active_admin_datetimepicker/inputs/filters/date_time_range_input.rb
index 5f3e859..bee36b3 100644
--- a/lib/active_admin_datetimepicker/inputs/filters/date_time_range_input.rb
+++ b/lib/active_admin_datetimepicker/inputs/filters/date_time_range_input.rb
@@ -5,6 +5,29 @@ class DateTimeRangeInput < DateRangeInput
include Base
include ActiveAdminDatetimepicker::Base
+ # ActiveAdmin 4's DateRangeInput renders date_field pairs, which drop
+ # the time; this keeps its markup but emits native datetime fields.
+ def to_html
+ return super unless ActiveAdminDatetimepicker.aa_v4?
+
+ input_wrapping do
+ [ label_html,
+ '