Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
f592af0
feat(products): migrate Edit Product page to Inertia
sahitya-chandra Jan 26, 2026
bca67ce
refactor: Exclude `update` action from `fetch_product_and_enforce_acc…
sahitya-chandra Jan 26, 2026
122c1b8
Merge remote-tracking branch 'origin/main' into migrate-edit-product-…
sahitya-chandra Jan 26, 2026
7744dc2
Refactor link policy and update product edit routes
sahitya-chandra Jan 27, 2026
0278c37
refactor: remove legacy update logic and comments from product edit c…
sahitya-chandra Jan 27, 2026
5f5ca59
refactor(products): use Inertia for publish/unpublish flow
sahitya-chandra Jan 27, 2026
fb05707
test(links): add specs for Inertia publish/unpublish flow
sahitya-chandra Jan 27, 2026
268e84f
revert: remove unrelated config changes
sahitya-chandra Jan 27, 2026
fd82986
fix(react_on_rails): wrap params in ActionController::Parameters for SSR
sahitya-chandra Jan 27, 2026
8c0d5ef
refactor: migrate product edit functionality to Inertia and update ro…
sahitya-chandra Jan 28, 2026
d0af2ea
Merge branch 'main' into migrate-edit-product-to-inertia
sahitya-chandra Jan 29, 2026
f2a14e2
Add authorization and collaborator access tests for product editing c…
sahitya-chandra Jan 29, 2026
08556c4
Merge origin/main into migrate-edit-product-to-inertia
sahitya-chandra Jan 29, 2026
00e8d0c
refactor: Update product edit routes and controller actions for Inert…
sahitya-chandra Jan 29, 2026
db73e2e
Merge origin/main into migrate-edit-product-to-inertia
sahitya-chandra Jan 29, 2026
b8c976d
small ref
sahitya-chandra Jan 29, 2026
41aa258
Merge branch 'main' into migrate-edit-product-to-inertia
sahitya-chandra Jan 29, 2026
a75d12a
Merge branch 'main' into migrate-edit-product-to-inertia
sahitya-chandra Jan 30, 2026
ffe1c9b
Refactor product presenters to use layout_props and introduce specifi…
sahitya-chandra Jan 30, 2026
50325b6
Merge branch 'main' into migrate-edit-product-to-inertia
sahitya-chandra Jan 30, 2026
8a01440
Merge branch 'main' into migrate-edit-product-to-inertia
sahitya-chandra Jan 31, 2026
b0f2de3
Merge branch 'main' into migrate-edit-product-to-inertia
sahitya-chandra Jan 31, 2026
d29201e
Merge branch 'main' into migrate-edit-product-to-inertia
sahitya-chandra Feb 1, 2026
ef4f490
Merge branch 'main' into migrate-edit-product-to-inertia
sahitya-chandra Feb 2, 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
311 changes: 51 additions & 260 deletions app/controllers/links_controller.rb

Large diffs are not rendered by default.

53 changes: 53 additions & 0 deletions app/controllers/products/edit/base_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# frozen_string_literal: true

module Products
module Edit
class BaseController < Sellers::BaseController
before_action :fetch_product
before_action :authorize_product
before_action :set_product_edit_title

layout "inertia"

protected
def check_offer_codes_validity
invalid_currency_offer_codes = @product.product_and_universal_offer_codes.reject do |offer_code|
offer_code.is_currency_valid?(@product)
end.map(&:code)
invalid_amount_offer_codes = @product.product_and_universal_offer_codes.reject { _1.is_amount_valid?(@product) }.map(&:code)

all_invalid_offer_codes = (invalid_currency_offer_codes + invalid_amount_offer_codes).uniq

if all_invalid_offer_codes.any?
has_currency_issues = invalid_currency_offer_codes.any?
has_amount_issues = invalid_amount_offer_codes.any?

if has_currency_issues && has_amount_issues
issue_description = "#{"has".pluralize(all_invalid_offer_codes.count)} currency mismatches or would discount this product below #{@product.min_price_formatted}"
elsif has_currency_issues
issue_description = "#{"has".pluralize(all_invalid_offer_codes.count)} currency #{"mismatch".pluralize(all_invalid_offer_codes.count)} with this product"
else
issue_description = "#{all_invalid_offer_codes.count > 1 ? "discount" : "discounts"} this product below #{@product.min_price_formatted}, but not to #{MoneyFormatter.format(0, @product.price_currency_type.to_sym, no_cents_if_whole: true, symbol: true)}"
end

flash[:warning] = "The following offer #{"code".pluralize(all_invalid_offer_codes.count)} #{issue_description}: #{all_invalid_offer_codes.join(", ")}. Please update #{all_invalid_offer_codes.length > 1 ? "them or they" : "it or it"} will not work at checkout."
end
end

private
def set_product_edit_title
set_meta_tag(title: @product.name)
end

def fetch_product
product_id = params[:product_id] || params[:id]
@product = Link.fetch_leniently(product_id, user: current_seller) || Link.fetch_leniently(product_id)
raise(ActiveRecord::RecordNotFound) if @product.nil? || @product.archived? || @product.deleted_at.present?
end

def authorize_product
authorize @product
end
end
end
end
82 changes: 82 additions & 0 deletions app/controllers/products/edit/content_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# frozen_string_literal: true

module Products
module Edit
class ContentController < BaseController
def edit
render inertia: "Products/Edit/Content", props: Products::Edit::ContentTabPresenter.new(product: @product, pundit_user:).props
end

def update
authorize @product

should_publish = params[:publish].present? && [email protected]?
should_unpublish = params[:unpublish].present? && @product.published?

if should_unpublish
@product.unpublish!
check_offer_codes_validity
return redirect_back fallback_location: edit_product_content_path(@product.unique_permalink), notice: "Unpublished!", status: :see_other
end

ActiveRecord::Base.transaction do
update_content_attributes
@product.publish! if should_publish
end

check_offer_codes_validity

if should_publish
redirect_to edit_product_share_path(@product.unique_permalink), notice: "Published!", status: :see_other
elsif params[:redirect_to].present?
redirect_to params[:redirect_to], notice: "Changes saved!", status: :see_other
else
redirect_back fallback_location: edit_product_content_path(@product.unique_permalink), notice: "Changes saved!", status: :see_other
end
rescue ActiveRecord::RecordNotSaved, ActiveRecord::RecordInvalid, Link::LinkInvalid => e
error_message = @product.errors.full_messages.first || e.message
redirect_to edit_product_content_path(@product.unique_permalink), alert: error_message, status: :see_other
end

private
def update_content_attributes
@product.assign_attributes(product_permitted_params.except(:files, :variants, :custom_domain, :rich_content))
SaveFilesService.perform(@product, product_permitted_params, rich_content_params)
update_rich_content
@product.save!
@product.generate_product_files_archives!
end

def update_rich_content
rich_content = product_permitted_params[:rich_content] || []
existing_rich_contents = @product.alive_rich_contents.to_a
rich_contents_to_keep = []

rich_content.each.with_index do |product_rich_content, index|
rc = existing_rich_contents.find { |c| c.external_id === product_rich_content[:id] } || @product.alive_rich_contents.build
description = product_rich_content[:description].to_h[:content]
product_rich_content[:description] = SaveContentUpsellsService.new(
seller: @product.user,
content: description,
old_content: rc.description || []
).from_rich_content
rc.update!(title: product_rich_content[:title].presence, description: product_rich_content[:description].presence || [], position: index)
rich_contents_to_keep << rc
end

(existing_rich_contents - rich_contents_to_keep).each(&:mark_deleted!)
end

def rich_content_params
rich_content = product_permitted_params[:rich_content] || []
rich_content_params = [*rich_content]
product_permitted_params[:variants]&.each { rich_content_params.push(*_1[:rich_content]) }
rich_content_params.flat_map { _1[:description] = _1.dig(:description, :content) }
end

def product_permitted_params
params.require(:product).permit(policy(@product).content_tab_permitted_attributes)
end
end
end
end
190 changes: 190 additions & 0 deletions app/controllers/products/edit/product_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
# frozen_string_literal: true

module Products
module Edit
class ProductController < BaseController
def edit
return redirect_to bundle_path(@product.external_id) if @product.is_bundle?

render inertia: "Products/Edit/Product", props: Products::Edit::ProductTabPresenter.new(product: @product, pundit_user:).props
end

def update
authorize @product

should_unpublish = params[:unpublish].present? && @product.published?
was_published = @product.published?

if should_unpublish
@product.unpublish!
check_offer_codes_validity
return redirect_back fallback_location: edit_product_product_path(@product.unique_permalink), notice: "Unpublished!", status: :see_other
end

ActiveRecord::Base.transaction do
update_product_attributes
end

check_offer_codes_validity

if params[:redirect_to].present?
redirect_to params[:redirect_to], notice: "Changes saved!", status: :see_other
elsif was_published
redirect_back fallback_location: edit_product_product_path(@product.unique_permalink), notice: "Changes saved!", status: :see_other
else
redirect_to edit_product_content_path(@product.unique_permalink), notice: "Changes saved!", status: :see_other
end
rescue ActiveRecord::RecordNotSaved, ActiveRecord::RecordInvalid, Link::LinkInvalid => e
error_message = if @product.errors.details[:custom_fields].present?
"You must add titles to all of your inputs"
else
@product.errors.full_messages.first || e.message
end
redirect_to edit_product_product_path(@product.unique_permalink), alert: error_message, status: :see_other
end

private

def update_product_attributes
@product.assign_attributes(product_permitted_params.except(
:description,
:cancellation_discount,
:custom_button_text_option,
:custom_summary,
:custom_attributes,
:covers,
:integrations,
:variants,
:section_ids,
:availabilities,
:call_limitation_info,
:installment_plan,
:default_offer_code_id,
:public_files,
:shipping_destinations,
:community_chat_enabled,
:custom_domain,
:file_attributes,
:refund_policy
))

if @product.native_type === ::Link::NATIVE_TYPE_COFFEE && product_permitted_params[:variants].present?
@product.suggested_price_cents = product_permitted_params[:variants].map { _1[:price_difference_cents] }.max
end

@product.description = SaveContentUpsellsService.new(
seller: @product.user,
content: product_permitted_params[:description],
old_content: @product.description_was
).from_html if product_permitted_params[:description].present?

@product.save_custom_button_text_option(product_permitted_params[:custom_button_text_option]) unless product_permitted_params[:custom_button_text_option].nil?
@product.save_custom_summary(product_permitted_params[:custom_summary]) unless product_permitted_params[:custom_summary].nil?
@product.save_custom_attributes((product_permitted_params[:custom_attributes] || []).filter { _1[:name].present? || _1[:description].present? })
@product.reorder_previews((product_permitted_params[:covers] || []).map.with_index.to_h)
@product.show_in_sections!(product_permitted_params[:section_ids] || [])
@product.save_shipping_destinations!(product_permitted_params[:shipping_destinations] || []) if @product.is_physical

if Feature.active?(:cancellation_discounts, @product.user) && (product_permitted_params[:cancellation_discount].present? || @product.cancellation_discount_offer_code.present?)
Product::SaveCancellationDiscountService.new(@product, product_permitted_params[:cancellation_discount]).perform
end

Product::SaveIntegrationsService.perform(@product, product_permitted_params[:integrations])
update_variants
update_availabilities
update_call_limitation_info
update_installment_plan
update_default_offer_code

Product::SavePostPurchaseCustomFieldsService.new(@product).perform

@product.description = SavePublicFilesService.new(
resource: @product,
files_params: product_permitted_params[:public_files],
content: @product.description
).process if product_permitted_params[:public_files].present?

toggle_community_chat!(product_permitted_params[:community_chat_enabled])
@product.save!
end

def update_variants
variant_category = @product.variant_categories_alive.first
variants = product_permitted_params[:variants] || []
if variants.any? || @product.is_tiered_membership?
variant_category_params = variant_category.present? ?
{ id: variant_category.external_id, name: variant_category.title } :
{ name: @product.is_tiered_membership? ? "Tier" : "Version" }

Product::VariantsUpdaterService.new(
product: @product,
variants_params: [{ **variant_category_params, options: variants }],
).perform
elsif variant_category.present?
Product::VariantsUpdaterService.new(
product: @product,
variants_params: [{ id: variant_category.external_id, options: nil }]
).perform
end
end

def update_availabilities
return unless @product.native_type == ::Link::NATIVE_TYPE_CALL
existing_availabilities = @product.call_availabilities
availabilities_to_keep = []
(product_permitted_params[:availabilities] || []).each do |availability_params|
availability = existing_availabilities.find { _1.id == availability_params[:id] } || @product.call_availabilities.build
availability.update!(availability_params.except(:id))
availabilities_to_keep << availability
end
(existing_availabilities - availabilities_to_keep).each(&:destroy!)
end

def update_call_limitation_info
return unless @product.native_type == ::Link::NATIVE_TYPE_CALL
@product.call_limitation_info.update!(product_permitted_params[:call_limitation_info]) if product_permitted_params[:call_limitation_info].present?
end

def update_installment_plan
return unless @product.eligible_for_installment_plans?
if @product.installment_plan && product_permitted_params[:installment_plan].present?
@product.installment_plan.assign_attributes(product_permitted_params[:installment_plan])
return unless @product.installment_plan.changed?
end

@product.installment_plan&.destroy_if_no_payment_options!
@product.reset_installment_plan
if product_permitted_params[:installment_plan].present?
@product.create_installment_plan!(product_permitted_params[:installment_plan])
end
end

def update_default_offer_code
default_offer_code_id = product_permitted_params[:default_offer_code_id]
return @product.default_offer_code = nil if default_offer_code_id.blank?

offer_code = @product.user.offer_codes.alive.find_by_external_id!(default_offer_code_id)
raise ::Link::LinkInvalid, "Offer code cannot be expired" if offer_code.inactive?
raise ::Link::LinkInvalid, "Offer code must be associated with this product or be universal" unless valid_for_product?(offer_code)
@product.default_offer_code = offer_code
rescue ActiveRecord::RecordNotFound
raise ::Link::LinkInvalid, "Invalid offer code"
end

def valid_for_product?(offer_code)
offer_code.universal? || @product.offer_codes.where(id: offer_code.id).exists?
end

def toggle_community_chat!(enabled)
return if enabled.nil?
return unless Feature.active?(:communities, current_seller)
return if [::Link::NATIVE_TYPE_COFFEE, ::Link::NATIVE_TYPE_BUNDLE].include?(@product.native_type)
@product.toggle_community_chat!(enabled)
end

def product_permitted_params
params.require(:product).permit(policy(@product).product_tab_permitted_attributes)
end
end
end
end
37 changes: 37 additions & 0 deletions app/controllers/products/edit/receipt_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# frozen_string_literal: true

module Products
module Edit
class ReceiptController < BaseController
def edit
render inertia: "Products/Edit/Receipt", props: Products::Edit::ReceiptTabPresenter.new(product: @product, pundit_user:).props
end

def update
begin
ActiveRecord::Base.transaction do
update_receipt_attributes
end
rescue ActiveRecord::RecordNotSaved, ActiveRecord::RecordInvalid, Link::LinkInvalid => e
error_message = @product.errors.full_messages.first || e.message
return redirect_to edit_product_receipt_path(@product.unique_permalink), alert: error_message, status: :see_other
end

check_offer_codes_validity

redirect_to edit_product_receipt_path(@product.unique_permalink), notice: "Changes saved!", status: :see_other
end

private

def update_receipt_attributes
@product.assign_attributes(product_permitted_params.except(:custom_domain))
@product.save!
end

def product_permitted_params
params.require(:product).permit(policy(@product).receipt_tab_permitted_attributes)
end
end
end
end
Loading