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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,19 @@ All notable changes to this project will be documented in this file.

### Added

- **Extensible server-rendered alerts**: `Folio::Console::Ui::AlertComponent` now accepts `stimulus_controllers:` (array of extra Stimulus controller identifiers to mount alongside `f-c-ui-alert`) and `data:` (extra data attributes merged onto the root element). `Folio::Console::Ui::FlashComponent` reads the parallel `alert_stimulus_controllers` / `alert_data` keys from the Rails flash hash and forwards them to the rendered alert. Host apps can now attach custom Stimulus controllers (e.g. background-job progress trackers) directly to flashes set by `redirect_to … flash: { … }` without monkey-patching the components.
- **`Folio::File.default_file_order` scope** — exposes the canonical newest-first ordering (`created_at DESC, id DESC`) used by console file listings and pickers, including a deterministic `id` tiebreaker for stable pagination.

### Changed

- **Default file search results are sorted newest first** — search results in `Folio::Console::FileControllerBase#index` and `Folio::Console::Api::FileControllerBase#index_json` now apply `created_at DESC, id DESC` after `filter_by_params`, so the newest uploads appear first regardless of which filter combination is active.
- **`ImageObject` `creditText`**: now uses `Folio::File#credit_text`, deduplicating matching `author` / `attribution_source` (e.g. `"Reuters / Reuters"` → `"Reuters"`) and falling back to `file_list_source` when both are
blank.
- **`stimulus_lightbox_item`**: when called with a `Folio::FilePlacement::Base`, the helper now respects placement-level overrides — caption uses `description_with_fallback` (placement description, then file description) and author uses `file.attribution_source` with fallback to `file.author`. Previously file-level metadata was used regardless of placement overrides, so a placement caption set by an editor was silently ignored by the lightbox while still being shown in the visible figcaption (rendered by `Folio::Console::Ui::ImageComponent` and downstream consumers via `description_with_fallback`). When called with a standalone `Folio::File` the behavior is unchanged. A new `author:` keyword argument was added symmetric to the existing `title:` — both take precedence over the defaults if you need to force specific values.

### Fixed

- **Console flash autohide on server-rendered alerts**: `Folio::Console::Ui::FlashComponent` and `AlertComponent` now honor the `autohide` flag set on Rails flash (`flash: { notice: "...", autohide: true }`). The Stimulus controller `f-c-ui-alert` reads a new `autohide` value and closes the alert after 5s. Previously `autohide` was silently filtered out during the Cells → ViewComponent refactor and only the JS-side `Ui.Alert.create` honored it; flashes set by a controller `redirect_to` stayed visible until manually dismissed. JS `Ui.Alert.create` now delegates autohide to the same Stimulus controller (single code path).
- **Console revision view**: Atoms preview iframe scrolls again in audit/revision mode when the editor uses horizontal layout (`pointer-events: auto` on `.f-c-simple-form-with-atoms__iframe` under `.f-c-layout-body--with-audit`). The read-only preview inside the iframe is unchanged (`.f-c-atoms-previews--non-interactive`).
Left form column scrolls again in audit/revision mode (`pointer-events: auto` on `.f-c-simple-form-with-atoms__form-scroll`, with `pointer-events: none` re-applied on `.f-c-simple-form-with-atoms__form-container` to keep form fields non-interactive).
- **friendly_id**: `strip_and_downcase_slug` now only normalizes the slug on new records or when the slug column was explicitly changed. Legacy records with mixed-case slugs are no longer silently downcased on every save, which previously broke `friendly_id` lookups (case-sensitive) on cached client-side URLs after the first save.
Expand Down
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ GIT
PATH
remote: .
specs:
folio (7.6.5)
folio (7.6.6)
aasm
activejob-uniqueness (>= 0.3.0)
acts-as-taggable-on
Expand Down
35 changes: 29 additions & 6 deletions app/components/folio/console/ui/alert_component.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,10 @@ window.FolioConsole.Ui.Alert.create = (data) => {
alert.dataset.controller = 'f-c-ui-alert'

if (data.autohide !== false) {
const autohideDelay = typeof data.autohide === 'number' ? data.autohide : 5000

setTimeout(() => {
const close = alert.querySelector('.f-c-ui-alert__close')
if (close) close.click()
}, autohideDelay)
alert.dataset.fCUiAlertAutohideValue = 'true'
if (typeof data.autohide === 'number') {
alert.dataset.fCUiAlertAutohideDelayValue = String(data.autohide)
}
}

if (data.data) {
Expand All @@ -78,8 +76,33 @@ window.FolioConsole.Ui.Alert.create = (data) => {
}

window.Folio.Stimulus.register('f-c-ui-alert', class extends window.Stimulus.Controller {
static values = {
autohide: Boolean,
autohideDelay: { type: Number, default: 5000 }
}

connect () {
if (this.autohideValue) {
this.autohideTimeout = setTimeout(() => {
const btn = this.element.querySelector('.f-c-ui-alert__close')
if (btn) btn.click()
}, this.autohideDelayValue)
}
}

disconnect () {
if (this.autohideTimeout) {
clearTimeout(this.autohideTimeout)
this.autohideTimeout = null
}
}

close (e) {
e.preventDefault()
if (this.autohideTimeout) {
clearTimeout(this.autohideTimeout)
this.autohideTimeout = null
}
this.element.parentNode.removeChild(this.element)
}
})
13 changes: 11 additions & 2 deletions app/components/folio/console/ui/alert_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,25 @@
class Folio::Console::Ui::AlertComponent < Folio::Console::ApplicationComponent
bem_class_name :flash

def initialize(variant: :info, closable: true, class_name: nil, flash: false, icon: nil)
def initialize(variant: :info, closable: true, class_name: nil, flash: false, icon: nil, autohide: false, stimulus_controllers: [], data: {})
@variant = variant
@closable = closable
@class_name = class_name
@flash = flash
@icon = icon
@autohide = autohide
@stimulus_controllers = Array.wrap(stimulus_controllers).compact_blank
@extra_data = (data || {}).transform_keys(&:to_s)
end

def data
stimulus_controller("f-c-ui-alert")
base = stimulus_controller("f-c-ui-alert", values: { autohide: @autohide })

if @stimulus_controllers.any?
base["controller"] = (["f-c-ui-alert"] + @stimulus_controllers).uniq.join(" ")
end

base.merge(@extra_data)
end

def icon_key
Expand Down
14 changes: 12 additions & 2 deletions app/components/folio/console/ui/flash_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,21 @@ class Folio::Console::Ui::FlashComponent < Folio::Console::ApplicationComponent
loader: :loader,
}

RESERVED_FLASH_KEYS = %w[timedout autohide alert_stimulus_controllers alert_data].freeze

def initialize(flash:)
@flash = if flash.present?
flash.filter { |key, _value| key != "timedout" && key != "autohide" }
flash_hash = flash.present? ? flash : nil

@autohide = flash_hash && (flash_hash["autohide"] || flash_hash[:autohide]) ? true : false
@alert_stimulus_controllers = flash_hash ? Array.wrap(flash_hash["alert_stimulus_controllers"] || flash_hash[:alert_stimulus_controllers]) : []
@alert_data = flash_hash ? (flash_hash["alert_data"] || flash_hash[:alert_data] || {}) : {}

@flash = if flash_hash
flash_hash.filter { |key, _value| !RESERVED_FLASH_KEYS.include?(key.to_s) }
else
flash
end
end

attr_reader :autohide, :alert_stimulus_controllers, :alert_data
end
2 changes: 1 addition & 1 deletion app/components/folio/console/ui/flash_component.slim
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
.f-c-ui-flash
- if @flash.present?
- @flash.each do |msg_type, message|
= render(Folio::Console::Ui::AlertComponent.new(variant: VARIANTS[msg_type.to_sym], flash: true))
= render(Folio::Console::Ui::AlertComponent.new(variant: VARIANTS[msg_type.to_sym], flash: true, autohide: autohide, stimulus_controllers: alert_stimulus_controllers, data: alert_data))
= message
Original file line number Diff line number Diff line change
Expand Up @@ -414,12 +414,18 @@ def broadcast_metadata_extracted(file)
end

def index_json
pagination, records = pagy(folio_console_records.ordered, items: 60)
pagination, records = pagy(index_json_records, items: 60)
meta = meta_from_pagy(pagination).merge(human_type: @klass.human_type)

json_from_records(records, Folio::Console::FileSerializer, meta:)
end

def index_json_records
return folio_console_records if @sorted_by_param

folio_console_records.default_file_order
end

def index_cache_key
"folio/console/api/site/#{Folio::Current.site.id}/file/#{@klass.model_name.plural}/index/#{@klass.count}/#{@klass.maximum(:updated_at)}"
end
Expand Down
13 changes: 13 additions & 0 deletions app/controllers/concerns/folio/console/file_controller_base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ def index
@turbo_frame_id = @klass.console_turbo_frame_id(modal: action_name == "index_for_modal",
picker: action_name == "index_for_picker")

apply_default_file_order

super
end

Expand Down Expand Up @@ -147,6 +149,17 @@ def index_pagy_items_per_page
PAGY_ITEMS
end

def apply_default_file_order
return if @sorted_by_param

records = folio_console_records
return unless records

name = folio_console_record_variable_name(plural: true)
instance_variable_set(name, records.default_file_order)
@sorted_by_param = :default_file_order
end

def message_bus_broadcast_update
return if folio_console_record.saved_changes.blank?

Expand Down
17 changes: 11 additions & 6 deletions app/helpers/folio/stimulus_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -94,11 +94,16 @@ def stimulus_lightbox
stimulus_controller(LIGHTBOX_CONTROLLER, inline: true)
end

def stimulus_lightbox_item(placement_or_file, title: nil, cloned: false, index: nil)
file = if placement_or_file.is_a?(Folio::FilePlacement::Base)
placement_or_file.file
def stimulus_lightbox_item(placement_or_file, title: nil, author: nil, cloned: false, index: nil)
if placement_or_file.is_a?(Folio::FilePlacement::Base)
placement = placement_or_file
file = placement.file
default_caption = placement.description_with_fallback
default_author = file.try(:attribution_source).presence || file.try(:author).presence
else
placement_or_file
file = placement_or_file
default_caption = file.try(:description).presence
default_author = file.try(:author).presence
end

thumb = file.thumb(Folio::LIGHTBOX_IMAGE_SIZE)
Expand All @@ -111,8 +116,8 @@ def stimulus_lightbox_item(placement_or_file, title: nil, cloned: false, index:
"src" => thumb.webp_url || thumb.url,
"w" => thumb.width,
"h" => thumb.height,
"author" => file.try(:author).presence || "",
"caption" => title || file.try(:description).presence || "",
"author" => author || default_author || "",
"caption" => title || default_caption || "",
}.to_json
}.compact
end
Expand Down
3 changes: 2 additions & 1 deletion app/models/folio/file.rb
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,8 @@ class Folio::File < Folio::ApplicationRecord
validate :validate_attribution_and_texts_if_needed

# Scopes
scope :ordered, -> { order(created_at: :desc) }
scope :ordered, -> { order(created_at: :desc, id: :desc) }
scope :default_file_order, -> { reorder(arel_table[:created_at].desc, arel_table[:id].desc) }

scope :by_placement, -> (placement_title) { order(created_at: :desc) }

Expand Down
2 changes: 1 addition & 1 deletion lib/folio/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

module Folio
VERSION = "7.6.5"
VERSION = "7.6.6"
end
37 changes: 37 additions & 0 deletions test/components/folio/console/ui/alert_component_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,41 @@ def test_render

assert_selector(".f-c-ui-alert")
end

def test_autohide_off_by_default
component = Folio::Console::Ui::AlertComponent.new(variant: :loader)

assert_equal "false", component.data["f-c-ui-alert-autohide-value"]
end

def test_autohide_opt_in
component = Folio::Console::Ui::AlertComponent.new(variant: :loader, autohide: true)

assert_equal "true", component.data["f-c-ui-alert-autohide-value"]
end

def test_extra_stimulus_controllers_are_appended
component = Folio::Console::Ui::AlertComponent.new(variant: :info, stimulus_controllers: ["x-progress", "x-other"])

assert_equal "f-c-ui-alert x-progress x-other", component.data["controller"]
end

def test_extra_data_is_merged_onto_root
component = Folio::Console::Ui::AlertComponent.new(variant: :info,
data: { "x-progress-session-id-value" => "abc",
"x-progress-expected-value" => 3 })

assert_equal "abc", component.data["x-progress-session-id-value"]
assert_equal 3, component.data["x-progress-expected-value"]
assert_equal "f-c-ui-alert", component.data["controller"]
end

def test_extra_data_does_not_clobber_existing_autohide
component = Folio::Console::Ui::AlertComponent.new(variant: :info,
autohide: true,
data: { "x-progress-session-id-value" => "abc" })

assert_equal "true", component.data["f-c-ui-alert-autohide-value"]
assert_equal "abc", component.data["x-progress-session-id-value"]
end
end
30 changes: 30 additions & 0 deletions test/components/folio/console/ui/flash_component_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,34 @@ def test_blank
assert_selector(".f-c-ui-flash", visible: false)
assert_no_selector(".f-c-ui-flash .f-c-ui-alert")
end

def test_autohide_propagates_from_flash
component = Folio::Console::Ui::FlashComponent.new(flash: { "notice" => "foo", "autohide" => true })

assert_equal true, component.autohide
assert_equal({ "notice" => "foo" }, component.instance_variable_get(:@flash))
end

def test_autohide_off_when_not_set
component = Folio::Console::Ui::FlashComponent.new(flash: { "notice" => "foo" })

assert_equal false, component.autohide
end

def test_alert_stimulus_controllers_extracted_from_flash
component = Folio::Console::Ui::FlashComponent.new(flash: { "notice" => "foo",
"alert_stimulus_controllers" => ["x-progress"],
"alert_data" => { "x-progress-session-id-value" => "abc" } })

assert_equal ["x-progress"], component.alert_stimulus_controllers
assert_equal({ "x-progress-session-id-value" => "abc" }, component.alert_data)
assert_equal({ "notice" => "foo" }, component.instance_variable_get(:@flash))
end

def test_alert_stimulus_controllers_default_empty
component = Folio::Console::Ui::FlashComponent.new(flash: { "notice" => "foo" })

assert_equal [], component.alert_stimulus_controllers
assert_equal({}, component.alert_data)
end
end
29 changes: 29 additions & 0 deletions test/controllers/folio/console/api/file_controller_base_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,35 @@ class Folio::Console::Api::FileControllerBaseTest < Folio::Console::BaseControll
assert_includes reload_url, "page=2", "reload_url should preserve page"
end

if %w[image video].include?(klass.human_type)
test "#{klass} - index with by_file_name sorts newest first" do
created_at = Time.zone.parse("2026-04-29 14:00:00")
query = "cs279api#{klass.human_type}"
older = create(klass.model_name.singular,
site: @site,
file_name: query,
created_at: created_at - 1.hour)
lower_id = create(klass.model_name.singular,
site: @site,
file_name: "#{query}-lower",
created_at:)
higher_id = create(klass.model_name.singular,
site: @site,
file_name: "#{query}-higher",
created_at:)
expected_ids = [higher_id.id, lower_id.id, older.id]

get url_for([:console, :api, klass, format: :json]), params: { by_file_name: query }

actual_ids = response.parsed_body["data"]
.map { |record| record["id"].to_i }
.select { |id| expected_ids.include?(id) }

assert_response :success
assert_equal expected_ids, actual_ids
end
end

test "#{klass} - pagination preserves explicit request_path after picker upload refresh" do
create_list(klass.model_name.singular, Folio::Console::FileControllerBase::PAGY_ITEMS + 1)

Expand Down
Loading