Skip to content

Commit faf22e1

Browse files
Merge pull request #57 from Relaticle/feature/enhancements
Feature/enhancements
2 parents 4c0683c + 67297c0 commit faf22e1

34 files changed

+1054
-276
lines changed

resources/lang/en/custom-fields.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
'default' => [
2525
'new_section' => 'New Section',
2626
],
27+
'default_section_name' => 'Default',
2728
],
2829

2930
'field' => [
@@ -65,6 +66,12 @@
6566
],
6667
'add_field' => 'Add Field',
6768
'system_defined_cannot_delete' => 'System-defined fields cannot be deleted.',
69+
'allow_multiple' => 'Allow Multiple Values',
70+
'allow_multiple_help' => 'When enabled, users can enter multiple values for this field.',
71+
'max_values' => 'Maximum Values',
72+
'max_values_help' => 'The maximum number of values that can be entered.',
73+
'unique_per_entity_type' => 'Unique Per Entity Type',
74+
'unique_per_entity_type_help' => 'Each value can only be assigned to one record of this entity type.',
6875
'validation' => [
6976
'label' => 'Validation',
7077
'rules' => 'Validation Rules',
@@ -315,6 +322,7 @@
315322
'multi_parameter_missing' => 'This validation rule requires multiple parameters. Please add all required parameters.',
316323
'parameter_missing' => 'This validation rule requires exactly :count parameter(s). Please add all required parameters.',
317324
'invalid_rule_for_field_type' => 'The selected rule is not valid for this field type.',
325+
'unique_value' => 'The value ":value" is already assigned to another record.',
318326
],
319327

320328
'empty_states' => [
@@ -328,9 +336,19 @@
328336
'description' => 'Drag and drop fields here or click the button below to add your first field.',
329337
'icon' => 'heroicon-o-squares-plus',
330338
],
339+
'fields_no_sections' => [
340+
'heading' => 'No custom fields yet',
341+
'description' => 'Click the button below to add your first custom field.',
342+
'icon' => 'heroicon-o-squares-plus',
343+
],
331344
],
332345

333346
'common' => [
334347
'inactive' => 'Inactive',
335348
],
349+
350+
'email' => [
351+
'add_email_placeholder' => 'Add email address...',
352+
],
353+
336354
];

resources/views/filament/pages/custom-fields-management.blade.php

Lines changed: 46 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -9,43 +9,57 @@
99
</x-filament::tabs>
1010

1111
<div class="custom-fields-component">
12-
<div
13-
x-sortable
14-
wire:end.stop="updateSectionsOrder($event.target.sortable.toArray())"
15-
class="flex flex-col gap-y-6"
16-
>
17-
@foreach ($this->sections as $section)
18-
@livewire('manage-custom-field-section', ['entityType' => $this->currentEntityType, 'section' => $section], key($section->id . str()->random(16)))
19-
@endforeach
12+
@if($this->isSectionsDisabled)
13+
{{-- Sections disabled mode: show flat field list with new component --}}
14+
@if($this->sections->first())
15+
@livewire('manage-fields-without-sections', [
16+
'entityType' => $this->currentEntityType,
17+
'section' => $this->sections->first(),
18+
], key('fields-without-sections-' . $this->currentEntityType))
19+
@endif
20+
@else
21+
{{-- Normal sections mode --}}
22+
<div
23+
x-sortable
24+
wire:end.stop="updateSectionsOrder($event.target.sortable.toArray())"
25+
class="flex flex-col gap-y-6"
26+
>
27+
@foreach ($this->sections as $section)
28+
@livewire('manage-custom-field-section', [
29+
'entityType' => $this->currentEntityType,
30+
'section' => $section,
31+
], key($section->id . str()->random(16)))
32+
@endforeach
2033

21-
@if(!count($this->sections))
22-
<div class="fi-ta-empty-state px-6 py-16">
23-
<div class="fi-ta-empty-state-content mx-auto grid max-w-md justify-items-center text-center">
24-
<div class="fi-ta-empty-state-icon-ctn mb-6 rounded-full bg-primary-50 p-4 dark:bg-primary-950/50">
25-
<x-filament::icon
26-
icon="{{ __('custom-fields::custom-fields.empty_states.sections.icon') }}"
27-
class="fi-ta-empty-state-icon h-8 w-8 text-primary-500 dark:text-primary-400"
28-
/>
29-
</div>
34+
@if(!count($this->sections))
35+
<div class="px-6 py-16">
36+
<div class="mx-auto grid max-w-md justify-items-center text-center">
37+
<div class="fi-ta-empty-state-icon-ctn mb-6 rounded-full bg-primary-50 p-4 dark:bg-primary-950/50">
38+
<x-filament::icon
39+
icon="{{ __('custom-fields::custom-fields.empty_states.sections.icon') }}"
40+
class="fi-ta-empty-state-icon h-8 w-8 text-primary-500 dark:text-primary-400"
41+
/>
42+
</div>
3043

31-
<h3 class="fi-ta-empty-state-heading text-lg font-semibold leading-7 text-gray-950 dark:text-white mb-2">
32-
{{ __('custom-fields::custom-fields.empty_states.sections.heading') }}
33-
</h3>
44+
<h3 class="fi-ta-empty-state-heading text-lg font-semibold leading-7 text-gray-950 dark:text-white mb-2">
45+
{{ __('custom-fields::custom-fields.empty_states.sections.heading') }}
46+
</h3>
3447

35-
<p class="fi-ta-empty-state-description text-sm text-gray-600 dark:text-gray-400 mb-6 leading-relaxed">
36-
{{ __('custom-fields::custom-fields.empty_states.sections.description') }}
37-
</p>
48+
<p class="fi-ta-empty-state-description text-sm text-gray-600 dark:text-gray-400 mb-6 leading-relaxed">
49+
{{ __('custom-fields::custom-fields.empty_states.sections.description') }}
50+
</p>
3851

39-
<div class="fi-ta-empty-state-action">
40-
{{ $this->createSectionAction }}
52+
<div class="fi-ta-empty-state-action">
53+
{{ $this->createSectionAction }}
54+
</div>
4155
</div>
4256
</div>
43-
</div>
44-
@else
45-
<div class="mt-6 flex justify-center">
46-
{{ $this->createSectionAction }}
47-
</div>
48-
@endif
49-
</div>
57+
@else
58+
<div class="mt-6 flex justify-center">
59+
{{ $this->createSectionAction }}
60+
</div>
61+
@endif
62+
</div>
63+
@endif
5064
</div>
5165
</x-filament-panels::page>

resources/views/livewire/manage-custom-field-section.blade.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
<x-filament::icon-button
1111
icon="heroicon-m-bars-4"
1212
color="gray"
13+
class="text-gray-500"
14+
size="xs"
1315
x-sortable-handle
1416
/>
1517

@@ -30,7 +32,7 @@
3032
x-sortable-group="fields"
3133
data-section-id="{{ $section->id }}"
3234
default="12"
33-
class="fi-sc fi-sc-has-gap fi-grid lg:fi-grid-cols"
35+
class="fi-sc fi-sc-has-gap fi-sc-dense fi-grid lg:fi-grid-cols"
3436
style="--cols-lg: repeat(12, minmax(0, 12fr)); --cols-default: repeat(2, minmax(0, 1fr));"
3537
@end.stop="$wire.updateFieldsOrder($event.to.getAttribute('data-section-id'), $event.to.sortable.toArray())"
3638
>
@@ -40,8 +42,8 @@ class="fi-sc fi-sc-has-gap fi-grid lg:fi-grid-cols"
4042

4143
@if(!count($this->fields))
4244
<div class="fi-grid-col" style="--col-span-default: span 12 / span 12;">
43-
<div class="fi-ta-empty-state py-12">
44-
<div class="fi-ta-empty-state-content mx-auto grid max-w-xs justify-items-center text-center">
45+
<div class="py-12">
46+
<div class="mx-auto grid max-w-xs justify-items-center text-center">
4547
<div class="fi-ta-empty-state-icon-ctn mb-4 rounded-full bg-gray-50 p-3 dark:bg-gray-800/50">
4648
<x-filament::icon
4749
icon="{{ __('custom-fields::custom-fields.empty_states.fields.icon') }}"

resources/views/livewire/manage-custom-field-width.blade.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ class="relative"
1414
<template x-for="(width, index) in widths" :key="index">
1515
<div
1616
wire:click="$parent.setWidth(fieldId, width)"
17-
class="h-6 flex-1 cursor-pointer bg-gray-200 hover:bg-gray-300 transition-colors"
17+
class="h-6 flex-1 cursor-pointer bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 transition-colors"
1818
:class="{
1919
'rounded-s-md': index === 0,
2020
'rounded-e-md': index === widths.length - 1
@@ -26,12 +26,12 @@ class="h-full w-full border-gray-300 transition-colors duration-200"
2626
'bg-primary-600 hover:bg-primary-600/80': isSelected(width),
2727
'rounded-s-md': index === 0 && isSelected(width),
2828
'rounded-e-md': index === widths.length - 1 && isSelected(width),
29-
'border-s': index !== widths.length - 1
29+
'border-s': index !== widths.length && index !== 0,
3030
}"
3131
></div>
3232
</div>
3333
</template>
3434
</div>
35-
<div class="absolute w-full h-full font-semibold text-sm flex items-center justify-center text-black">{{ $selectedWidth }}%</div>
35+
<div class="absolute w-full h-full font-semibold text-sm flex items-center justify-center text-gray-900 dark:text-white">{{ $selectedWidth }}%</div>
3636
</div>
3737
</div>

resources/views/livewire/manage-custom-field.blade.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@ class="fi-section !px-2 fi-compact !py-2 shadow-none fi-grid-col flex justify-be
1010
<x-filament::icon-button
1111
icon="heroicon-m-bars-3"
1212
color="gray"
13+
class="ml-0.5 text-gray-500"
14+
size="xs"
1315
/>
1416

1517
<x-filament::icon
1618
:icon="$field->typeData?->icon ?? 'heroicon-o-document-text'"
17-
class="h-5 w-5 text-gray-500 dark:text-gray-400"
19+
class="h-4.5 w-4.5 text-neutral-700 dark:text-neutral-400 ml-2"
1820
:aria-label="$field->name"
1921
/>
2022

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<div class="flex flex-col gap-y-6">
2+
@if(count($this->fields))
3+
<div
4+
x-sortable
5+
x-sortable-group="fields"
6+
class="fi-sc fi-sc-has-gap fi-sc-dense fi-grid lg:fi-grid-cols"
7+
style="--cols-lg: repeat(12, minmax(0, 12fr)); --cols-default: repeat(2, minmax(0, 1fr));"
8+
@end.stop="$wire.updateFieldsOrder($event.to.sortable.toArray())"
9+
>
10+
@foreach ($this->fields as $field)
11+
@livewire('manage-custom-field', ['field' => $field], key($field->id . $field->width->value . str()->random(16)))
12+
@endforeach
13+
</div>
14+
15+
<div class="flex justify-center">
16+
{{ $this->createFieldAction() }}
17+
</div>
18+
@else
19+
<div class="px-6 py-16">
20+
<div class="mx-auto grid max-w-md justify-items-center text-center">
21+
<div class="fi-ta-empty-state-icon-ctn mb-6 rounded-full bg-primary-50 p-4 dark:bg-primary-950/50">
22+
<x-filament::icon
23+
icon="{{ __('custom-fields::custom-fields.empty_states.fields_no_sections.icon') }}"
24+
class="fi-ta-empty-state-icon h-8 w-8 text-primary-500 dark:text-primary-400"
25+
/>
26+
</div>
27+
28+
<h3 class="fi-ta-empty-state-heading text-lg font-semibold leading-7 text-gray-950 dark:text-white mb-2">
29+
{{ __('custom-fields::custom-fields.empty_states.fields_no_sections.heading') }}
30+
</h3>
31+
32+
<p class="fi-ta-empty-state-description text-sm text-gray-600 dark:text-gray-400 mb-6 leading-relaxed">
33+
{{ __('custom-fields::custom-fields.empty_states.fields_no_sections.description') }}
34+
</p>
35+
36+
<div class="fi-ta-empty-state-action">
37+
{{ $this->createFieldAction() }}
38+
</div>
39+
</div>
40+
</div>
41+
@endif
42+
43+
<x-filament-actions::modals/>
44+
</div>
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Relaticle\CustomFields\Console\Commands;
6+
7+
use Illuminate\Console\Command;
8+
use Illuminate\Support\Facades\DB;
9+
use Throwable;
10+
11+
/**
12+
* Migrates email field values from string_value to json_value format.
13+
*
14+
* This command is needed after the EmailFieldType was changed from STRING
15+
* data type to MULTI_CHOICE, which stores values in json_value as an array.
16+
*/
17+
class MigrateEmailFieldValuesCommand extends Command
18+
{
19+
protected $signature = 'custom-fields:migrate-email-values
20+
{--dry-run : Show what would be migrated without making changes}
21+
{--force : Run without confirmation in production}';
22+
23+
protected $description = 'Migrate email field values from string_value to json_value array format';
24+
25+
public function handle(): int
26+
{
27+
$isDryRun = $this->option('dry-run');
28+
29+
if (app()->isProduction() && ! $isDryRun && ! $this->option('force') && ! $this->confirm('You are running in production. Are you sure you want to continue?')) {
30+
$this->info('Migration cancelled.');
31+
32+
return self::SUCCESS;
33+
}
34+
35+
$fieldTable = config('custom-fields.database.table_names.custom_fields');
36+
$valueTable = config('custom-fields.database.table_names.custom_field_values');
37+
38+
// Find all email type custom fields
39+
$emailFields = DB::table($fieldTable)
40+
->where('type', 'email')
41+
->pluck('id');
42+
43+
if ($emailFields->isEmpty()) {
44+
$this->info('No email fields found. Nothing to migrate.');
45+
46+
return self::SUCCESS;
47+
}
48+
49+
$this->info(sprintf('Found %d email field(s).', $emailFields->count()));
50+
51+
// Find values that need migration (have string_value but no json_value)
52+
$valuesToMigrate = DB::table($valueTable)
53+
->whereIn('custom_field_id', $emailFields)
54+
->whereNotNull('string_value')
55+
->where('string_value', '!=', '')
56+
->where(function ($query): void {
57+
$query->whereNull('json_value')
58+
->orWhere('json_value', '=', '[]')
59+
->orWhere('json_value', '=', 'null');
60+
})
61+
->get();
62+
63+
if ($valuesToMigrate->isEmpty()) {
64+
$this->info('No email values need migration. All values are already in the correct format.');
65+
66+
return self::SUCCESS;
67+
}
68+
69+
$this->info(sprintf('Found %d email value(s) to migrate.', $valuesToMigrate->count()));
70+
71+
if ($isDryRun) {
72+
$this->warn('Dry run mode - no changes will be made.');
73+
$this->newLine();
74+
75+
$this->table(
76+
['ID', 'Entity Type', 'Entity ID', 'Current Value', 'New Format'],
77+
$valuesToMigrate->map(fn ($value): array => [
78+
$value->id,
79+
$value->entity_type,
80+
$value->entity_id,
81+
$value->string_value,
82+
json_encode([$value->string_value]),
83+
])->toArray()
84+
);
85+
86+
return self::SUCCESS;
87+
}
88+
89+
$bar = $this->output->createProgressBar($valuesToMigrate->count());
90+
$bar->start();
91+
92+
$migrated = 0;
93+
$errors = 0;
94+
95+
foreach ($valuesToMigrate as $value) {
96+
try {
97+
DB::table($valueTable)
98+
->where('id', $value->id)
99+
->update([
100+
'json_value' => json_encode([$value->string_value]),
101+
'string_value' => null,
102+
]);
103+
104+
$migrated++;
105+
} catch (Throwable $e) {
106+
$this->newLine();
107+
$this->error(sprintf('Failed to migrate value ID %d: %s', $value->id, $e->getMessage()));
108+
$errors++;
109+
}
110+
111+
$bar->advance();
112+
}
113+
114+
$bar->finish();
115+
$this->newLine(2);
116+
117+
$this->info(sprintf('Migration complete. Migrated: %d, Errors: %d', $migrated, $errors));
118+
119+
return $errors > 0 ? self::FAILURE : self::SUCCESS;
120+
}
121+
}

0 commit comments

Comments
 (0)