-
Notifications
You must be signed in to change notification settings - Fork 91
Improvements #72
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improvements #72
Conversation
…in import preview
Track unique new company names during import preview processing. Shows count of companies that will be created when importing People/Opportunities with company references. - Add newCompanies property to ImportSessionData - Accumulate unique new company names across chunks - Display new companies count in preview stats area - Remove per-row match badges (simplify UI)
…ning - Fix bug: enrichRowWithCompanyMatch was reading $row['id'] (person/opportunity ID) instead of $row['company_id'] (actual company ID) - Add hasMappingWarnings() to detect multiple warning conditions - Add hasCompanyNameWithoutId() to detect company_name without company_id mapping - Add getMappingWarningsHtml() to generate dynamic modal content - Extend existing warning modal to show both unique identifier and company warnings
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR introduces significant improvements to the import wizard functionality, including intelligent column mapping, session management, and cleanup mechanisms. The changes focus on enhancing the user experience during data imports and improving system reliability.
Key changes:
- Added data type inference for unmapped CSV columns with confidence scoring
- Implemented consolidated session tracking with heartbeat monitoring
- Enhanced column matching with plural/singular variants support
- Removed polymorphic entity attachment trait from Task and Note importers
- Added scheduled cleanup command for orphaned import sessions
- Upgraded phone number custom field type with migration
Reviewed changes
Copilot reviewed 37 out of 39 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/Unit/Services/Import/DataTypeInferencerTest.php | New test suite for data type inference service |
| tests/Unit/Services/Import/ColumnMatcherTest.php | New test suite for column matching service |
| tests/Feature/Filament/App/Imports/TaskImporterTest.php | Simplified tests to focus on assignee attachment only |
| tests/Feature/Filament/App/Imports/ImportWizardUITest.php | Added tests for preview caching, hash-based regeneration, and session cleanup |
| routes/console.php | Added scheduled cleanup command for import sessions |
| database/migrations/2026_01_02_152157_update_phone_number_custom_fields_type.php | Migration to convert phone_number fields from text to phone type |
| composer.json | Changed custom-fields to local path repository with @dev version |
| app-modules/ImportWizard/src/Services/DataTypeInferencer.php | New service to infer field types from sample data |
| app-modules/ImportWizard/src/Services/ColumnMatcher.php | New service for intelligent column header matching |
| app-modules/ImportWizard/src/Data/ImportSessionData.php | New DTO for consolidated session cache management |
| app-modules/ImportWizard/src/Enums/PreviewStatus.php | New enum for preview processing states |
| app-modules/ImportWizard/src/Jobs/ProcessImportPreview.php | Added heartbeat monitoring and cancellation logic |
| app-modules/ImportWizard/src/Console/CleanupOrphanedImportsCommand.php | Enhanced with dual-check safety system and dry-run support |
| app-modules/ImportWizard/src/Filament/Imports/* | Expanded guess patterns for better column matching |
| app-modules/ImportWizard/src/Livewire/* | Implemented preview input hashing, heartbeat tracking, and field labels |
| app-modules/ImportWizard/resources/views/* | Added UI for inferred mappings and new company tracking |
| lang/vendor/custom-fields/en/custom-fields.php | Removed unused language file |
| $assignee = User::factory()->create(); | ||
| $assignee->teams()->attach($this->team); |
Copilot
AI
Jan 3, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test creates a user with User::factory()->create() without explicitly attaching them to the team first. While line 19 shows the user being attached to the team, the order could cause confusion. Consider creating the user and immediately attaching them to the team in a clearer sequence to ensure the test intent is obvious.
| { | ||
| private const string EMAIL_PATTERN = '/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/'; | ||
|
|
||
| private const string PHONE_PATTERN = '/^[\+]?\d?[-.\s]?\(?\d{1,4}\)?[-.\s]?\d{1,4}[-.\s]?\d{1,4}[-.\s]?\d{0,4}$/'; |
Copilot
AI
Jan 3, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The PHONE_PATTERN regex is very permissive and may match non-phone numbers. For example, it would match sequences like '1-2-3-4' or '....'. Consider adding minimum digit requirements or using a more specific pattern to reduce false positives. The pattern could also match dates in certain formats (e.g., '2024-01-15'), which is why date checking is done first, but this ordering dependency creates a fragile design.
| ); | ||
|
|
||
| Action::configureUsing(fn (Action $action): Action => $action->size(Size::Small)->iconPosition('before')); | ||
| DeleteAction::configureUsing(fn (DeleteAction $action): DeleteAction => $action->label('Delete record')); |
Copilot
AI
Jan 3, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The DeleteAction::configureUsing() sets a label 'Delete record' for all delete actions globally. This may not be appropriate for all contexts. For example, deleting a batch of records or a file attachment might need different wording. Consider if this global configuration is too broad, or if specific resources should override this label as needed.
| DeleteAction::configureUsing(fn (DeleteAction $action): DeleteAction => $action->label('Delete record')); |
| return new class extends Migration | ||
| { | ||
| public function up(): void | ||
| { | ||
| // Get all phone_number custom field IDs that are currently text type | ||
| /** @var \Illuminate\Support\Collection<int, int|string> */ | ||
| $phoneFieldIds = DB::table('custom_fields') | ||
| ->where('code', 'phone_number') | ||
| ->where('type', 'text') | ||
| ->pluck('id'); | ||
|
|
||
| if ($phoneFieldIds->isEmpty()) { | ||
| return; | ||
| } | ||
|
|
||
| // Update the field type from text to phone | ||
| DB::table('custom_fields') | ||
| ->whereIn('id', $phoneFieldIds) | ||
| ->update(['type' => 'phone']); | ||
|
|
||
| // Migrate values from text_value to json_value (as JSON array) | ||
| // Phone type uses multiChoice schema which stores values in json_value | ||
| DB::table('custom_field_values') | ||
| ->whereIn('custom_field_id', $phoneFieldIds) | ||
| ->whereNotNull('text_value') | ||
| ->whereNull('json_value') | ||
| ->orderBy('id') | ||
| ->each(function (object $row): void { | ||
| DB::table('custom_field_values') | ||
| ->where('id', $row->id) | ||
| ->update([ | ||
| 'json_value' => json_encode([$row->text_value]), | ||
| 'text_value' => null, | ||
| ]); | ||
| }); | ||
| } | ||
| }; |
Copilot
AI
Jan 3, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This migration is missing a down() method to reverse the changes. Migrations should be reversible when possible. The down method should convert phone fields back to text type and migrate json_value back to text_value.
| Cache::put( | ||
| self::cacheKey($sessionId), | ||
| [...$this->toArray(), 'heartbeat' => (int) now()->timestamp], |
Copilot
AI
Jan 3, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The refresh() method updates the cache with a spread of $this->toArray() plus the new heartbeat, but it doesn't update the current object's $this->heartbeat property. This could lead to inconsistency if the same ImportSessionData instance is used after calling refresh(). Consider updating the object's heartbeat property as well, or making it clear through naming/documentation that this method only updates the cache.
| Cache::put( | |
| self::cacheKey($sessionId), | |
| [...$this->toArray(), 'heartbeat' => (int) now()->timestamp], | |
| $heartbeat = (int) now()->timestamp; | |
| $this->heartbeat = $heartbeat; | |
| Cache::put( | |
| self::cacheKey($sessionId), | |
| [...$this->toArray(), 'heartbeat' => $heartbeat], |
| public static function update(string $sessionId, array $changes): void | ||
| { | ||
| $data = self::find($sessionId); | ||
|
|
||
| if (! $data instanceof self) { | ||
| return; | ||
| } | ||
|
|
||
| Cache::put( | ||
| self::cacheKey($sessionId), | ||
| [...$data->toArray(), ...$changes], | ||
| now()->addHours(self::ttlHours()) | ||
| ); | ||
| } |
Copilot
AI
Jan 3, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The update() method has a race condition. Between calling find() (line 64) and Cache::put() (line 70-73), another process could update the cache, and those changes would be overwritten. For example, if two job chunks update progress simultaneously, one update could be lost. Consider using atomic cache operations like Cache::lock() or designing the update to merge changes more safely.
| ->label('Company') | ||
| ->color('primary') | ||
| ->url(fn (People $record): ?string => $record->company ? CompanyResource::getUrl('view', [$record->company]) : null), | ||
| ->url(fn(People $record): ?string => $record->company ? CompanyResource::getUrl('view', [$record->company]) : null), |
Copilot
AI
Jan 3, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The inline lambda function syntax fn(People $record): ?string => is missing a space after fn. According to Laravel Pint (which this project uses), there should be a space between fn and the parameter list. The existing code on line 39 has the correct format with fn (...), but this line uses fn(...) without the space.
| ->url(fn(People $record): ?string => $record->company ? CompanyResource::getUrl('view', [$record->company]) : null), | |
| ->url(fn (People $record): ?string => $record->company ? CompanyResource::getUrl('view', [$record->company]) : null), |
| $normalized = preg_replace('/[\s_\-\.]+/', '_', trim($value)); | ||
|
|
||
| return Str::lower($normalized ?? $value); |
Copilot
AI
Jan 3, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The normalize() method uses a regex pattern that replaces multiple separators with underscores, but the pattern [\s_\-\.]+ will match sequences of mixed separators (e.g., ".- _") and replace them with a single underscore. While this is likely the intended behavior, the subsequent call to collapse multiple underscores may not be necessary since the regex already handles this. Additionally, if the preg_replace fails and returns null, the fallback to $value may not be properly normalized. Consider handling this edge case more explicitly.
| $normalized = preg_replace('/[\s_\-\.]+/', '_', trim($value)); | |
| return Str::lower($normalized ?? $value); | |
| $trimmed = trim($value); | |
| $normalized = preg_replace('/[\s_\-\.]+/', '_', $trimmed); | |
| if ($normalized === null) { | |
| $normalized = $trimmed; | |
| } | |
| return Str::lower($normalized); |
| private function enrichRowWithCompanyMatch(array $row, string $teamId): array | ||
| { | ||
| $companyId = (string) ($row['id'] ?? ''); | ||
| $companyId = (string) ($row['company_id'] ?? ''); |
Copilot
AI
Jan 3, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The company_id is read from $row['company_id'] but this field was previously read as $row['id'] (see diff line removing it). This change might break existing functionality if the row structure hasn't been updated elsewhere. Verify that all places populating this row data have been updated to use 'company_id' instead of 'id'.
| $companyId = (string) ($row['company_id'] ?? ''); | |
| $companyId = (string) ($row['company_id'] ?? $row['id'] ?? ''); |
| // Clean up orphaned import sessions hourly | ||
| // Deletes temp files from abandoned imports (user left without completing) | ||
| Schedule::command('import:cleanup')->everyFifteenMinutes(); |
Copilot
AI
Jan 3, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment says the cleanup runs hourly, but the code specifies everyFifteenMinutes(). The comment should be updated to match the actual schedule frequency.
No description provided.