feat: add show author option to portal settings and update related views (#225)

* feat: add show author option to portal settings and update related views

* fix: update portal reference to use local variable for show author condition

* feat: enhance show_author handling in portal config and add related tests
This commit is contained in:
Gabriel Jablonski 2026-02-26 14:32:52 -03:00 committed by GitHub
parent c026ee2fc8
commit 21007bd20b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 92 additions and 4 deletions

View File

@ -23,7 +23,7 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
def update def update
ActiveRecord::Base.transaction do ActiveRecord::Base.transaction do
@portal.update!(portal_params.merge(live_chat_widget_params)) if params[:portal].present? @portal.update!(merged_portal_params.merge(live_chat_widget_params)) if params[:portal].present?
# @portal.custom_domain = parsed_custom_domain # @portal.custom_domain = parsed_custom_domain
process_attached_logo if params[:blob_id].present? process_attached_logo if params[:blob_id].present?
rescue ActiveRecord::RecordInvalid => e rescue ActiveRecord::RecordInvalid => e
@ -79,10 +79,20 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
def portal_params def portal_params
params.require(:portal).permit( params.require(:portal).permit(
:id, :color, :custom_domain, :header_text, :homepage_link, :id, :color, :custom_domain, :header_text, :homepage_link,
:name, :page_title, :slug, :archived, { config: [:default_locale, { allowed_locales: [] }] } :name, :page_title, :slug, :archived, { config: [:default_locale, :show_author, { allowed_locales: [] }] }
) )
end end
def merged_portal_params
update_params = portal_params.to_h
if update_params.key?('config')
base_config = @portal.config.is_a?(Hash) ? @portal.config : {}
incoming_config = update_params['config']
update_params['config'] = incoming_config.is_a?(Hash) ? base_config.merge(incoming_config) : base_config
end
update_params
end
def live_chat_widget_params def live_chat_widget_params
permitted_params = params.permit(:inbox_id) permitted_params = params.permit(:inbox_id)
return {} unless permitted_params.key?(:inbox_id) return {} unless permitted_params.key?(:inbox_id)

View File

@ -15,6 +15,7 @@ import Input from 'dashboard/components-next/input/Input.vue';
import Avatar from 'dashboard/components-next/avatar/Avatar.vue'; import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
import ComboBox from 'dashboard/components-next/combobox/ComboBox.vue'; import ComboBox from 'dashboard/components-next/combobox/ComboBox.vue';
import ColorPicker from 'dashboard/components-next/colorpicker/ColorPicker.vue'; import ColorPicker from 'dashboard/components-next/colorpicker/ColorPicker.vue';
import Switch from 'dashboard/components-next/switch/Switch.vue';
const props = defineProps({ const props = defineProps({
activePortal: { activePortal: {
@ -45,6 +46,7 @@ const state = reactive({
liveChatWidgetInboxId: '', liveChatWidgetInboxId: '',
logoUrl: '', logoUrl: '',
avatarBlobId: '', avatarBlobId: '',
showAuthor: true,
}); });
const originalState = reactive({ ...state }); const originalState = reactive({ ...state });
@ -117,6 +119,7 @@ watch(
homePageLink: newVal.homepage_link, homePageLink: newVal.homepage_link,
slug: newVal.slug, slug: newVal.slug,
liveChatWidgetInboxId: newVal.inbox?.id || '', liveChatWidgetInboxId: newVal.inbox?.id || '',
showAuthor: newVal.config?.show_author !== false,
}); });
if (newVal.logo) { if (newVal.logo) {
const { const {
@ -149,6 +152,7 @@ const handleUpdatePortal = () => {
homepage_link: state.homePageLink, homepage_link: state.homePageLink,
blob_id: state.avatarBlobId, blob_id: state.avatarBlobId,
inbox_id: state.liveChatWidgetInboxId, inbox_id: state.liveChatWidgetInboxId,
config: { show_author: state.showAuthor },
}; };
emit('updatePortal', portal); emit('updatePortal', portal);
}; };
@ -335,6 +339,21 @@ const handleAvatarDelete = () => {
<ColorPicker v-model="state.widgetColor" /> <ColorPicker v-model="state.widgetColor" />
</div> </div>
</div> </div>
<div
class="grid items-start justify-between w-full gap-2 grid-cols-[200px,1fr]"
>
<label
class="text-sm font-medium whitespace-nowrap py-2.5 text-n-slate-12"
>
{{ t('HELP_CENTER.PORTAL_SETTINGS.FORM.SHOW_AUTHOR.LABEL') }}
</label>
<div class="flex flex-col gap-1 py-2.5">
<Switch v-model="state.showAuthor" />
<span class="text-xs text-n-slate-11">
{{ t('HELP_CENTER.PORTAL_SETTINGS.FORM.SHOW_AUTHOR.HELP_TEXT') }}
</span>
</div>
</div>
<div class="flex justify-end w-full gap-2"> <div class="flex justify-end w-full gap-2">
<Button <Button
:label="t('HELP_CENTER.PORTAL_SETTINGS.FORM.SAVE_CHANGES')" :label="t('HELP_CENTER.PORTAL_SETTINGS.FORM.SAVE_CHANGES')"

View File

@ -747,6 +747,10 @@
"BRAND_COLOR": { "BRAND_COLOR": {
"LABEL": "Brand color" "LABEL": "Brand color"
}, },
"SHOW_AUTHOR": {
"LABEL": "Show article authors",
"HELP_TEXT": "Display author information on category pages"
},
"SAVE_CHANGES": "Save changes" "SAVE_CHANGES": "Save changes"
}, },
"CONFIGURATION_FORM": { "CONFIGURATION_FORM": {

View File

@ -747,6 +747,10 @@
"BRAND_COLOR": { "BRAND_COLOR": {
"LABEL": "Cor da Marca" "LABEL": "Cor da Marca"
}, },
"SHOW_AUTHOR": {
"LABEL": "Mostrar autores dos artigos",
"HELP_TEXT": "Exibir informações de autoria nas páginas de categorias"
},
"SAVE_CHANGES": "Salvar Alterações" "SAVE_CHANGES": "Salvar Alterações"
}, },
"CONFIGURATION_FORM": { "CONFIGURATION_FORM": {

View File

@ -44,7 +44,11 @@ class Portal < ApplicationRecord
scope :active, -> { where(archived: false) } scope :active, -> { where(archived: false) }
CONFIG_JSON_KEYS = %w[allowed_locales default_locale website_token].freeze CONFIG_JSON_KEYS = %w[allowed_locales default_locale website_token show_author].freeze
def show_author?
!ActiveModel::Type::Boolean.new.cast(config['show_author']).equal?(false)
end
def file_base_data def file_base_data
{ {

View File

@ -15,6 +15,7 @@ json.config do
json.partial! 'api/v1/models/portal_config', formats: [:json], locale: locale, portal: portal json.partial! 'api/v1/models/portal_config', formats: [:json], locale: locale, portal: portal
end end
end end
json.show_author portal.show_author?
end end
if portal.channel_web_widget if portal.channel_web_widget

View File

@ -46,8 +46,10 @@
</div> </div>
<div class="flex justify-between flex-row items-center <%= !@is_plain_layout_enabled && 'px-2' %>"> <div class="flex justify-between flex-row items-center <%= !@is_plain_layout_enabled && 'px-2' %>">
<div class="flex flex-row items-center gap-1"> <div class="flex flex-row items-center gap-1">
<% if portal.show_author? %>
<%= render "public/api/v1/portals/authors", category: category, show_expanded: false %> <%= render "public/api/v1/portals/authors", category: category, show_expanded: false %>
<span class="text-slate-600 dark:text-slate-400">•</span> <span class="text-slate-600 dark:text-slate-400">•</span>
<% end %>
<span class="text-sm font-medium text-slate-600 dark:text-slate-400"><%= render 'public/api/v1/portals/article_count', article_count: category.articles.published.order(position: :asc).size %></span> <span class="text-sm font-medium text-slate-600 dark:text-slate-400"><%= render 'public/api/v1/portals/article_count', article_count: category.articles.published.order(position: :asc).size %></span>
</div> </div>
<div> <div>

View File

@ -22,8 +22,10 @@
<% end %> <% end %>
</div> </div>
<div class="flex flex-row items-center gap-1"> <div class="flex flex-row items-center gap-1">
<% if portal.show_author? %>
<%= render "public/api/v1/portals/authors", category: category, show_expanded: true %> <%= render "public/api/v1/portals/authors", category: category, show_expanded: true %>
<span class="text-slate-600 dark:text-slate-400">•</span> <span class="text-slate-600 dark:text-slate-400">•</span>
<% end %>
<span class="flex items-center text-base text-slate-600 dark:text-slate-400 font-medium"><%= render 'public/api/v1/portals/article_count', article_count: category.articles.published.size %></span> <span class="flex items-center text-base text-slate-600 dark:text-slate-400 font-medium"><%= render 'public/api/v1/portals/article_count', article_count: category.articles.published.size %></span>
</div> </div>
</div> </div>

View File

@ -131,7 +131,49 @@ RSpec.describe 'Api::V1::Accounts::Portals', type: :request do
json_response = response.parsed_body json_response = response.parsed_body
expect(json_response['name']).to eql(portal_params[:portal][:name]) expect(json_response['name']).to eql(portal_params[:portal][:name])
expect(json_response['config']).to eql({ 'allowed_locales' => [{ 'articles_count' => 0, 'categories_count' => 0, 'code' => 'en' }, expect(json_response['config']).to eql({ 'allowed_locales' => [{ 'articles_count' => 0, 'categories_count' => 0, 'code' => 'en' },
{ 'articles_count' => 0, 'categories_count' => 0, 'code' => 'es' }] }) { 'articles_count' => 0, 'categories_count' => 0, 'code' => 'es' }],
'show_author' => true })
end
it 'persists show_author as false when explicitly set' do
put "/api/v1/accounts/#{account.id}/portals/#{portal.slug}",
params: { portal: { config: { show_author: false } } },
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['config']['show_author']).to be(false)
portal.reload
expect(portal.show_author?).to be(false)
end
it 'preserves show_author when updating other portal fields' do
portal.update!(config: portal.config.merge('show_author' => false))
put "/api/v1/accounts/#{account.id}/portals/#{portal.slug}",
params: { portal: { name: 'renamed_portal' } },
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
portal.reload
expect(portal.show_author?).to be(false)
expect(portal.name).to eql('renamed_portal')
end
it 'preserves show_author when updating only allowed_locales' do
portal.update!(config: portal.config.merge('show_author' => false))
put "/api/v1/accounts/#{account.id}/portals/#{portal.slug}",
params: { portal: { config: { allowed_locales: %w[en fr] } } },
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
portal.reload
expect(portal.show_author?).to be(false)
end end
it 'archive portal' do it 'archive portal' do