From 0b6ae26f3b4c3c8278e569fa6644a544e996c25d Mon Sep 17 00:00:00 2001 From: erdgeist Date: Mon, 13 Jul 2026 16:36:38 +0200 Subject: Add per-node translation management: list, edit, destroy, compare Replaces the old locale-switch-and-edit-the-same-screen workflow, which conflated presentation locale with content locale and let an editor silently drift into editing the wrong language with no persistent signal that anything had changed. Non-default-locale content now has its own explicit routes and screens, never sharing a route param with the ambient chrome locale. - PageTranslationsController: index/show/edit/update/destroy, scoped to Page.non_default_locales; update handles first-time creation too, so there's no separate new/create step. - Reads go through the actual PageTranslation row (Page#translation_summary), never through the Globalize fallback-bearing accessor -- fallback is correct for public rendering but wrong for editing, where a missing translation needs to look empty, not borrowed from another locale. - Translations ride on the page's own draft/head cycle; no independent publish state. - nodes#show gains a Translations section (per-locale Lock+Edit / Create+Lock, Destroy, a link into the read-only Compare view) and a locale indicator on its own default-locale content; nodes#edit, nodes#update, and nodes#autosave are pinned to the default locale via Globalize.with_locale regardless of the ambient route locale. - nodes#show no longer double-loads the node or calls wipe_draft! on every view (see previous commit for why that's now safe). - .preview_link_row is renamed .aligned_action_row now that it has a second real consumer. --- .../page_translations_controller_test.rb | 78 ++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 test/controllers/page_translations_controller_test.rb (limited to 'test/controllers/page_translations_controller_test.rb') diff --git a/test/controllers/page_translations_controller_test.rb b/test/controllers/page_translations_controller_test.rb new file mode 100644 index 0000000..d84ac45 --- /dev/null +++ b/test/controllers/page_translations_controller_test.rb @@ -0,0 +1,78 @@ +require 'test_helper' + +class PageTranslationsControllerTest < ActionController::TestCase + test "index lists the default locale's existing translation and flags a missing one" do + login_as :quentin + node = Node.root.children.create!(:slug => "translations_index_test") + node.publish_draft! + + get :index, params: { :node_id => node.id } + + assert_response :success + assert_equal [:en], assigns(:translations).map { |t| t[:locale] } + assert_equal false, assigns(:translations).first[:exists] + end + + test "update creates a first-time translation on a fresh draft, leaving head untouched" do + login_as :quentin + node = Node.root.children.create!(:slug => "translations_create_test") + Globalize.with_locale(:de) { node.draft.update!(:title => "Deutscher Titel") } + node.publish_draft! + + patch :update, params: { :node_id => node.id, :translation_locale => "en", :page => { :title => "English Title" } } + + node.reload + assert_not_nil node.draft + assert_includes node.draft.translated_locales, :en + assert_not_includes node.head.translated_locales, :en + end + + test "no route exists for the default locale under the translations resource" do + login_as :quentin + node = Node.root.children.create!(:slug => "translations_route_constraint_test") + + assert_raises(ActionController::UrlGenerationError) do + patch :update, params: { :node_id => node.id, :translation_locale => "de", :page => { :title => "x" } } + end + end + + test "update with Save + Unlock + Exit unlocks the node and redirects to nodes#show" do + login_as :quentin + node = Node.root.children.create!(:slug => "translations_exit_test") + + patch :update, params: { :node_id => node.id, :translation_locale => "en", :page => { :title => "x" }, :commit => "Save + Unlock + Exit" } + + assert_nil node.reload.lock_owner + assert_redirected_to node_path(node) + end + + test "update saves the translation onto the draft" do + login_as :quentin + node = Node.root.children.create!(:slug => "translations_update_test") + Globalize.with_locale(:en) { node.draft.update!(:title => "Original") } + + patch :update, params: { :node_id => node.id, :translation_locale => "en", :page => { :title => "Revised" } } + + assert_equal "Revised", Globalize.with_locale(:en) { node.draft.reload.title } + end + + test "destroy refuses to remove the only remaining translation" do + login_as :quentin + node = Node.root.children.create!(:slug => "translations_destroy_last_test") + Globalize.with_locale(:en) { node.draft.update!(:title => "Only translation") } + node.draft.translations.where(:locale => :de).delete_all + + delete :destroy, params: { :node_id => node.id, :translation_locale => "en" } + + assert_equal "Can't remove the only remaining translation.", flash[:error] + end + + test "destroy is a safe no-op, not a false success, when the translation doesn't exist" do + login_as :quentin + node = Node.root.children.create!(:slug => "translations_destroy_missing_test") + + delete :destroy, params: { :node_id => node.id, :translation_locale => "en" } + + assert_match(/No EN translation exists/, flash[:error]) + end +end -- cgit v1.3 From aa14587b052a0aa7884aaf27183981975168ab14 Mon Sep 17 00:00:00 2001 From: erdgeist Date: Mon, 13 Jul 2026 17:09:31 +0200 Subject: Wire real autosave into translation editing The shared TinyMCE setup initializes cccms.setup_autosave() on any page with a textarea.with_editor, unconditionally starting a 7-second interval that submits to the form's data-autosave-url -- which the translation edit form never set, so the interval PUT the current page URL itself and 404'd on a nonexistent route. Fixed by actually giving it something to talk to, rather than suppressing it: a real autosave endpoint, and update now goes through Node#autosave!/#save_draft! -- the same pipeline the primary editor uses, fixed earlier this session for exactly this kind of cross-locale carryover. This was the autosave-buffer parity already flagged as due after the proof of concept; the shared JS just forced the timing. --- app/controllers/page_translations_controller.rb | 17 ++++++++++++++--- app/views/page_translations/edit.html.erb | 3 ++- config/routes.rb | 7 +++++-- test/controllers/page_translations_controller_test.rb | 19 +++++++++++++++++++ 4 files changed, 40 insertions(+), 6 deletions(-) (limited to 'test/controllers/page_translations_controller_test.rb') diff --git a/app/controllers/page_translations_controller.rb b/app/controllers/page_translations_controller.rb index 47f01f7..9446cd2 100644 --- a/app/controllers/page_translations_controller.rb +++ b/app/controllers/page_translations_controller.rb @@ -3,7 +3,7 @@ class PageTranslationsController < ApplicationController before_action :login_required before_action :find_node - before_action :find_locale, :only => [:show, :edit, :update, :destroy] + before_action :find_locale, :only => [:show, :edit, :update, :autosave, :destroy] def index page = @node.draft || @node.head @@ -26,8 +26,8 @@ class PageTranslationsController < ApplicationController end def update - draft = ensure_editable_draft - Globalize.with_locale(@locale) { draft.update!(translation_params) } + Globalize.with_locale(@locale) { @node.autosave!(translation_params, current_user) } + @node.save_draft!(current_user) flash[:notice] = "#{@locale.upcase} translation saved. Publish the draft to make it live." if params[:commit] == "Save + Unlock + Exit" @@ -41,6 +41,17 @@ class PageTranslationsController < ApplicationController redirect_to node_path(@node) end + def autosave + Globalize.with_locale(@locale) { @node.autosave!(translation_params, current_user) } + head :ok + rescue LockedByAnotherUser => e + render plain: e.message, status: :locked + rescue ActiveRecord::RecordInvalid => e + render plain: e.message, status: :unprocessable_entity + rescue StandardError => e + render plain: "Autosave failed", status: :internal_server_error + end + def destroy base = @node.draft || @node.head unless base && base.translated_locales.include?(@locale) diff --git a/app/views/page_translations/edit.html.erb b/app/views/page_translations/edit.html.erb index 7371a42..89b594e 100644 --- a/app/views/page_translations/edit.html.erb +++ b/app/views/page_translations/edit.html.erb @@ -18,7 +18,8 @@
- <%= form_with url: node_translation_path(@node, @locale), method: :patch, local: true, id: "translation_edit_form" do |f| %> + <%= form_with url: node_translation_path(@node, @locale), method: :patch, local: true, id: "translation_edit_form", + data: { autosave_url: autosave_node_translation_path(@node, @locale), show_url: node_path(@node) } do |f| %>
Title
diff --git a/config/routes.rb b/config/routes.rb index 38a497d..826bb18 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -58,8 +58,11 @@ Cccms::Application.routes.draw do resources :translations, controller: 'page_translations', param: :translation_locale, constraints: { translation_locale: /en/ }, - only: [:index, :show, :edit, :update, :destroy] - + only: [:index, :show, :edit, :update, :destroy] do + member do + put :autosave + end + end resources :related_assets, only: [:create, :destroy, :update] do collection do diff --git a/test/controllers/page_translations_controller_test.rb b/test/controllers/page_translations_controller_test.rb index d84ac45..8c899fd 100644 --- a/test/controllers/page_translations_controller_test.rb +++ b/test/controllers/page_translations_controller_test.rb @@ -18,6 +18,7 @@ class PageTranslationsControllerTest < ActionController::TestCase node = Node.root.children.create!(:slug => "translations_create_test") Globalize.with_locale(:de) { node.draft.update!(:title => "Deutscher Titel") } node.publish_draft! + node.lock_for_editing!(users(:quentin)) patch :update, params: { :node_id => node.id, :translation_locale => "en", :page => { :title => "English Title" } } @@ -39,6 +40,7 @@ class PageTranslationsControllerTest < ActionController::TestCase test "update with Save + Unlock + Exit unlocks the node and redirects to nodes#show" do login_as :quentin node = Node.root.children.create!(:slug => "translations_exit_test") + node.lock_for_editing!(users(:quentin)) patch :update, params: { :node_id => node.id, :translation_locale => "en", :page => { :title => "x" }, :commit => "Save + Unlock + Exit" } @@ -50,6 +52,7 @@ class PageTranslationsControllerTest < ActionController::TestCase login_as :quentin node = Node.root.children.create!(:slug => "translations_update_test") Globalize.with_locale(:en) { node.draft.update!(:title => "Original") } + node.lock_for_editing!(users(:quentin)) patch :update, params: { :node_id => node.id, :translation_locale => "en", :page => { :title => "Revised" } } @@ -75,4 +78,20 @@ class PageTranslationsControllerTest < ActionController::TestCase assert_match(/No EN translation exists/, flash[:error]) end + + test "autosave writes the translation without creating a new revision or touching the draft" do + login_as :quentin + node = Node.root.children.create!(:slug => "translations_autosave_test") + node.publish_draft! + node.lock_for_editing!(users(:quentin)) + page_count_before = node.pages.count + + put :autosave, params: { :node_id => node.id, :translation_locale => "en", :page => { :title => "in progress" } } + + assert_response :success + node.reload + assert_not_nil node.autosave + assert_equal page_count_before, node.pages.count + assert_equal "in progress", Globalize.with_locale(:en) { node.autosave.title } + end end -- cgit v1.3