Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
eb3d118
feat(tiptap): add locale support to has_folio_tiptap_content
mreq Jan 8, 2026
89b3c3f
feat(tiptap): add locale switching frontend and improve backend
mreq Jan 8, 2026
737168d
refactor(console/ui): convert flag cell to component
mreq Jan 8, 2026
5d2e949
fix(tiptap): fix idempotency check and test arguments
mreq Jan 8, 2026
57558ba
feat(pages): add tiptap_content to traco fields
mreq Jan 9, 2026
468bb3d
feat(tiptap_revisions): add attribute_name support for locale-specifi…
mreq Jan 9, 2026
8529e15
refactor(tiptap): unify attribute_name usage in frontend
mreq Jan 9, 2026
8358bae
feat(tiptap): persist selected attribute in cookie per record
mreq Jan 9, 2026
ab87861
fix(tiptap): layout css
mreq Jan 9, 2026
deb6801
fix(test): locale_switch_component_test
mreq Jan 9, 2026
00ccecb
chore(js): add locale_switch_component to console base js
mreq Jan 9, 2026
6d657f1
feat(tiptap): add attribute name support to word count component
mreq Jan 9, 2026
1b03eb5
feat(tiptap): style locale_switch_component
mreq Jan 9, 2026
4d9debe
fix(test): word_count_component_test
mreq Jan 9, 2026
091330b
chore(docs): tiptap locale support
mreq Jan 9, 2026
7b39cde
chore(changelog): add tiptap locale support entry
mreq Jan 9, 2026
a022ca6
Merge branch 'master' into petr/tiptap-multi-lang
mreq Jan 12, 2026
6f0981a
docs(tiptap): add model setup example with required warning
mreq Jan 12, 2026
f4dc354
fix(tiptap): add conditional check for folio attachments in update_ti…
mreq Jan 12, 2026
1f0f076
fix(tiptap): store scroll positions per locale
mreq Jan 13, 2026
c550ca4
feat(tiptap): persist locale selection for new records
mreq Jan 13, 2026
bfc6ca8
test(tiptap): split locale configuration tests into separate file
mreq Jan 13, 2026
d494c56
refactor(batch_service): remove unnecessary Redis reconnect delay con…
ornsteinfilip Jan 22, 2026
69f696a
chore(deps): update sidekiq and sidekiq-cron dependencies to latest v…
ornsteinfilip Jan 22, 2026
3c8348a
chore(deps): add connection_pool dependency to gemspec
ornsteinfilip Jan 22, 2026
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
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ This includes but is not limited to:
- **Models:** `rails generate model ModelName`
- **Controllers:** `rails generate controller ControllerName`
- **View Components:** `rails generate folio:component namespace/name`
- **Note:** For Folio components (in the `Folio::` namespace), use a leading slash: `rails generate folio:component /folio/console/ui/flag` generates `Folio::Console::Ui::FlagComponent`
- **Atoms:** `rails generate folio:atom namespace/atom_name`
- **Cells:** `rails generate folio:cell namespace/cell_name`
- **Console (admin) resources:** Check available Folio generators with `rails generate --help | grep folio`
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file.

## [Unreleased]

### Added

- **TipTap Locale Support**: Added locale support to Tiptap editor via `has_folio_tiptap_content(locales: [...])` option, enabling separate content fields per locale with locale switcher UI in console

## [7.1.2] - 2026-01-12

### Added
Expand Down
1 change: 1 addition & 0 deletions app/assets/javascripts/folio/console/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@
//= require folio/console/tiptap/overlay/form_component
//= require folio/console/tiptap/overlay_component
//= require folio/console/tiptap/simple_form_wrap/autosave_info_component
//= require folio/console/tiptap/simple_form_wrap/locale_switch_component
//= require folio/console/tiptap/simple_form_wrap/word_count_component
//= require folio/console/tiptap/simple_form_wrap_component
//= require folio/console/users/invite_and_copy/invite_and_copy
Expand Down
16 changes: 10 additions & 6 deletions app/assets/javascripts/folio/input/tiptap.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ window.Folio.Stimulus.register('f-input-tiptap', class extends window.Stimulus.C
newRecord: { type: Boolean, default: false },
placementType: String,
placementId: Number,
attributeName: String,
latestRevisionAt: String,
hasUnsavedChanges: { type: Boolean, default: false }
}
Expand Down Expand Up @@ -153,7 +154,7 @@ window.Folio.Stimulus.register('f-input-tiptap', class extends window.Stimulus.C
this.inputTarget.value = ''
}

this.dispatch('updateWordCount', { detail: { wordCount } })
this.dispatch('updateWordCount', { detail: { wordCount, attributeName: this.attributeNameValue } })

if (!this.ignoreValueChangesValue) {
this.inputTarget.dispatchEvent(new window.Event('change', { bubbles: true }))
Expand Down Expand Up @@ -200,7 +201,7 @@ window.Folio.Stimulus.register('f-input-tiptap', class extends window.Stimulus.C
words: value[valueKeys.word_count],
characters: value[valueKeys.character_count]
})
this.dispatch('updateWordCount', { detail: { wordCount } })
this.dispatch('updateWordCount', { detail: { wordCount, attributeName: this.attributeNameValue } })
}

const data = {
Expand Down Expand Up @@ -369,13 +370,15 @@ window.Folio.Stimulus.register('f-input-tiptap', class extends window.Stimulus.C
iframe: this.tiptapScrollTop
}

window.sessionStorage.setItem('f-input-tiptap-scroll', JSON.stringify({ at: Date.now(), scroll }))
const storageKey = `f-input-tiptap-scroll:${this.attributeNameValue}`
window.sessionStorage.setItem(storageKey, JSON.stringify({ at: Date.now(), scroll }))
}

restoreScrollPositions () {
if (this.typeValue !== 'block') return

const stored = window.sessionStorage.getItem('f-input-tiptap-scroll')
const storageKey = `f-input-tiptap-scroll:${this.attributeNameValue}`
const stored = window.sessionStorage.getItem(storageKey)

if (!stored) return

Expand All @@ -396,7 +399,7 @@ window.Folio.Stimulus.register('f-input-tiptap', class extends window.Stimulus.C
}
}

window.sessionStorage.removeItem('f-input-tiptap-scroll')
window.sessionStorage.removeItem(storageKey)
} catch (e) {
console.error('Failed to restore scroll positions:', e)
}
Expand All @@ -408,7 +411,8 @@ window.Folio.Stimulus.register('f-input-tiptap', class extends window.Stimulus.C

const data = {
tiptap_revision: {
content: this.latestContent
content: this.latestContent,
attribute_name: this.attributeNameValue || 'tiptap_content'
},
placement: {
type: this.placementTypeValue,
Expand Down
2 changes: 1 addition & 1 deletion app/cells/folio/console/atoms/locale_switch/show.slim
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@
class=active_class(locale)
data-locale=locale
]
== cell('folio/console/ui/flag', locale)
== render_view_component(Folio::Console::Ui::FlagComponent.new(locale:))
2 changes: 1 addition & 1 deletion app/cells/folio/console/catalogue_cell.rb
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ def published_dates
def locale_flag(locale_attr = :locale)
attribute(locale_attr, compact: true, aligned: true, skip_desktop_header: true) do
if record.send(locale_attr)
cell("folio/console/ui/flag", record.send(locale_attr))
render_view_component(Folio::Console::Ui::FlagComponent.new(locale: record.send(locale_attr)))
end
end
end
Expand Down
9 changes: 0 additions & 9 deletions app/cells/folio/console/ui/flag_cell.rb

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ window.Folio.Stimulus.register('f-c-tiptap-simple-form-wrap-autosave-info', clas
static values = {
placementType: String,
placementId: Number,
attributeName: String,
deleteUrl: String
}

Expand All @@ -16,7 +17,8 @@ window.Folio.Stimulus.register('f-c-tiptap-simple-form-wrap-autosave-info', clas
const data = {
placement: {
type: this.placementTypeValue,
id: this.placementIdValue
id: this.placementIdValue,
attribute_name: this.attributeNameValue || 'tiptap_content'
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
# frozen_string_literal: true

class Folio::Console::Tiptap::SimpleFormWrap::AutosaveInfoComponent < Folio::Console::ApplicationComponent
attr_reader :object
attr_reader :object, :attribute_name

def initialize(object:)
def initialize(object:, attribute_name: nil)
@object = object
@attribute_name = attribute_name || "tiptap_content"
end

def data
stimulus_controller("f-c-tiptap-simple-form-wrap-autosave-info",
values: {
placement_type: object.class.base_class.name,
placement_id: object.id,
attribute_name: attribute_name,
delete_url: controller.delete_revision_console_api_tiptap_revisions_path,
})
end
Expand All @@ -21,12 +23,12 @@ def render?
end

def has_unsaved_changes?
object.has_tiptap_revision?
object.has_tiptap_revision?(attribute_name: attribute_name)
end

private
def latest_revision_info
latest_revision = object.latest_tiptap_revision(user: Folio::Current.user)
l(latest_revision.created_at, format: :short)
latest_revision = object.latest_tiptap_revision(user: Folio::Current.user, attribute_name: attribute_name)
l(latest_revision.created_at, format: :short) if latest_revision
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
= t('.title')

.f-c-tiptap-simple-form-wrap-autosave-info__middle
= latest_revision_info
- if latest_revision_info.present?
= latest_revision_info

.f-c-tiptap-simple-form-wrap-autosave-info__bottom
.f-c-tiptap-simple-form-wrap-autosave-info__buttons
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
window.Folio.Stimulus.register('f-c-tiptap-simple-form-wrap-locale-switch', class extends window.Stimulus.Controller {
static targets = ['button']

onAttributeClick (e) {
const button = e.currentTarget
const attributeName = button.dataset.attributeName

// Update active state on buttons
this.buttonTargets.forEach(btn => {
btn.classList.toggle('f-c-tiptap-simple-form-wrap-locale-switch__btn--active', btn === button)
})

// Dispatch event to parent SimpleFormWrap component
this.dispatch('attributeChanged', { detail: { attributeName } })
}
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# frozen_string_literal: true

class Folio::Console::Tiptap::SimpleFormWrap::LocaleSwitchComponent < Folio::Console::ApplicationComponent
def initialize(attribute_names:, locales:, selected_attribute: nil)
@attribute_names = attribute_names
@locales = locales
@selected_attribute = selected_attribute
end

def controller_data
stimulus_controller("f-c-tiptap-simple-form-wrap-locale-switch")
end

def attribute_names_with_locales
@attribute_names.zip(@locales).map do |attribute_name, locale|
{
attribute_name: attribute_name,
locale: locale,
active: attribute_name == @selected_attribute
}
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
.f-c-tiptap-simple-form-wrap-locale-switch
display: flex
align-items: center
gap: 4px
margin-left: auto

&__btn
+unbutton
width: 30px
height: 24px
display: flex
align-items: center
justify-content: center
background-color: transparent
transition: $transition-base

&:hover,
&--active
background-color: $medium-gray
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.f-c-tiptap-simple-form-wrap-locale-switch data=controller_data
- attribute_names_with_locales.each do |item|
button.f-c-tiptap-simple-form-wrap-locale-switch__btn[
type="button"
class=(item[:active] ? "f-c-tiptap-simple-form-wrap-locale-switch__btn--active" : nil)
data-attribute-name=item[:attribute_name]
data=stimulus_target("button")
data=stimulus_action(click: "onAttributeClick")
]
= render(Folio::Console::Ui::FlagComponent.new(locale: item[:locale]))
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
window.Folio.Stimulus.register('f-c-tiptap-simple-form-wrap-word-count', class extends window.Stimulus.Controller {
static targets = ['wordsCount', 'charactersCount']

static values = {
attributeName: String
}

updateWordCount (e) {
if (!e || !e.detail || !e.detail.wordCount) return
if (!e?.detail?.wordCount) return
this.updateCounts(e.detail.wordCount)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
# frozen_string_literal: true

class Folio::Console::Tiptap::SimpleFormWrap::WordCountComponent < Folio::Console::ApplicationComponent
def initialize(data: nil)
def initialize(attribute_name:, data: nil)
@attribute_name = attribute_name
@data = data || {}
end

def controller_data
stimulus_merge(@data,
stimulus_controller("f-c-tiptap-simple-form-wrap-word-count",
values: {
attribute_name: @attribute_name,
},
action: {
"f-c-tiptap-simple-form-wrap:updateWordCount" => "updateWordCount",
}))
Expand Down
65 changes: 61 additions & 4 deletions app/components/folio/console/tiptap/simple_form_wrap_component.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
window.Folio.Stimulus.register('f-c-tiptap-simple-form-wrap', class extends window.Stimulus.Controller {
static targets = ['scrollIco', 'scroller', 'wordCount', 'fields']
static targets = ['scrollIco', 'scroller', 'wordCount', 'fields', 'attributeWrap']

static values = {
scrolledToBottom: Boolean
scrolledToBottom: Boolean,
cookieKey: String,
newRecordCookieKey: String
}

connect () {
this.onScroll = window.Folio.throttle(this.onScrollRaw.bind(this))

// Initialize current attribute name from visible attribute wrap
const visibleWrap = this.attributeWrapTargets.find(wrap => !wrap.hidden)
if (visibleWrap) {
this.currentAttributeName = visibleWrap.dataset.attributeName
}
}

disconnect () {
Expand Down Expand Up @@ -37,10 +45,15 @@ window.Folio.Stimulus.register('f-c-tiptap-simple-form-wrap', class extends wind
}

updateWordCount (e) {
const wordCount = e.detail && e.detail.wordCount
const { wordCount, attributeName } = e.detail || {}
if (!wordCount) return
if (!this.hasWordCountTarget) return
this.wordCountTarget.dispatchEvent(new CustomEvent('f-c-tiptap-simple-form-wrap:updateWordCount', { detail: { wordCount } }))
this.wordCountTargets.forEach(target => {
const wrap = target.closest('.f-c-tiptap-simple-form-wrap__attribute-wrap')
if (wrap?.dataset.attributeName === attributeName) {
target.dispatchEvent(new CustomEvent('f-c-tiptap-simple-form-wrap:updateWordCount', { detail: { wordCount, attributeName } }))
}
})
}

onContinueUnsavedChanges (e) {
Expand Down Expand Up @@ -114,4 +127,48 @@ window.Folio.Stimulus.register('f-c-tiptap-simple-form-wrap', class extends wind
const visible = activeLink.classList.contains('f-c-file-placements-multi-picker-fields-nav-link')
this.element.classList.toggle('f-c-tiptap-simple-form-wrap--multi-picker-visible', visible)
}

onAttributeChanged (e) {
const { attributeName } = e.detail

// Track current attribute name for new records
this.currentAttributeName = attributeName

// Save to cookie if cookie key is available
if (this.cookieKeyValue) {
const inOneDay = new Date(new Date().getTime() + 24 * 60 * 60 * 1000)
window.Cookies.set(this.cookieKeyValue, attributeName, { expires: inOneDay, path: '' })
}

// Hide all attribute wraps (editors and autosave components)
this.attributeWrapTargets.forEach(wrap => {
wrap.hidden = wrap.dataset.attributeName !== attributeName
})

// Trigger resize on the visible iframe
const visibleWrap = this.attributeWrapTargets.find(wrap => wrap.dataset.attributeName === attributeName)
if (visibleWrap) {
const iframe = visibleWrap.querySelector('.f-input-tiptap__iframe')
if (iframe && iframe.contentWindow) {
// Dispatch resize event to iframe
iframe.contentWindow.postMessage({
type: 'f-input-tiptap:window-resize',
windowWidth: window.innerWidth
}, window.origin)

// Also trigger a resize event on the window
setTimeout(() => {
window.dispatchEvent(new Event('resize'))
}, 100)
}
}
}

onFormSubmit (e) {
// Set short-lived generic cookie for new records on form submit
if (this.newRecordCookieKeyValue && this.currentAttributeName) {
const inFifteenSeconds = new Date(new Date().getTime() + 15 * 1000)
window.Cookies.set(this.newRecordCookieKeyValue, this.currentAttributeName, { expires: inFifteenSeconds, path: '' })
}
}
})
Loading