diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index b0ee5d8..6b01ca5 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -35,6 +35,7 @@ Before submitting a pull request: - Check the codebase to ensure that your feature doesn't already exist. - Check the pull requests to ensure that another person hasn't already submitted the feature or fix. +- Link resolved issues in the PR body using [GitHub keywords](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue) (`Closes #123`, `Fixes #123`, or `Resolves #123`) so they auto-close on merge. ## Requirements diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..3ac0c0a --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,13 @@ +## Summary + + + +## Linked issues + + + +Closes # + +## Test plan + +- [ ] diff --git a/.github/scripts/install-smoke-dependencies.sh b/.github/scripts/install-smoke-dependencies.sh new file mode 100755 index 0000000..8113e5f --- /dev/null +++ b/.github/scripts/install-smoke-dependencies.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash + +set -euo pipefail + +LARAVEL="${1}" +PHP="${2}" +FILAMENT="${3}" + +case "${LARAVEL}" in + 13.*) + TESTBENCH='^11.0' + CARBON='^3.8.6' + PEST='^4.4.1' + PEST_PLUGIN_LARAVEL='^4.1' + ;; + 12.*) + TESTBENCH='^10.0' + CARBON='^3.8.6' + PEST='^3.8.6' + PEST_PLUGIN_LARAVEL='^3.1' + ;; + *) + echo "Unsupported Laravel version for smoke tests: ${LARAVEL}" >&2 + exit 1 + ;; +esac + +composer require \ + "laravel/framework:${LARAVEL}" \ + "filament/forms:^${FILAMENT}" \ + "filament/support:^${FILAMENT}" \ + --no-interaction --no-update + +composer require --dev \ + "orchestra/testbench:${TESTBENCH}" \ + "nesbot/carbon:${CARBON}" \ + "filament/filament:^${FILAMENT}" \ + "pestphp/pest:${PEST}" \ + "pestphp/pest-plugin-laravel:${PEST_PLUGIN_LARAVEL}" \ + --no-interaction --no-update + +composer update --prefer-stable --prefer-dist --no-interaction diff --git a/.github/scripts/install-test-dependencies.sh b/.github/scripts/install-test-dependencies.sh new file mode 100755 index 0000000..5f2682f --- /dev/null +++ b/.github/scripts/install-test-dependencies.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash + +set -euo pipefail + +LARAVEL="${1}" +STABILITY="${2}" + +composer require "laravel/framework:${LARAVEL}" --no-interaction --no-update + +case "${LARAVEL}" in + 13.*) + TESTBENCH='^11.0' + CARBON='^3.8.6' + PEST='^4.4.1' + PEST_PLUGIN_LARAVEL='^4.1' + ;; + 12.*) + TESTBENCH='^10.0' + CARBON='^3.8.6' + PEST='^3.8.6' + PEST_PLUGIN_LARAVEL='^3.1' + ;; + 11.*) + TESTBENCH='^9.0' + CARBON='^3.8.4' + PEST='^3.8.6' + PEST_PLUGIN_LARAVEL='^3.1' + ;; + *) + echo "Unsupported Laravel version: ${LARAVEL}" >&2 + exit 1 + ;; +esac + +composer require --dev \ + "orchestra/testbench:${TESTBENCH}" \ + "nesbot/carbon:${CARBON}" \ + "pestphp/pest:${PEST}" \ + "pestphp/pest-plugin-laravel:${PEST_PLUGIN_LARAVEL}" \ + --no-interaction --no-update + +composer update "--${STABILITY}" --prefer-dist --no-interaction diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index ad2781f..e8da656 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -6,6 +6,10 @@ on: pull_request: branches: [main] +defaults: + run: + shell: bash + jobs: test: runs-on: ${{ matrix.os }} @@ -13,20 +17,14 @@ jobs: fail-fast: true matrix: os: [ubuntu-latest, windows-latest] - php: [8.4, 8.3, 8.2] - laravel: [12.*, 11.*] + php: [8.5, 8.4, 8.3, 8.2] + laravel: [13.*, 12.*, 11.*] stability: [prefer-lowest, prefer-stable] - include: - - laravel: 12.* - testbench: ^10.0 - carbon: ^3.8.6 - pest-plugin-laravel: ^3.1 - larastan: ^3.1 + exclude: + - laravel: 13.* + php: 8.2 - laravel: 11.* - testbench: ^9.0 - carbon: ^3.8.4 - pest-plugin-laravel: ^3.1 - larastan: ^3.1 + php: 8.5 name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.stability }} - ${{ matrix.os }} @@ -36,7 +34,6 @@ jobs: - name: Configure Git line endings if: runner.os == 'Windows' - shell: bash run: git config --global core.autocrlf false - name: Setup PHP @@ -52,9 +49,7 @@ jobs: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" - name: Install dependencies - run: | - composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" "nesbot/carbon:${{ matrix.carbon }}" "pestphp/pest-plugin-laravel:${{ matrix.pest-plugin-laravel }}" "nunomaduro/larastan:${{ matrix.larastan }}" --no-interaction --no-update - composer update --${{ matrix.stability }} --prefer-dist --no-interaction + run: bash .github/scripts/install-test-dependencies.sh "${{ matrix.laravel }}" "${{ matrix.stability }}" - name: List Installed Dependencies run: composer show -D @@ -64,3 +59,45 @@ jobs: - name: Execute tests run: vendor/bin/pest --ci --coverage --min=0 + + - name: Upload coverage to Codecov + if: matrix.os == 'ubuntu-latest' && matrix.php == '8.4' && matrix.laravel == '13.*' && matrix.stability == 'prefer-stable' + uses: codecov/codecov-action@v5 + with: + files: build/logs/clover.xml + fail_ci_if_error: false + + platform-smoke: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - php: 8.5 + laravel: 13.* + filament: '5.0' + - php: 8.4 + laravel: 13.* + filament: '5.0' + - php: 8.2 + laravel: 12.* + filament: '4.0' + + name: Smoke P${{ matrix.php }} - L${{ matrix.laravel }} - F${{ matrix.filament }} + + steps: + - name: Checkout code + uses: actions/checkout@v5 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo + coverage: none + + - name: Install dependencies + run: bash .github/scripts/install-smoke-dependencies.sh "${{ matrix.laravel }}" "${{ matrix.php }}" "${{ matrix.filament }}" + + - name: Execute tests + run: vendor/bin/pest --ci --no-coverage diff --git a/README.md b/README.md index 88899f8..ce960f6 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,21 @@

- Tests - Styling - Laravel v12.x - Filament v4.x - Tailwind v4.x - PHP 8.4 - Packagist + CI + Coverage + Latest Version on Packagist + Total Downloads

-Use **[Flatpickr](https://flatpickr.js.org/)** as your datepicker in the Filament Forms and Panels. -![image](https://github.com/user-attachments/assets/334ea64a-48c3-48bc-a640-72162802a646) +Use **[Flatpickr](https://flatpickr.js.org/)** as your datepicker in Filament Forms and Panels. +![image](https://github.com/user-attachments/assets/334ea64a-48c3-48bc-a640-72162802a646) ## Supported Versions -| Package Version | Supported Filament Version(s) | -|-----------------|-------------------------------| -| v2.x | Filament v2 | -| v4.x | Filament v3 | -| v5.x | Filament v4 | + +| Package | Filament | Laravel | PHP | +|---------|----------|---------|-----| +| **v5.x** (current) | 4.x, 5.x | 11.x – 13.x | 8.2 – 8.5 (PHP 8.5 from Laravel 12+; Laravel 13 from PHP 8.3+) | +| v4.x | 3.x | 10.x – 11.x | 8.1 – 8.3 | +| v2.x | 2.x | 9.x – 10.x | 8.0 – 8.2 | ## Installation @@ -50,15 +48,16 @@ You can use the Flatpickr component from this package as: * Week Picker, * Multiple-Date Picker * Month Picker +* Year Picker Most of the fluent config methods are similar to [Flatpickr's official](https://flatpickr.js.org/options/) options in naming. -This package is also an extension of [Filament's DateTimePicker](https://filamentphp.com/docs/3.x/forms/fields/date-time-picker), so most of the methods are similar to the ones in the DateTimePicker component. You can use the Flatpickr component as a drop-in replacement for the DateTimePicker component. +This package is inspired by [Filament's DateTimePicker](https://filamentphp.com/docs/4.x/forms/overview) and can be used as a flexible alternative with Flatpickr-specific options. Here are some examples of the methods. Refer to Flatpickr's Official Documentation for details on each of the configurations. ```php -use Coolsam\FilamentFlatpickr\Forms\Components\Flatpickr; +use Coolsam\Flatpickr\Forms\Components\Flatpickr; // Basic, Date Field Flatpickr::make('test_field') // Minimal Config as a datepicker @@ -84,17 +83,18 @@ Flatpickr::make('test_field') ->conjunction(',') // Applicable only for the MultiDatePicker: Separate inputs using this conjunction. The package will use this conjunction to explode the inputs to an array. ->inline(true) // Display the datepicker inline with the input, instead of using a popover. ->disableMobile(true) // Disable mobile-version of the datepicker on mobile devices. - ->mode(\Coolsam\FilamentFlatpickr\Enums\FlatpickrMode::RANGE) // Set the mode as single, range or multiple. Alternatively, you can just use ->range() or ->multiple() - ->monthSelectorType(\Coolsam\FilamentFlatpickr\Enums\FlatpickrMonthSelectorType::DROPDOWN) + ->mode(\Coolsam\Flatpickr\Enums\FlatpickrMode::RANGE) // Set the mode as single, range or multiple. Alternatively, you can just use ->range() or ->multiple() + ->monthSelectorType(\Coolsam\Flatpickr\Enums\FlatpickrMonthSelectorType::DROPDOWN) ->shorthandCurrentMonth(true) ->noCalendar(true) // use this in conjunction with `time()` to have a timePicker - ->position(\Coolsam\FilamentFlatpickr\Enums\FlatpickrPosition::AUTO_CENTER) + ->position(\Coolsam\Flatpickr\Enums\FlatpickrPosition::AUTO_CENTER) ->showMonths(1) ->weekNumbers(true) ->time24hr(true) ->timePicker() // Configure a timepicker out of the box ->weekPicker() // configure a week picker out of the box ->monthPicker() // configure a month picker out of the box + ->yearPicker() // configure a year picker out of the box ->rangePicker() // configure a date range picker out of the box ->multiplePicker() // Configure a multiple date picker out of the box ; @@ -106,6 +106,7 @@ Flatpickr::make('test_field') \Coolsam\Flatpickr\Forms\Components\Flatpickr::make('start_time')->timePicker(), \Coolsam\Flatpickr\Forms\Components\Flatpickr::make('week_number')->weekPicker()->format('W Y'), \Coolsam\Flatpickr\Forms\Components\Flatpickr::make('month')->monthPicker()->format('Y-m')->displayFormat('F Y'), +\Coolsam\Flatpickr\Forms\Components\Flatpickr::make('year')->yearPicker(), \Coolsam\Flatpickr\Forms\Components\Flatpickr::make('range')->rangePicker(), \Coolsam\Flatpickr\Forms\Components\Flatpickr::make('occupied_slots')->multiplePicker()->format('Y-m-d')->displayFormat('F j, Y'), ``` @@ -117,7 +118,7 @@ You can set the package's theme globally under the `theme` config in the `confi The `\Coolsam\Flatpickr\Enums\FlatpickrTheme::DEFAULT` theme is already set by default and conforms to the filament design system. ```php -use Coolsam\FilamentFlatpickr\Enums\FlatpickrTheme; +use Coolsam\Flatpickr\Enums\FlatpickrTheme; return [ 'theme' => FlatpickrTheme::AIRBNB, ]; @@ -126,7 +127,7 @@ See the screenshots below for the different themes. ## State Types The package supports the following state types: -- `string` or `CarbonInterface` for DateTimePicker, DatePicker, TimePicker, WeekPicker, MonthPicker +- `string` or `CarbonInterface` for DateTimePicker, DatePicker, TimePicker, WeekPicker, MonthPicker, YearPicker - `array` for RangePicker, MultiplePicker (an array of date strings or CarbonInterface instances) ## Screenshots diff --git a/composer.json b/composer.json index 2827288..a2755d5 100644 --- a/composer.json +++ b/composer.json @@ -30,9 +30,8 @@ "filament/filament": "^4.0|^5.0", "laravel/pint": "^1.0", "nunomaduro/larastan": "^3.1.0", - "orchestra/testbench": "^9.12", - "pestphp/pest-plugin-laravel": "^3.1", - "pestphp/pest-plugin-livewire": "^3.0", + "orchestra/testbench": "^10.0|^11.0", + "pestphp/pest-plugin-laravel": "^3.1|^4.1", "phpstan/extension-installer": "^1.4.3", "spatie/laravel-ray": "^1.39" }, @@ -73,4 +72,4 @@ }, "minimum-stability": "dev", "prefer-stable": true -} \ No newline at end of file +} diff --git a/tests/FlatpickrComponentTest.php b/tests/FlatpickrComponentTest.php new file mode 100644 index 0000000..bea2204 --- /dev/null +++ b/tests/FlatpickrComponentTest.php @@ -0,0 +1,102 @@ +weekPicker()->format('W Y'); + + expect($component->isWeekPicker())->toBeTrue() + ->and($component->getFormat())->toBe('W Y') + ->and($component->getFlatpickrAttributes()['weekPicker'])->toBeTrue(); +}); + +it('configures month picker defaults and attributes', function () { + $component = Flatpickr::make('month')->monthPicker()->format('Y-m'); + + expect($component->isMonthPicker())->toBeTrue() + ->and($component->getFormat())->toBe('Y-m') + ->and($component->getFlatpickrAttributes()['monthPicker'])->toBeTrue(); +}); + +it('configures time picker attributes with no calendar', function () { + $component = Flatpickr::make('start_time')->timePicker(); + + $attributes = $component->getFlatpickrAttributes(); + + expect($component->isTimePicker())->toBeTrue() + ->and($attributes['timePicker'])->toBeTrue() + ->and($attributes['noCalendar'])->toBeTrue() + ->and($attributes['enableTime'])->toBeTrue() + ->and($attributes['dateFormat'])->toBe('H:i'); +}); + +it('uses seconds format for time picker when enabled', function () { + $component = Flatpickr::make('start_time')->timePicker()->seconds(); + + expect($component->getFormat())->toBe('H:i:S') + ->and($component->getFlatpickrAttributes()['dateFormat'])->toBe('H:i:S'); +}); + +it('dehydrates datetime values to carbon instances', function () { + $component = Flatpickr::make('published_at')->time(true)->format('Y-m-d H:i:s'); + + $result = Flatpickr::dehydrateFlatpickr($component, '2024-06-15 14:30:00'); + + expect($result)->toBeInstanceOf(CarbonInterface::class) + ->and($result->format('Y-m-d H:i:s'))->toBe('2024-06-15 14:30:00'); +}); + +it('passes min and max dates through to flatpickr attributes', function () { + $component = Flatpickr::make('date') + ->format('Y-m-d') + ->minDate('2024-01-01') + ->maxDate(Carbon::parse('2024-12-31')); + + $attributes = $component->getFlatpickrAttributes(); + + expect($attributes['minDate'])->toBe('2024-01-01') + ->and($attributes['maxDate'])->toBe('2024-12-31'); +}); + +it('detects range picker mode from fluent helper and enum', function () { + $fromHelper = Flatpickr::make('range')->rangePicker(); + $fromMode = Flatpickr::make('range')->mode(FlatpickrMode::RANGE); + + expect($fromHelper->isRangePicker())->toBeTrue() + ->and($fromMode->isRangePicker())->toBeTrue() + ->and($fromHelper->getFlatpickrAttributes()['rangePicker'])->toBeTrue(); +}); + +it('detects multiple picker mode from fluent helper and enum', function () { + $fromHelper = Flatpickr::make('dates')->multiplePicker(); + $fromMode = Flatpickr::make('dates')->mode(FlatpickrMode::MULTIPLE); + + expect($fromHelper->isMultiplePicker())->toBeTrue() + ->and($fromMode->isMultiplePicker())->toBeTrue() + ->and($fromHelper->getFlatpickrAttributes()['multiplePicker'])->toBeTrue(); +}); + +it('defaults month picker format when not explicitly configured', function () { + $component = Flatpickr::make('month')->monthPicker(); + + expect($component->getFormat())->toBe('Y-m-d') + ->and($component->isMonthPicker())->toBeTrue() + ->and($component->getFlatpickrAttributes()['monthPicker'])->toBeTrue(); +}); + +it('filters out unparsable single date values during dehydration', function () { + $component = Flatpickr::make('date')->format('Y-m-d'); + + expect(Flatpickr::dehydrateFlatpickr($component, 'not-a-date'))->toBe([]); +}); + +it('uses the default range separator and conjunction', function () { + $range = Flatpickr::make('range')->rangePicker(); + $multiple = Flatpickr::make('dates')->multiplePicker(); + + expect($range->getRangeSeparator())->toBe(' to ') + ->and($multiple->getConjunction())->toBe(','); +}); diff --git a/tests/FlatpickrHelpersTest.php b/tests/FlatpickrHelpersTest.php new file mode 100644 index 0000000..c6687f5 --- /dev/null +++ b/tests/FlatpickrHelpersTest.php @@ -0,0 +1,29 @@ +toBe('coolsam/flatpickr'); +}); + +it('coerces boolean values consistently', function (mixed $value, bool $expected) { + expect(FilamentFlatpickr::getBool($value))->toBe($expected); +})->with([ + 'true boolean' => [true, true], + 'false boolean' => [false, false], + 'string true' => ['true', true], + 'string false' => ['false', false], + 'integer one' => [1, true], + 'integer zero' => [0, false], + 'string yes' => ['yes', true], + 'string no' => ['no', false], +]); + +it('coerces integer values consistently', function (mixed $value, int $expected) { + expect(FilamentFlatpickr::getInt($value))->toBe($expected); +})->with([ + 'integer' => [12, 12], + 'numeric string' => ['3', 3], + 'invalid string' => ['abc', 0], + 'null' => [null, 0], +]); diff --git a/tests/FlatpickrServiceProviderTest.php b/tests/FlatpickrServiceProviderTest.php new file mode 100644 index 0000000..159ef88 --- /dev/null +++ b/tests/FlatpickrServiceProviderTest.php @@ -0,0 +1,13 @@ +toBeString() + ->and(FilamentAsset::getStyleHref('flatpickr-styles', 'coolsam/flatpickr'))->toBeString(); +}); + +it('exposes the flatpickr package name', function () { + expect(FlatpickrServiceProvider::$name)->toBe('flatpickr'); +}); diff --git a/tests/FlatpickrValidationTest.php b/tests/FlatpickrValidationTest.php new file mode 100644 index 0000000..f848763 --- /dev/null +++ b/tests/FlatpickrValidationTest.php @@ -0,0 +1,84 @@ +format('Y-m-d'); + + expect($component->getValidationRules())->toContain('date'); +}); + +it('does not register the date rule for range pickers', function () { + $component = Flatpickr::make('range')->rangePicker()->format('Y-m-d'); + + expect($component->getValidationRules())->not->toContain('date'); +}); + +it('validates multiple picker values as dates', function () { + $component = Flatpickr::make('dates') + ->multiplePicker() + ->format('Y-m-d') + ->conjunction(','); + + $rules = $component->getValidationRules(); + $closureRule = collect($rules)->first(fn ($rule) => $rule instanceof Closure); + + expect($closureRule)->toBeInstanceOf(Closure::class); + + $validator = Validator::make( + ['dates' => '2024-06-01,not-a-date'], + ['dates' => [$closureRule]], + ); + + expect($validator->fails())->toBeTrue(); +}); + +it('validates range picker values as dates', function () { + $component = Flatpickr::make('range') + ->rangePicker() + ->format('Y-m-d'); + + $rules = $component->getValidationRules(); + $closureRule = collect($rules)->first(fn ($rule) => $rule instanceof Closure); + + expect($closureRule)->toBeInstanceOf(Closure::class); + + $validator = Validator::make( + ['range' => '2024-06-01 to invalid'], + ['range' => [$closureRule]], + ); + + expect($validator->fails())->toBeTrue(); +}); + +it('registers min and max date rules for single date fields', function () { + $component = Flatpickr::make('date') + ->format('Y-m-d') + ->minDate('2024-01-01') + ->maxDate('2024-12-31'); + + $rules = collect($component->getValidationRules()) + ->filter(fn ($rule) => is_string($rule)) + ->values() + ->all(); + + expect($rules)->toContain('after_or_equal:2024-01-01') + ->and($rules)->toContain('before_or_equal:2024-12-31'); +}); + +it('does not register min and max date rules for range pickers', function () { + $component = Flatpickr::make('range') + ->rangePicker() + ->format('Y-m-d') + ->minDate('2024-01-01') + ->maxDate('2024-12-31'); + + $stringRules = collect($component->getValidationRules()) + ->filter(fn ($rule) => is_string($rule)) + ->values() + ->all(); + + expect($stringRules)->not->toContain('after_or_equal:2024-01-01') + ->and($stringRules)->not->toContain('before_or_equal:2024-12-31'); +});