feat: Adiciona field type social_link para redes sociais do usuário

Implementa o field type 'social_link' com seletor de rede e URL de
perfil, composto por:

- SocialLinkItem: field type com colunas 'network' (varchar 64) e
  'url' (varchar 2048), cardinalidade ilimitada
- SocialLinkWidget: widget com select de rede e input de URL
- SocialLinkFormatter: formatter que renderiza links com classe CSS
  por rede (social-link--{network}), target _blank e rel noopener
- config/optional: field.storage e field.field para user
- config/translations/pt-br: tradução do label e description
- hook_install e update_10002: configura form/view displays
- UserInfoBlock: expõe social_links via getSocialLinks()
- Template: adiciona seção de redes sociais e remove referências
  obsoletas a category e dept_code
- translations/site_users.pt-br.po: strings do novo field type

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-23 14:10:28 -03:00
parent 1dafd4a865
commit 8bab0515e1
11 changed files with 383 additions and 31 deletions

View File

@@ -0,0 +1,20 @@
langcode: en
status: true
dependencies:
config:
- field.storage.user.field_user_social_links
module:
- site_users
- user
id: user.user.field_user_social_links
field_name: field_user_social_links
entity_type: user
bundle: user
label: 'Social Links'
description: 'Social network profile links.'
required: false
translatable: false
default_value: {}
default_value_callback: ''
settings: {}
field_type: social_link

View File

@@ -0,0 +1,18 @@
langcode: en
status: true
dependencies:
module:
- site_users
- user
id: user.field_user_social_links
field_name: field_user_social_links
entity_type: user
type: social_link
settings: {}
module: site_users
locked: false
cardinality: -1
translatable: false
indexes: {}
persist_with_no_fields: false
custom_storage: false

View File

@@ -0,0 +1,2 @@
label: 'Redes Sociais'
description: 'Links de perfil em redes sociais.'

View File

@@ -67,6 +67,16 @@ function site_users_install() {
]); ]);
} }
// Campo Redes Sociais.
if (!$form_display->getComponent('field_user_social_links')) {
$form_display->setComponent('field_user_social_links', [
'type' => 'social_link_widget',
'weight' => 16,
'settings' => [],
'region' => 'content',
]);
}
// Campo Foto Padrão - oculto no form (será gerenciado via hook_form_alter). // Campo Foto Padrão - oculto no form (será gerenciado via hook_form_alter).
$form_display->removeComponent('field_user_default_photo'); $form_display->removeComponent('field_user_default_photo');
@@ -127,6 +137,17 @@ function site_users_install() {
]); ]);
} }
// Campo Redes Sociais.
if (!$view_display->getComponent('field_user_social_links')) {
$view_display->setComponent('field_user_social_links', [
'type' => 'social_link_formatter',
'weight' => 16,
'label' => 'above',
'settings' => [],
'region' => 'content',
]);
}
// Campo Foto Padrão. // Campo Foto Padrão.
if (!$view_display->getComponent('field_user_default_photo')) { if (!$view_display->getComponent('field_user_default_photo')) {
$view_display->setComponent('field_user_default_photo', [ $view_display->setComponent('field_user_default_photo', [
@@ -212,3 +233,57 @@ function site_users_update_10001() {
return t('Default photo field created successfully.'); return t('Default photo field created successfully.');
} }
/**
* Adds the field_user_social_links field for social network profile links.
*/
function site_users_update_10002() {
// Create field storage if it does not exist.
if (!FieldStorageConfig::loadByName('user', 'field_user_social_links')) {
FieldStorageConfig::create([
'field_name' => 'field_user_social_links',
'entity_type' => 'user',
'type' => 'social_link',
'module' => 'site_users',
'cardinality' => -1,
'translatable' => FALSE,
])->save();
}
// Create field instance if it does not exist.
if (!FieldConfig::loadByName('user', 'user', 'field_user_social_links')) {
FieldConfig::create([
'field_name' => 'field_user_social_links',
'entity_type' => 'user',
'bundle' => 'user',
'label' => 'Social Links',
'description' => 'Social network profile links.',
'required' => FALSE,
])->save();
}
// Add to form display.
$form_display = EntityFormDisplay::load('user.user.default');
if ($form_display && !$form_display->getComponent('field_user_social_links')) {
$form_display->setComponent('field_user_social_links', [
'type' => 'social_link_widget',
'weight' => 16,
'settings' => [],
'region' => 'content',
])->save();
}
// Add to view display.
$view_display = EntityViewDisplay::load('user.user.default');
if ($view_display && !$view_display->getComponent('field_user_social_links')) {
$view_display->setComponent('field_user_social_links', [
'type' => 'social_link_formatter',
'weight' => 16,
'label' => 'above',
'settings' => [],
'region' => 'content',
])->save();
}
return t('Social links field created successfully.');
}

View File

@@ -55,8 +55,7 @@ function site_users_entity_field_access($operation, FieldDefinitionInterface $fi
$profile_fields = [ $profile_fields = [
'field_user_name', 'field_user_name',
'field_user_phone', 'field_user_phone',
'field_user_social_links',
'field_user_bio', 'field_user_bio',
]; ];

View File

@@ -109,9 +109,8 @@ class UserInfoBlock extends BlockBase implements ContainerFactoryPluginInterface
'username' => $user->getDisplayName(), 'username' => $user->getDisplayName(),
'name' => $this->getFieldValue($user, 'field_user_name'), 'name' => $this->getFieldValue($user, 'field_user_name'),
'phone' => $this->getFieldValue($user, 'field_user_phone'), 'phone' => $this->getFieldValue($user, 'field_user_phone'),
'bio' => $this->getFieldValue($user, 'field_user_bio'), 'bio' => $this->getFieldValue($user, 'field_user_bio'),
'social_links' => $this->getSocialLinks($user),
'photo_url' => $default_photo_url, 'photo_url' => $default_photo_url,
'photo_alt' => $default_photo ? $default_photo->label() : '', 'photo_alt' => $default_photo ? $default_photo->label() : '',
]; ];
@@ -150,6 +149,34 @@ class UserInfoBlock extends BlockBase implements ContainerFactoryPluginInterface
return NULL; return NULL;
} }
/**
* Returns the social network links of a user.
*
* @param \Drupal\user\UserInterface $user
* The user entity.
*
* @return array
* Array of items with 'network' and 'url' keys.
*/
protected function getSocialLinks(UserInterface $user): array {
$links = [];
if (!$user->hasField('field_user_social_links') || $user->get('field_user_social_links')->isEmpty()) {
return $links;
}
foreach ($user->get('field_user_social_links') as $item) {
if (!$item->isEmpty()) {
$links[] = [
'network' => $item->network,
'url' => $item->url,
];
}
}
return $links;
}
/** /**
* Obtém o valor de um campo do usuário. * Obtém o valor de um campo do usuário.
* *

View File

@@ -0,0 +1,52 @@
<?php
namespace Drupal\site_users\Plugin\Field\FieldFormatter;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\FormatterBase;
use Drupal\Core\Url;
use Drupal\site_users\Plugin\Field\FieldType\SocialLinkItem;
/**
* Plugin implementation of the 'social_link_formatter' formatter.
*
* @FieldFormatter(
* id = "social_link_formatter",
* label = @Translation("Social link"),
* field_types = {
* "social_link"
* }
* )
*/
class SocialLinkFormatter extends FormatterBase {
/**
* {@inheritdoc}
*/
public function viewElements(FieldItemListInterface $items, $langcode): array {
$elements = [];
$networks = SocialLinkItem::getNetworks();
foreach ($items as $delta => $item) {
if ($item->isEmpty()) {
continue;
}
$network_label = $networks[$item->network] ?? $item->network;
$elements[$delta] = [
'#type' => 'link',
'#title' => $network_label,
'#url' => Url::fromUri($item->url),
'#attributes' => [
'class' => ['social-link', 'social-link--' . $item->network],
'target' => '_blank',
'rel' => 'noopener noreferrer',
],
];
}
return $elements;
}
}

View File

@@ -0,0 +1,84 @@
<?php
namespace Drupal\site_users\Plugin\Field\FieldType;
use Drupal\Core\Field\FieldItemBase;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\TypedData\DataDefinition;
/**
* Plugin implementation of the 'social_link' field type.
*
* @FieldType(
* id = "social_link",
* label = @Translation("Social link"),
* description = @Translation("Stores a social network type and profile URL."),
* default_widget = "social_link_widget",
* default_formatter = "social_link_formatter"
* )
*/
class SocialLinkItem extends FieldItemBase {
/**
* Returns the list of available social networks.
*
* @return array
* Associative array keyed by machine name, valued by label.
*/
public static function getNetworks(): array {
return [
'facebook' => 'Facebook',
'instagram' => 'Instagram',
'linkedin' => 'LinkedIn',
'twitter' => 'X (Twitter)',
'youtube' => 'YouTube',
'github' => 'GitHub',
'tiktok' => 'TikTok',
'telegram' => 'Telegram',
'whatsapp' => 'WhatsApp',
'pinterest' => 'Pinterest',
];
}
/**
* {@inheritdoc}
*/
public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition): array {
$properties['network'] = DataDefinition::create('string')
->setLabel(t('Network'))
->setRequired(TRUE);
$properties['url'] = DataDefinition::create('uri')
->setLabel(t('URL'))
->setRequired(TRUE);
return $properties;
}
/**
* {@inheritdoc}
*/
public static function schema(FieldStorageDefinitionInterface $field_definition): array {
return [
'columns' => [
'network' => [
'type' => 'varchar',
'length' => 64,
],
'url' => [
'type' => 'varchar',
'length' => 2048,
],
],
];
}
/**
* {@inheritdoc}
*/
public function isEmpty(): bool {
return empty($this->get('network')->getValue())
|| empty($this->get('url')->getValue());
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Drupal\site_users\Plugin\Field\FieldWidget;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\WidgetBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\site_users\Plugin\Field\FieldType\SocialLinkItem;
/**
* Plugin implementation of the 'social_link_widget' widget.
*
* @FieldWidget(
* id = "social_link_widget",
* label = @Translation("Social link"),
* field_types = {
* "social_link"
* }
* )
*/
class SocialLinkWidget extends WidgetBase {
/**
* {@inheritdoc}
*/
public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state): array {
$item = $items[$delta];
$element['network'] = [
'#type' => 'select',
'#title' => $this->t('Network'),
'#options' => ['' => $this->t('- Select -')] + SocialLinkItem::getNetworks(),
'#default_value' => $item->network ?? '',
];
$element['url'] = [
'#type' => 'url',
'#title' => $this->t('Profile URL'),
'#default_value' => $item->url ?? '',
'#maxlength' => 2048,
'#placeholder' => 'https://',
];
return $element;
}
}

View File

@@ -1,20 +1,19 @@
{# {#
/** /**
* @file * @file
* Template para o bloco de informações do usuário. * Template for the user information block.
* *
* Variáveis disponíveis: * Available variables:
* - user_info: Array com informações do usuário: * - user_info: Array with user information:
* - uid: ID do usuário * - uid: User ID
* - username: Nome de usuário (display name) * - username: Display name
* - name: Nome completo * - name: Full name
* - phone: Telefone * - phone: Phone number
* - category: Categoria * - bio: Biography
* - dept_code: Código do departamento * - social_links: Array of social links, each with 'network' and 'url' keys
* - bio: Biografia * - photo_url: Default photo URL
* - photo_url: URL da foto padrão * - photo_alt: Photo alternative text
* - photo_alt: Texto alternativo da foto * - user: User entity.
* - user: Entidade do usuário.
*/ */
#} #}
<div class="site-user-info-block"> <div class="site-user-info-block">
@@ -35,22 +34,9 @@
{{ user_info.name ?: user_info.username }} {{ user_info.name ?: user_info.username }}
</h2> </h2>
{% if user_info.category %}
<div class="site-user-info-block__category">
{{ user_info.category }}
</div>
{% endif %}
{% if user_info.dept_code %}
<div class="site-user-info-block__dept">
<span class="site-user-info-block__label">{{ 'Departamento:'|t }}</span>
{{ user_info.dept_code }}
</div>
{% endif %}
{% if user_info.phone %} {% if user_info.phone %}
<div class="site-user-info-block__phone"> <div class="site-user-info-block__phone">
<span class="site-user-info-block__label">{{ 'Telefone:'|t }}</span> <span class="site-user-info-block__label">{{ 'Phone:'|t }}</span>
<a href="tel:{{ user_info.phone }}">{{ user_info.phone }}</a> <a href="tel:{{ user_info.phone }}">{{ user_info.phone }}</a>
</div> </div>
{% endif %} {% endif %}
@@ -60,5 +46,18 @@
{{ user_info.bio }} {{ user_info.bio }}
</div> </div>
{% endif %} {% endif %}
{% if user_info.social_links %}
<div class="site-user-info-block__social-links">
{% for link in user_info.social_links %}
<a href="{{ link.url }}"
class="social-link social-link--{{ link.network }}"
target="_blank"
rel="noopener noreferrer">
{{ link.network }}
</a>
{% endfor %}
</div>
{% endif %}
</div> </div>
</div> </div>

View File

@@ -102,3 +102,32 @@ msgstr "Você pode adicionar no máximo @max fotos. Atualmente há @count fotos
# Install/update # Install/update
msgid "Default photo field created successfully." msgid "Default photo field created successfully."
msgstr "Campo de foto padrão criado com sucesso." msgstr "Campo de foto padrão criado com sucesso."
msgid "Social links field created successfully."
msgstr "Campo de redes sociais criado com sucesso."
# Social link field type
msgid "Social link"
msgstr "Link social"
msgid "Stores a social network type and profile URL."
msgstr "Armazena um tipo de rede social e URL de perfil."
msgid "Network"
msgstr "Rede"
msgid "- Select -"
msgstr "- Selecione -"
msgid "Profile URL"
msgstr "URL do perfil"
msgid "Social Links"
msgstr "Redes Sociais"
msgid "Social network profile links."
msgstr "Links de perfil em redes sociais."
# Template
msgid "Phone:"
msgstr "Telefone:"