Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down
13 changes: 10 additions & 3 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

![ActiveAdminDatetimepicker](https://raw.githubusercontent.com/activeadmin-plugins/active_admin_datetimepicker/master/screen/screen.png "ActiveAdminDatetimepicker")

## Installation
Expand Down Expand Up @@ -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 `<input type="datetime-local">` 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
Expand Down
4 changes: 2 additions & 2 deletions active_admin_datetimepicker.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -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
13 changes: 11 additions & 2 deletions config/initializers/active_admin_datetimepicker.rb
Original file line number Diff line number Diff line change
@@ -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
11 changes: 11 additions & 0 deletions lib/active_admin_datetimepicker.rb
Original file line number Diff line number Diff line change
@@ -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'
Expand Down
83 changes: 79 additions & 4 deletions lib/active_admin_datetimepicker/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -59,4 +135,3 @@ def _default_datetime_picker_options
end
end
end

Original file line number Diff line number Diff line change
Expand Up @@ -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,
'<div class="filters-form-input-group">',
builder.text_field(gt_input_name, input_html_options_for(gt_input_name, gt_input_placeholder)),
builder.text_field(lt_input_name, input_html_options_for(lt_input_name, lt_input_placeholder)),
'</div>'
].join("\n").html_safe
end
end

def wrapper_html_options
super.tap do |options|
# ActiveAdmin 4 dropped the filter_<as> class that active_admin_date_range_preset looks for.
options[:class] = "#{options[:class]} filter_date_time_range" if ActiveAdminDatetimepicker.aa_v4?
options[:data] = (options[:data] || {}).merge(datetimepicker_range: true)
end
end

def input_html_options(input_name = gt_input_name, placeholder = gt_input_placeholder)
super.tap do |options|
options[:class] = html_class
Expand All @@ -13,16 +36,22 @@ def input_html_options(input_name = gt_input_name, placeholder = gt_input_placeh

# This method is for compatibility for ActiveAdmin 2.6
def input_html_options_for(input_name, placeholder)
super.merge placeholder: placeholder,
value: input_value(input_name)
options = super
value = ActiveAdminDatetimepicker.aa_v4? ? native_input_value(input_name) : input_value(input_name)
bound = input_name == gt_input_name ? 'from' : 'to'
options.merge(
placeholder: placeholder,
value: value,
data: (options[:data] || {}).merge(datetimepicker_range_input: bound)
)
end

def gt_input_name
column && column.type == :date ? super : "#{method}_gteq_datetime_picker"
date_column? ? super : "#{method}_gteq_datetime_picker"
end

def lt_input_name
column && column.type == :date ? super : "#{method}_lteq_datetime_picker"
date_column? ? super : "#{method}_lteq_datetime_picker"
end

end
Expand Down
20 changes: 20 additions & 0 deletions spec/data_hooks_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
require 'spec_helper'

describe 'data-* hooks', type: :feature do
before { add_author_resource }

it 'marks the filter range wrapper and its from/to inputs' do
visit '/admin/authors'

within('#q_created_at_input[data-datetimepicker-range]') do
expect(page).to have_css('input#q_created_at_gteq_datetime_picker[data-datetimepicker-input][data-datetimepicker-range-input="from"]')
expect(page).to have_css('input#q_created_at_lteq_datetime_picker[data-datetimepicker-input][data-datetimepicker-range-input="to"]')
end
end

it 'marks the form input' do
visit '/admin/authors/new'

expect(page).to have_css('input#author_birthday[data-datetimepicker-input]')
end
end
2 changes: 1 addition & 1 deletion spec/edit_form_spec.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
require 'spec_helper'

describe 'authors index', type: :feature, js: true do
describe 'authors index', type: :feature, js: true, aa: 3 do
before do
add_author_resource
end
Expand Down
2 changes: 1 addition & 1 deletion spec/filter_form_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ def pick_date(input_selector, date_num)
sleep 0.2
end

describe 'authors index', type: :feature, js: true do
describe 'authors index', type: :feature, js: true, aa: 3 do
before do
add_author_resource
end
Expand Down
Loading
Loading