feat(panel-admin): profile tab on UserResource #256#293
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds a Filament UserResource with a tabbed UserForm (account and profile sections), list/create/edit pages (ListUsers, CreateUser, EditUser). EditUser loads the tenant-scoped Profile, maps social_links to/from a repeater, upserts profile via UpsertProfile, toggles availability via ToggleAvailability, updates user name/email, and shows success notifications. UserResource table columns and filters are configured. PanelAdminServiceProvider registers the resource. A Pest feature test suite verifies EditUser behaviors. Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app-modules/panel-admin/src/Filament/Resources/Users/Pages/EditUser.php`:
- Around line 49-57: The code coerces an empty social links repeater to null,
preventing UpsertProfileDTO/UpsertProfile from persisting a cleared list; in the
UpsertProfileDTO creation (the $dto assignment using UpsertProfileDTO::fromArray
and the 'social_links' key) stop converting [] to null — pass $socialLinks
directly (i.e. 'social_links' => $socialLinks) so an explicit empty array will
be persisted as “no links” instead of being ignored.
- Around line 91-103: The helper methods socialLinksToRepeater and
repeaterToSocialLinks lack precise array value-type annotations, causing phpstan
iterable value-type errors; add explicit docblock types: for
socialLinksToRepeater accept ?array<string,string> and return
array<int,array{platform:string,handle:string}> (i.e. `@param`
?array<string,string> $socialLinks and `@return`
array<int,array{platform:string,handle:string}>), and for repeaterToSocialLinks
accept array<int,array{platform:string,handle:string}> and return
array<string,string> (i.e. `@param`
array<int,array{platform:string,handle:string}> $repeaterData and `@return`
array<string,string>), leaving runtime signatures unchanged but satisfying
static analysis for the functions socialLinksToRepeater and
repeaterToSocialLinks.
- Around line 42-77: handleRecordUpdate currently updates only the profile and
availability and ignores user account fields from $data, so edits to name/email
are lost; update handleRecordUpdate to extract the account data from $data (e.g.
$data['account'] or top-level keys used in the form), set them on the $record
(the User model) and persist before returning (use $record->fill(...) and
$record->save() or $record->update(...)), while keeping the existing calls to
UpsertProfile::handle($profile, $dto) and ToggleAvailability::handle($profile,
$available, $startAvailability).
- Around line 107-114: The loop in EditUser.php that builds $links from
$repeaterData can silently overwrite entries when the same platform appears
twice; before assigning $links[$key] = (string) $handle check if $key already
exists and, if so, raise a validation error (e.g. throw
Illuminate\Validation\ValidationException or
ValidationException::withMessages([...]) for the social links field) or
otherwise mark the record invalid so the user gets feedback; reference the loop
variables $repeaterData, $platform, $handle, $links and the SocialPlatform enum
to locate and update the logic.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository: he4rt/coderabbit/.coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 448c6372-355b-4fb3-81ef-ab5fb4a2771a
📒 Files selected for processing (7)
app-modules/panel-admin/src/Filament/Resources/Users/Pages/CreateUser.phpapp-modules/panel-admin/src/Filament/Resources/Users/Pages/EditUser.phpapp-modules/panel-admin/src/Filament/Resources/Users/Pages/ListUsers.phpapp-modules/panel-admin/src/Filament/Resources/Users/Schemas/UserForm.phpapp-modules/panel-admin/src/Filament/Resources/Users/UserResource.phpapp-modules/panel-admin/src/PanelAdminServiceProvider.phpapp-modules/panel-admin/tests/Feature/Users/UserResourceTest.php
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app-modules/panel-admin/src/Filament/Resources/Users/Pages/EditUser.php`:
- Around line 78-81: The PHPStan errors are caused by using the generic Model
type for $record while accessing User-specific properties (name/email) in
EditUser::mutateFormDataUsing; fix by importing App\Models\User and narrowing
the type: either change the method param to User $record or add a docblock like
`@param` User $record (or add an inline assertion /** `@var` User $record */ or
assert($record instanceof User) before using $record) so accesses to
$record->name and $record->email are recognized as valid.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository YAML (base), Central YAML (inherited)
Review profile: CHILL
Plan: Pro
Run ID: f7470b8d-8533-49f0-974e-70b3f0caba71
📒 Files selected for processing (2)
app-modules/panel-admin/src/Filament/Resources/Users/Pages/EditUser.phpapp-modules/panel-admin/src/Filament/Resources/Users/UserResource.php
🚧 Files skipped from review as they are similar to previous changes (1)
- app-modules/panel-admin/src/Filament/Resources/Users/UserResource.php
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app-modules/panel-admin/src/Filament/Resources/Users/UserResource.php`:
- Around line 71-73: The "Criado este mês" filter currently only checks month
and may match other years; update the Filter::make('created_at')->query callback
to constrain by both month and year by adding a whereYear('created_at',
now()->year) alongside the existing whereMonth call so the Builder query checks
the current month AND the current year.
- Around line 45-63: The UserResource is referencing related fields via
TextColumn::make('profile.*') but the User model lacks a profile() relationship
and the relation isn't being eager-loaded, so add a profile() relation method to
the User model (returning a hasOne to the Profile model) and update UserResource
to eager-load('profile') in the query provider (or
getEloquentQuery/getTableQuery override) so
TextColumn::make('profile.headline'), 'profile.seniority_level',
'profile.years_experience', and 'profile.available_for_proposals' resolve
correctly.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository YAML (base), Central YAML (inherited)
Review profile: CHILL
Plan: Pro
Run ID: 904f892c-ffd1-48ee-a6ad-a6778dbddf0a
📒 Files selected for processing (1)
app-modules/panel-admin/src/Filament/Resources/Users/UserResource.php
| TextColumn::make('profile.headline') | ||
| ->label('Headline') | ||
| ->default('—') | ||
| ->searchable(), | ||
|
|
||
| TextColumn::make('profile.seniority_level') | ||
| ->label('Seniority') | ||
| ->badge() | ||
| ->default('—'), | ||
|
|
||
| TextColumn::make('profile.years_experience') | ||
| ->label('Exp.') | ||
| ->formatStateUsing(fn ($state) => $state ? $state.' anos' : '—'), | ||
|
|
||
| TextColumn::make('profile.available_for_proposals') | ||
| ->label('Disponível') | ||
| ->badge() | ||
| ->formatStateUsing(fn ($state) => $state ? 'Sim' : 'Não') | ||
| ->color(fn ($state) => $state ? 'success' : 'gray'), |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Check for profile relationship definition on User model
# Search for profile relationship method in User model
ast-grep --pattern $'class User $$$ {
$$$
profile($$$) {
$$$
}
$$$
}'
# Alternative: search for relationship definition with rg
rg -nP --type=php -A5 'function profile\(' app-modules/identity/Repository: he4rt/heartdevs.com
Length of output: 45
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) Locate the User model file(s)
echo "== User model candidates =="
fd -t f 'User.php' app-modules src . | head -n 50 || true
# Broader: search for class User definitions
echo
echo "== class User { occurrences =="
rg -n --type=php 'class\s+User\b' . | head -n 50 || true
# 2) Find any relationship method named profile() across repo
echo
echo "== function profile( occurrences =="
rg -n --type=php 'function\s+profile\s*\(' . | head -n 50 || true
# 3) Inspect UserResource.php around table() method
echo
echo "== UserResource.php: table() method area =="
FILE="app-modules/panel-admin/src/Filament/Resources/Users/UserResource.php"
if [ -f "$FILE" ]; then
# print ~120 lines around table() definition
rg -n --type=php 'function\s+table\s*\(' "$FILE"
# show from table() to columns/closing (best-effort)
start=$(rg -n --type=php 'function\s+table\s*\(' "$FILE" | head -n 1 | cut -d: -f1)
if [ -n "${start:-}" ]; then
from=$((start-20))
to=$((start+180))
sed -n "${from},${to}p" "$FILE" | nl -ba | sed -n '1,220p'
fi
else
echo "Missing: $FILE"
fiRepository: he4rt/heartdevs.com
Length of output: 1879
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE_USER="app-modules/identity/src/User/Models/User.php"
FILE_RESOURCE="app-modules/panel-admin/src/Filament/Resources/Users/UserResource.php"
echo "== profile mentions in User model =="
if [ -f "$FILE_USER" ]; then
rg -n --type=php 'profile' "$FILE_USER" || true
echo
echo "== User model excerpt around relationship-like methods =="
# show first ~220 lines to include relationship methods; cap output
sed -n '1,220p' "$FILE_USER" | cat -n
fi
echo
echo "== relationship methods in identity models mentioning profile =="
fd -t f 'Models/*.php' app-modules/identity/src | head -n 50 >/dev/null 2>&1 || true
rg -n --type=php 'function\s+[A-Za-z0-9_]*profile[A-Za-z0-9_]*\s*\(' app-modules/identity/src || true
rg -n --type=php '->profile' app-modules/identity/src || true
rg -n --type=php 'with\(\s*[\"\x27]profile[\"\x27]\s*\)' . || true
echo
echo "== Inspect UserResource::table() =="
if [ -f "$FILE_RESOURCE" ]; then
# find where table() starts
start=$(rg -n --type=php 'function\s+table\s*\(' "$FILE_RESOURCE" | head -n 1 | cut -d: -f1)
if [ -n "${start:-}" ]; then
from=$((start-30))
to=$((start+220))
sed -n "${from},${to}p" "$FILE_RESOURCE" | cat -n
fi
fi
echo
echo "== Search for any eager loading in UserResource.php =="
rg -n --type=php 'with\(|eager|modifyQueryUsing|withCount|load\(' "$FILE_RESOURCE" || trueRepository: he4rt/heartdevs.com
Length of output: 9180
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Any profile() methods in app-modules (PHP) =="
rg -n --type=php 'function\s+profile\s*\(' app-modules . || true
echo
echo "== Any getProfile accessors in app-modules =="
rg -n --type=php '(function\s+getProfile\s*\(|\bgetProfile\b|->profile\b)' app-modules . || true
echo
echo "== Search for profile fields used in Filament columns =="
for f in "headline" "seniority_level" "years_experience" "available_for_proposals"; do
echo "-- $f --"
rg -n --type=php "$f" app-modules/identity app-modules/panel-admin . | head -n 50 || true
done
echo
echo "== Inspect HasAddress trait for possible profile accessor =="
HAS_ADDRESS_FILE="$(fd -t f 'HasAddress.php' . | head -n 5 | tr '\n' ' ' || true)"
if [ -z "${HAS_ADDRESS_FILE:-}" ]; then
echo "HasAddress.php not found via fd"
else
echo "Found: $HAS_ADDRESS_FILE"
for f in $HAS_ADDRESS_FILE; do
echo
echo "---- $f (profile mentions) ----"
rg -n --type=php 'profile' "$f" || true
echo
echo "---- $f (excerpt) ----"
sed -n '1,220p' "$f" | cat -n
done
fi
echo
echo "== Filament columns - confirm UserResource doesn't eager load =="
FILE_RESOURCE="app-modules/panel-admin/src/Filament/Resources/Users/UserResource.php"
if [ -f "$FILE_RESOURCE" ]; then
sed -n '1,120p' "$FILE_RESOURCE" | cat -n
rg -n --type=php 'modifyQueryUsing|with\(|eager|load\(' "$FILE_RESOURCE" || true
fiRepository: he4rt/heartdevs.com
Length of output: 29345
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Inspect Profile model (relationships) =="
FILE_PROFILE="app-modules/profile/src/Models/Profile.php"
if [ -f "$FILE_PROFILE" ]; then
sed -n '1,220p' "$FILE_PROFILE" | cat -n
else
echo "Missing $FILE_PROFILE"
fi
echo
echo "== Search for Profile::class usage in identity/user models =="
rg -n --type=php 'Profile::class' app-modules/identity/src || true
echo
echo "== Search for relationships to user_profiles / headline/seniority fields (hints of inverse relation) =="
rg -n --type=php 'user_profiles|UserProfile|available_for_proposals|years_experience|seniority_level' app-modules/identity/src || true
echo
echo "== Inspect UserResourceTest expectations for profile.* columns =="
FILE_TEST="app-modules/panel-admin/tests/Feature/Users/UserResourceTest.php"
if [ -f "$FILE_TEST" ]; then
sed -n '1,220p' "$FILE_TEST" | cat -n
fi
echo
echo "== Inspect EditUser::getProfile (how profile is loaded) =="
FILE_EDIT="app-modules/panel-admin/src/Filament/Resources/Users/Pages/EditUser.php"
if [ -f "$FILE_EDIT" ]; then
sed -n '1,220p' "$FILE_EDIT" | cat -n
fi
echo
echo "== Broader: any method named profile* or accessors in User model traits =="
# show traits used by User model
sed -n '1,140p' "app-modules/identity/src/User/Models/User.php" | cat -n | rg -n 'use\s+' || true
# search for profile methods in any trait used by User
rg -n --type=php 'function\s+profile\s*\(' app-modules app/ || true
rg -n --type=php 'getProfileAttribute' app-modules app/ || trueRepository: he4rt/heartdevs.com
Length of output: 15193
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Inspect InteractsWithTenants trait =="
FILE_TENANT="app-modules/identity/src/Tenant/Concerns/InteractsWithTenants.php"
if [ -f "$FILE_TENANT" ]; then
sed -n '1,220p' "$FILE_TENANT" | cat -n
echo
rg -n --type=php 'profile' "$FILE_TENANT" || true
else
echo "Missing $FILE_TENANT"
fi
echo
echo "== Inspect ListUsers page =="
FILE_LIST="app-modules/panel-admin/src/Filament/Resources/Users/Pages/ListUsers.php"
if [ -f "$FILE_LIST" ]; then
sed -n '1,260p' "$FILE_LIST" | cat -n
echo
rg -n --type=php 'with\(|modifyQueryUsing|eager|profile' "$FILE_LIST" || true
else
echo "Missing $FILE_LIST"
fi
echo
echo "== Search for profile accessors/relations across repo =="
rg -n --type=php 'getProfileAttribute\s*\(' . || true
rg -n --type=php 'function\s+profile\s*\(' . || true
rg -n --type=php '\bprofile\s*\(\)\s*:' . || true
rg -n --type=php '->profile\b' app-modules/identity app-modules/panel-admin . || true
echo
echo "== Search for with(\"profile\") or with('profile') usage =="
rg -n --type=php 'with\(\s*[\"'\'']profile[\"'\'']\s*\)' . || trueRepository: he4rt/heartdevs.com
Length of output: 2806
🌐 Web query:
Filament TextColumn make('relation.field') eager load N+1 dot notation behavior
💡 Result:
In Filament, using dot notation (e.g., TextColumn::make('relation.field')) automatically triggers eager loading for the specified relationship to avoid the N+1 query problem [1][2]. Filament analyzes the column name and detects the relationship chain, ensuring the related records are fetched efficiently [1][2]. However, if you are accessing relationship data through custom methods—such as within description() or closures like state()—Filament may not automatically detect the need for eager loading, which can result in N+1 issues [3][4]. To resolve or prevent N+1 issues in these scenarios: 1. Use modifyQueryUsing: For table columns, you can force eager loading by using the modifyQueryUsing() method on the table definition [3]: $table->modifyQueryUsing(fn (Builder $query) => $query->with('relation.nestedRelation')) 2. Eager Load at the Query Level: If you find specific parts of your application are still triggering N+1 queries, ensure that the underlying Eloquent query is prepared with the necessary relationships using standard Laravel eager loading techniques before the data reaches the Filament components [5]. 3. Pivot Relationships: When dealing with pivot table data (e.g., pivot.relation.field), standard eager loading also applies, but ensure the relationships are correctly defined in your pivot model [6]. If you are encountering N+1 issues despite using dot notation, it is typically because the data access is occurring outside of the standard column resolution path (e.g., inside a custom state() or description() closure) [3][4]. In such cases, manually defining the eager load in modifyQueryUsing is the recommended solution [3].
Citations:
- 1: https://filamentphp.com/docs/4.x/tables/overview
- 2: https://filamentphp.com/docs/4.x/tables/columns
- 3: How to eager load a relationship used in a Table `description()`? filamentphp/filament#12034
- 4: Eager load relation on table filamentphp/filament#11840
- 5: Fix nested Repeater Eager Loading filamentphp/filament#9802
- 6: N+1 with multiple many to one relationship on single pivot table filamentphp/filament#5806
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE_USER="app-modules/identity/src/User/Models/User.php"
echo "== User.php line count =="
wc -l "$FILE_USER"
echo
echo "== Last 80 lines of User.php =="
tail -n 80 "$FILE_USER" | cat -nRepository: he4rt/heartdevs.com
Length of output: 2637
Fix profile.* columns by adding the missing User::profile() relationship (and then eager-load it).
app-modules/panel-admin/src/Filament/Resources/Users/UserResource.php renders TextColumn::make('profile.*'), but app-modules/identity/src/User/Models/User.php defines no profile() relationship (Profile model only has user()/tenant()), so those columns can’t be resolved correctly.
⚡ Proposed fix (eager-load once the relationship exists)
public static function table(Table $table): Table
{
return $table
+ ->modifyQueryUsing(fn (Builder $query) => $query->with('profile'))
->columns([🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app-modules/panel-admin/src/Filament/Resources/Users/UserResource.php` around
lines 45 - 63, The UserResource is referencing related fields via
TextColumn::make('profile.*') but the User model lacks a profile() relationship
and the relation isn't being eager-loaded, so add a profile() relation method to
the User model (returning a hasOne to the Profile model) and update UserResource
to eager-load('profile') in the query provider (or
getEloquentQuery/getTableQuery override) so
TextColumn::make('profile.headline'), 'profile.seniority_level',
'profile.years_experience', and 'profile.available_for_proposals' resolve
correctly.
| Filter::make('created_at') | ||
| ->label('Criado este mês') | ||
| ->query(fn (Builder $query) => $query->whereMonth('created_at', now()->month)), |
There was a problem hiding this comment.
Add year constraint to the month filter.
The filter is labeled "Criado este mês" (Created this month) but only checks the month, not the year. This means in June 2026 it will match users created in June of any year (2025, 2024, etc.), not just June 2026.
📅 Proposed fix to check both month and year
Filter::make('created_at')
->label('Criado este mês')
- ->query(fn (Builder $query) => $query->whereMonth('created_at', now()->month)),
+ ->query(fn (Builder $query) => $query
+ ->whereMonth('created_at', now()->month)
+ ->whereYear('created_at', now()->year)),📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| Filter::make('created_at') | |
| ->label('Criado este mês') | |
| ->query(fn (Builder $query) => $query->whereMonth('created_at', now()->month)), | |
| Filter::make('created_at') | |
| ->label('Criado este mês') | |
| ->query(fn (Builder $query) => $query | |
| ->whereMonth('created_at', now()->month) | |
| ->whereYear('created_at', now()->year)), |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app-modules/panel-admin/src/Filament/Resources/Users/UserResource.php` around
lines 71 - 73, The "Criado este mês" filter currently only checks month and may
match other years; update the Filter::make('created_at')->query callback to
constrain by both month and year by adding a whereYear('created_at',
now()->year) alongside the existing whereMonth call so the Builder query checks
the current month AND the current year.
790e1c0 to
4aa9c9b
Compare
Closes #256
Contexto
Parte da PRD #250 — Módulo Profile Fase 1.
O admin precisa visualizar e editar o perfil de qualquer membro para fins de moderação. Em vez de criar um ProfileResource separado, foi adicionada uma tab "Profile" dentro do UserResource existente.
O que foi feito
UserResourceno Admin panel com páginasListUsers,CreateUsereEditUserUserFormcom as seções:UpsertProfileeToggleAvailabilityfirstOrCreateUserResourceregistrado noPanelAdminServiceProviderTestes
admin sees profile tab on user resourceprofile tab loads member dataadmin can edit member biovalidates bio max lengthtoggle availability shows start availability fieldChecklist
Description
Adds a "Profile" tab to the Admin UserResource so admins can view/edit tenant-scoped member profiles (personal, professional, social links, availability). Implements List/Create/Edit user pages, a tabbed UserForm schema with conditional availability, and save handlers that call UpsertProfile and ToggleAvailability, creating missing profiles on-the-fly.
References
#250— Módulo Profile Fase 1#256— feat(panel-admin): profile tab on UserResourceDependencies & Requirements
Contributor Summary
Changes Summary