diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..4597dfb --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: TappNetwork diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 0000000..fe4cfe6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,66 @@ +name: Bug Report +description: Report an Issue or Bug with the Package +title: "[Bug]: " +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + We're sorry to hear you have a problem. Can you help us solve it by providing the following details. + - type: textarea + id: what-happened + attributes: + label: What happened? + description: What did you expect to happen? + placeholder: I cannot currently do X thing because when I do, it breaks X thing. + validations: + required: true + - type: textarea + id: how-to-reproduce + attributes: + label: How to reproduce the bug + description: How did this occur, please add any config values used and provide a set of reliable steps if possible. + placeholder: When I do X I see Y. + validations: + required: true + - type: input + id: package-version + attributes: + label: Package Version + description: What version of our Package are you running? Please be as specific as possible + placeholder: 2.0.0 + validations: + required: true + - type: input + id: php-version + attributes: + label: PHP Version + description: What version of PHP are you running? Please be as specific as possible + placeholder: 8.2.0 + validations: + required: true + - type: input + id: laravel-version + attributes: + label: Laravel Version + description: What version of Laravel are you running? Please be as specific as possible + placeholder: 9.0.0 + validations: + required: true + - type: dropdown + id: operating-systems + attributes: + label: Which operating systems does with happen with? + description: You may select more than one. + multiple: true + options: + - macOS + - Windows + - Linux + - type: textarea + id: notes + attributes: + label: Notes + description: Use this field to provide any other notes that you feel might be relevant to the issue. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..0abc156 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,11 @@ +blank_issues_enabled: false +contact_links: + - name: Ask a question + url: https://github.com/TappNetwork/filament-value-range-filter/discussions/new?category=q-a + about: Ask the community for help + - name: Request a feature + url: https://github.com/TappNetwork/filament-value-range-filter/discussions/new?category=ideas + about: Share ideas for new features + - name: Report a security issue + url: https://github.com/TappNetwork/filament-value-range-filter/security/policy + about: Learn how to notify us for sensitive bugs diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..39b1580 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,19 @@ +# Please see the documentation for all configuration options: +# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + labels: + - "dependencies" + + - package-ecosystem: "composer" + directory: "/" + schedule: + interval: "weekly" + labels: + - "dependencies" diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml new file mode 100644 index 0000000..056063f --- /dev/null +++ b/.github/workflows/dependabot-auto-merge.yml @@ -0,0 +1,33 @@ +name: dependabot-auto-merge +on: pull_request_target + +permissions: + pull-requests: write + contents: write + +jobs: + dependabot: + runs-on: ubuntu-latest + timeout-minutes: 5 + if: ${{ github.actor == 'dependabot[bot]' }} + steps: + + - name: Dependabot metadata + id: metadata + uses: dependabot/fetch-metadata@v2.1.0 + with: + github-token: "${{ secrets.GITHUB_TOKEN }}" + + - name: Auto-merge Dependabot PRs for semver-minor updates + if: ${{steps.metadata.outputs.update-type == 'version-update:semver-minor'}} + run: gh pr merge --auto --merge "$PR_URL" + env: + PR_URL: ${{github.event.pull_request.html_url}} + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} + + - name: Auto-merge Dependabot PRs for semver-patch updates + if: ${{steps.metadata.outputs.update-type == 'version-update:semver-patch'}} + run: gh pr merge --auto --merge "$PR_URL" + env: + PR_URL: ${{github.event.pull_request.html_url}} + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} diff --git a/.github/workflows/pint.yml b/.github/workflows/pint.yml new file mode 100644 index 0000000..109191e --- /dev/null +++ b/.github/workflows/pint.yml @@ -0,0 +1,23 @@ +name: PHP Linting (Pint) +on: + workflow_dispatch: + push: + branches-ignore: + - 'dependabot/npm_and_yarn/*' +jobs: + phplint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 2 + - name: "laravel-pint" + uses: aglipanci/laravel-pint-action@0.1.0 + with: + preset: laravel + + - name: Commit changes + uses: stefanzweifel/git-auto-commit-action@v4 + with: + commit_message: PHP Linting (Pint) + skip_fetch: true diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml new file mode 100644 index 0000000..8b1cb5d --- /dev/null +++ b/.github/workflows/run-tests.yml @@ -0,0 +1,55 @@ +name: run-tests + +on: + push: + paths: + - '**.php' + - '.github/workflows/run-tests.yml' + - 'phpunit.xml.dist' + - 'composer.json' + - 'composer.lock' + +jobs: + test: + runs-on: ${{ matrix.os }} + timeout-minutes: 5 + strategy: + fail-fast: true + matrix: + os: [ubuntu-latest] + php: [8.3, 8.2] + laravel: [10.*] + stability: [prefer-lowest, prefer-stable] + include: + - laravel: 10.* + testbench: 8.* + carbon: ^2.63 + + name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.stability }} - ${{ matrix.os }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - 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: Setup problem matchers + run: | + echo "::add-matcher::${{ runner.tool_cache }}/php.json" + 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 }}" --no-interaction --no-update + composer update --${{ matrix.stability }} --prefer-dist --no-interaction + + - name: List Installed Dependencies + run: composer show -D + + - name: Execute tests + run: vendor/bin/pest --ci diff --git a/.github/workflows/update-changelog.yml b/.github/workflows/update-changelog.yml new file mode 100644 index 0000000..9ba1ece --- /dev/null +++ b/.github/workflows/update-changelog.yml @@ -0,0 +1,28 @@ +name: 'Update Changelog' + +on: + release: + types: [released] + +jobs: + update: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{ github.event.release.target_commitish }} + + - name: Update Changelog + uses: stefanzweifel/changelog-updater-action@v1 + with: + latest-version: ${{ github.event.release.name }} + release-notes: ${{ github.event.release.body }} + + - name: Commit updated CHANGELOG + uses: stefanzweifel/git-auto-commit-action@v4 + with: + branch: ${{ github.event.release.target_commitish }} + commit_message: Update CHANGELOG + file_pattern: CHANGELOG.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c8e37f6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +/node_modules +/vendor +.phpunit.result.cache +Homestead.json +Homestead.yaml +auth.json +npm-debug.log +yarn-error.log +/.idea +/.vscode +/build +/coverage +.DS_Store +composer.phar +phpunit.xml +phpstan.neon diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..f9b495f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +# Changelog + +All notable changes to the "Filament Value Range Filter" will be documented in this file. diff --git a/README.md b/README.md new file mode 100644 index 0000000..2260d8b --- /dev/null +++ b/README.md @@ -0,0 +1,88 @@ +# Filament Value Range Filter + +[![Latest Version on Packagist](https://img.shields.io/packagist/v/tapp/filament-value-range-filter.svg?style=flat-square)](https://packagist.org/packages/tapp/filament-value-range-filter) +![Code Style Action Status](https://github.com/TappNetwork/filament-value-range-filter/actions/workflows/pint.yml/badge.svg) +[![Total Downloads](https://img.shields.io/packagist/dt/tapp/filament-value-range-filter.svg?style=flat-square)](https://packagist.org/packages/tapp/filament-value-range-filter) + +A value range filter for Laravel Filament. + +## Installation + +```bash +composer require tapp/filament-value-range-filter +``` + +Optionally, you can publish the translations files with: + +```bash +php artisan vendor:publish --tag="filament-value-range-filter-translations" +``` + +## Appareance + +![Filament Value Range Filters](https://raw.githubusercontent.com/TappNetwork/filament-value-range-filter/main/docs/filters.png) + +![Filament Value Range Filter Between Indicator](https://raw.githubusercontent.com/TappNetwork/filament-value-range-filter/main/docs/filter_indicator.png) + +![Filament Value Range Filter Options](https://raw.githubusercontent.com/TappNetwork/filament-value-range-filter/main/docs/filter_range_options.png) + +## Usage + +### Filter + +Add to your Filament resource: + +```php +use Tapp\FilamentValueRangeFilter\Filters\ValueRangeFilter; + +public static function table(Table $table): Table +{ + return $table + //... + ->filters([ + ValueRangeFilter::make('project_value') + ->currency(), + ValueRangeFilter::make('estimated_hours'), + // ... + ]) +} +``` + +### Options + +#### Currency + +You may use the `->currency()` method to format the values on placeholder and filter indicator as currency. The default currency format is `USD`. + +```php +ValueRangeFilter::make('project_value') + ->currency(), +``` + +**Change the currency format** + +The `->currencyCode()` and `->locale()` methods can be used to change the currency format. +You can pass one of the [ISO 4217 currency codes](https://www.iban.com/currency-codes) to the `->currencyCode()` method. + +```php +ValueRangeFilter::make('project_value') + ->currency() + ->currencyCode('EUR') + ->locale('fr'), +``` + +![Filament Value Range Filter Between currency in EUR](https://raw.githubusercontent.com/TappNetwork/filament-value-range-filter/main/docs/between_eur.png) + +![Filament Value Range Filter Between currency in EUR Indicator](https://raw.githubusercontent.com/TappNetwork/filament-value-range-filter/main/docs/filter_indicator_eur.png) + +**Currency value** + +When using currency values, the filter assumes that the value stored on database that will be compared with the provided value on filter is in the smallest unit of the currency (e.g., cents for USD). Therefore, the value provided in the filter is by default multiplied by 100 to be compared with the value stored in the database. + +If the values stored in your database are not in the currency's smallest unit and you do not need the value provided in the filter to be multiplied by 100, pass 'false' to the `->currencyInSmallestUnit()` method: + +```php +ValueRangeFilter::make('project_value') + ->currency() + ->currencyInSmallestUnit(false), +``` diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..9ed3f61 --- /dev/null +++ b/composer.json @@ -0,0 +1,94 @@ +{ + "name": "tapp/filament-value-range-filter", + "description": "Filament country code field.", + "keywords": [ + "tapp network", + "filament", + "laravel", + "value range", + "filter" + ], + "license": "MIT", + "authors": [ + { + "name": "Tapp Network", + "email": "steve@tappnetwork.com", + "role": "Developer" + }, + { + "name": "Tapp Network", + "email": "andreia.bohner@tappnetwork.com", + "role": "Developer" + } + ], + "homepage": "https://github.com/TappNetwork/filament-value-range-filter", + "support": { + "issues": "https://github.com/TappNetwork/filament-value-range-filter/issues", + "source": "https://github.com/TappNetwork/filament-value-range-filter" + }, + "require": { + "php": "^8.1", + "filament/filament": "^3.0-stable" + }, + "require-dev": { + "laravel/pint": "^1.14", + "nunomaduro/collision": "^8.1.1||^7.10.0", + "larastan/larastan": "^2.9", + "orchestra/testbench": "^9.0.0||^8.22.0", + "pestphp/pest": "^2.34", + "pestphp/pest-plugin-arch": "^2.7", + "pestphp/pest-plugin-laravel": "^2.3", + "phpstan/extension-installer": "^1.3", + "phpstan/phpstan-deprecation-rules": "^1.1", + "phpstan/phpstan-phpunit": "^1.3", + "spatie/laravel-ray": "^1.35" + }, + "autoload": { + "psr-4": { + "Tapp\\FilamentValueRangeFilter\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "Tapp\\FilamentValueRangeFilter\\Tests\\": "tests/", + "Workbench\\App\\": "workbench/app/" + } + }, + "scripts": { + "post-autoload-dump": "@composer run prepare", + "clear": "@php vendor/bin/testbench package:purge-skeleton --ansi", + "prepare": "@php vendor/bin/testbench package:discover --ansi", + "build": [ + "@composer run prepare", + "@php vendor/bin/testbench workbench:build --ansi" + ], + "start": [ + "Composer\\Config::disableProcessTimeout", + "@composer run build", + "@php vendor/bin/testbench serve" + ], + "analyse": "vendor/bin/phpstan analyse", + "test": "vendor/bin/pest", + "test-coverage": "vendor/bin/pest --coverage", + "format": "vendor/bin/pint" + }, + "config": { + "sort-packages": true, + "allow-plugins": { + "pestphp/pest-plugin": true, + "phpstan/extension-installer": true + } + }, + "extra": { + "laravel": { + "providers": [ + "Tapp\\FilamentValueRangeFilter\\FilamentValueRangeFilterServiceProvider" + ], + "aliases": { + "FilamentValueRangeFilter": "Tapp\\FilamentValueRangeFilter\\Facades\\FilamentValueRangeFilter" + } + } + }, + "minimum-stability": "dev", + "prefer-stable": true +} diff --git a/config/filament-value-range-filter.php b/config/filament-value-range-filter.php new file mode 100644 index 0000000..ca5d8ed --- /dev/null +++ b/config/filament-value-range-filter.php @@ -0,0 +1,5 @@ + 'is equal to', + 'range.options.between' => 'is between', + 'range.options.greater_than' => 'is greater than', + 'range.options.less_than' => 'is less than', + +]; diff --git a/src/Concerns/HasCurrency.php b/src/Concerns/HasCurrency.php new file mode 100644 index 0000000..8d37ebe --- /dev/null +++ b/src/Concerns/HasCurrency.php @@ -0,0 +1,56 @@ +isCurrency = $condition; + + return $this; + } + + public function isCurrency(): bool + { + return (bool) $this->evaluate($this->isCurrency); + } + + public function currencyInSmallestUnit(bool|Closure $condition = true): static + { + $this->isCurrencyInSmallestUnit = $condition; + + return $this; + } + + public function isCurrencyInSmallestUnit(): bool + { + return (bool) $this->evaluate($this->isCurrencyInSmallestUnit); + } + + /** + * ISO 4217 currency code + * + * @see https://www.php.net/manual/en/numberformatter.formatcurrency.php + * @see https://www.iban.com/currency-codes + */ + public function currencyCode(string|Closure $currencyCode = 'USD'): static + { + $this->currencyCode = $currencyCode; + + return $this; + } + + public function getCurrencyCode(): string + { + return $this->evaluate($this->currencyCode); + } +} diff --git a/src/Facades/FilamentValueRangeFilter.php b/src/Facades/FilamentValueRangeFilter.php new file mode 100644 index 0000000..983535e --- /dev/null +++ b/src/Facades/FilamentValueRangeFilter.php @@ -0,0 +1,16 @@ +name('filament-value-range-filter') + ->hasConfigFile() + ->hasTranslations(); + } + + public function packageBooted(): void + { + // + } +} diff --git a/src/Filters/ValueRangeFilter.php b/src/Filters/ValueRangeFilter.php new file mode 100644 index 0000000..9c4a52c --- /dev/null +++ b/src/Filters/ValueRangeFilter.php @@ -0,0 +1,213 @@ +form(fn () => [ + Forms\Components\Fieldset::make($this->getlabel()) + ->schema([ + Forms\Components\Select::make('range_condition') + ->hiddenLabel() + ->placeholder('Select condition') + ->live() + ->options([ + 'equal' => __('filament-value-range-filter::filament-value-range-filter.range.options.equal'), + 'between' => __('filament-value-range-filter::filament-value-range-filter.range.options.between'), + 'greater_than' => __('filament-value-range-filter::filament-value-range-filter.range.options.greater_than'), + 'less_than' => __('filament-value-range-filter::filament-value-range-filter.range.options.less_than'), + ]) + ->afterStateUpdated(function (Set $set) { + $set('range_equal', null); + $set('range_between_from', null); + $set('range_between_to', null); + $set('range_greater_than', null); + $set('range_less_than', null); + }), + Forms\Components\TextInput::make('range_equal') + ->hiddenLabel() + ->numeric() + ->placeholder(fn (): string => $this->getFormattedValue(0)) + ->visible(fn (Get $get): bool => $get('range_condition') === 'equal' || empty($get('range_condition'))), + Forms\Components\Grid::make([ + 'default' => 1, + 'sm' => 2, + ]) + ->schema([ + Forms\Components\TextInput::make('range_between_from') + ->hiddenLabel() + ->numeric() + ->placeholder(fn (): string => $this->getFormattedValue(0)), + Forms\Components\TextInput::make('range_between_to') + ->hiddenLabel() + ->numeric() + ->placeholder(fn (): string => $this->getFormattedValue(0)), + ]) + ->visible(fn (Get $get): bool => $get('range_condition') === 'between'), + Forms\Components\TextInput::make('range_greater_than') + ->hiddenLabel() + ->numeric() + ->placeholder(fn (): string => $this->getFormattedValue(0)) + ->visible(fn (Get $get): bool => $get('range_condition') === 'greater_than'), + Forms\Components\TextInput::make('range_less_than') + ->hiddenLabel() + ->numeric() + ->placeholder(fn (): string => $this->getFormattedValue(0)) + ->visible(fn (Get $get): bool => $get('range_condition') === 'less_than'), + ]) + ->columns(1), + ]) + ->query(function (Builder $query, array $data): Builder { + return $query + ->when( + $data['range_equal'], + fn (Builder $query, $value): Builder => $query->where($this->getName(), '=', $this->getValue($value)), + ) + ->when( + $data['range_between_from'] && $data['range_between_to'], + function (Builder $query, $value) use ($data) { + $query->where($this->getName(), '>=', $this->getValue($data['range_between_from']))->where($this->getName(), '<=', $this->getValue($data['range_between_to'])); + }, + ) + ->when( + $data['range_greater_than'], + fn (Builder $query, $value): Builder => $query->where($this->getName(), '>', $this->getValue($value)), + ) + ->when( + $data['range_less_than'], + fn (Builder $query, $value): Builder => $query->where($this->getName(), '<', $this->getValue($value)), + ); + }) + ->indicateUsing(function (array $data): array { + $indicators = []; + + if ($data['range_between_from'] || $data['range_between_to']) { + $indicators[] = Indicator::make($this->getIndicatorBetweenLabel() ?? $this->getLabel().' is between '.$this->getFormattedValue($data['range_between_from']).' and '.$this->getFormattedValue($data['range_between_to'])) + ->removeField('range_between_from') + ->removeField('range_between_to'); + } + + if ($data['range_equal']) { + $indicators[] = Indicator::make($this->getIndicatorEqualLabel() ?? $this->getLabel().' is equal to '.$this->getFormattedValue($data['range_equal'])) + ->removeField('range_equal'); + } + + if ($data['range_greater_than']) { + $indicators[] = Indicator::make($this->getIndicatorGreaterThanLabel() ?? $this->getLabel().' is greater than '.$this->getFormattedValue($data['range_greater_than'])) + ->removeField('range_greater_than'); + } + + if ($data['range_less_than']) { + $indicators[] = Indicator::make($this->getIndicatorLessThanLabel() ?? $this->getLabel().' is less than '.$this->getFormattedValue($data['range_less_than'])) + ->removeField('range_less_than'); + } + + return $indicators; + }); + } + + protected function getValue($value) + { + if ($this->isCurrency()) { + return $this->isCurrencyInSmallestUnit ? $value * 100 : $value; + } + + return $value; + } + + protected function getFormattedValue($value) + { + if ($this->isCurrency() && $value !== null) { + return $this->isCurrency ? Number::currency($value, in: $this->currencyCode, locale: $this->locale) : $value; + } + + return $value; + } + + public function indicatorBetweenLabel(?string $label): static + { + $this->indicatorBetweenLabel = $label; + + return $this; + } + + public function getIndicatorBetweenLabel(): ?string + { + return $this->evaluate($this->indicatorBetweenLabel); + } + + public function indicatorEqualLabel(?string $label): static + { + $this->indicatorEqualLabel = $label; + + return $this; + } + + public function getIndicatorEqualLabel(): ?string + { + return $this->evaluate($this->indicatorEqualLabel); + } + + public function indicatorGreaterThanLabel(?string $label): static + { + $this->indicatorGreaterThanLabel = $label; + + return $this; + } + + public function getIndicatorGreaterThanLabel(): ?string + { + return $this->evaluate($this->indicatorGreaterThanLabel); + } + + public function indicatorLessThanLabel(?string $label): static + { + $this->indicatorLessThanLabel = $label; + + return $this; + } + + public function getIndicatorLessThanLabel(): ?string + { + return $this->evaluate($this->indicatorLessThanLabel); + } + + public function locale(string|Closure $locale = 'en'): static + { + $this->locale = $locale; + + return $this; + } + + public function getLocale(): string + { + return $this->evaluate($this->locale); + } +} diff --git a/tests/ArchTest.php b/tests/ArchTest.php new file mode 100644 index 0000000..87fb64c --- /dev/null +++ b/tests/ArchTest.php @@ -0,0 +1,5 @@ +expect(['dd', 'dump', 'ray']) + ->each->not->toBeUsed(); diff --git a/tests/ExampleTest.php b/tests/ExampleTest.php new file mode 100644 index 0000000..5d36321 --- /dev/null +++ b/tests/ExampleTest.php @@ -0,0 +1,5 @@ +toBeTrue(); +}); diff --git a/tests/Pest.php b/tests/Pest.php new file mode 100644 index 0000000..bcee919 --- /dev/null +++ b/tests/Pest.php @@ -0,0 +1,5 @@ +in(__DIR__); diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 0000000..e043e74 --- /dev/null +++ b/tests/TestCase.php @@ -0,0 +1,28 @@ +set('database.default', 'testing'); + } +}