diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index f33babc..ccb6546 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1,4 +1,4 @@ -name: CoreBundle +name: PHP Symfony CI on: push: @@ -7,18 +7,46 @@ on: branches: [ main, develop ] jobs: - phpunit: + build: runs-on: ubuntu-latest + + strategy: + matrix: + php: [8.1, 8.2, 8.3] + symfony: [6.4.*, 7.0.*, 7.1.*] + exclude: + - php: 8.1 + symfony: 7.1.* + - php: 8.1 + symfony: 7.0.* + + services: + mysql: + image: mysql:latest + env: + MYSQL_ROOT_PASSWORD: root + ports: + - 3306:3306 + options: >- + --health-cmd "mysqladmin ping --silent" + --health-interval 10s + --health-timeout 5s + --health-retries 3 + steps: - - uses: shivammathur/setup-php@2cb9b829437ee246e9b3cac53555a39208ca6d28 + - uses: actions/checkout@v4 + - name: Setup PHP ${{ matrix.php }} + uses: shivammathur/setup-php@v2 with: - php-version: '8.1' - - uses: actions/checkout@v2 - - name: Install Dependencies - run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist - - name: Execute tests (Unit and Feature tests) via PHPUnit + php-version: ${{ matrix.php }} + tools: flex + - name: Download dependencies + env: + SYMFONY_REQUIRE: ${{ matrix.symfony }} + uses: ramsey/composer-install@v2 + - name: Run test suite on PHP ${{ matrix.php }} and Symfony ${{ matrix.symfony }} run: vendor/bin/simple-phpunit - - name: Check Code Styles + - name: Run ECS run: vendor/bin/ecs - - name: Check PHP Stan + - name: Run PHPStan run: vendor/bin/phpstan analyse src tests diff --git a/CHANGELOG.md b/CHANGELOG.md index 8060a63..cb7a670 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # CHANGELOG +## v1.2.0 + - Removed symfony ^5.4 support + - Introduced `FlashBagExecption` to handle flash messages in a more flexible way + - Introduced `BadgeFormatter` to display badges in a standardized way + - Improved `datetime_controller.js` to handle more date formats + - Introduced `input-mask_controller.js` to handle input masks like Money or Security-Numbers + ## v1.0.6 - More documentation and better styling of the documentation - Deprecated methods `getContainer` and `get` of `BaseCommand` diff --git a/composer.json b/composer.json index f270e7f..ed105e4 100644 --- a/composer.json +++ b/composer.json @@ -18,26 +18,26 @@ "php": ">=8.1", "ext-bcmath": "*", "doctrine/collections": "~1.0|~2.0", - "symfony/console": "^5.4|^6.4|^7.0", - "symfony/framework-bundle": "^5.4|^6.4|^7.0", - "symfony/http-kernel": "^5.4|^6.4|^7.0", - "symfony/intl" : "^5.4|^6.4|^7.0", - "symfony/options-resolver" : "^5.4|^6.4|^7.0", + "symfony/console": "^6.4|^7.0", + "symfony/framework-bundle": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/intl" : "^6.4|^7.0", + "symfony/options-resolver" : "^6.4|^7.0", "whatwedo/twig-bootstrap-icons": "^1.0.0", - "symfony/translation": "^5.4|^6.4|^7.0", - "symfony/form": "^5.4|^6.4|^7.0", - "symfony/stopwatch": "^5.4|^6.4|^7.0", + "symfony/translation": "^6.4|^7.0", + "symfony/form": "^6.4|^7.0", + "symfony/stopwatch": "^6.4|^7.0", "symfony/test-pack": "^1.1.0", "symfony/orm-pack": "^2.4.1" }, "require-dev": { - "symfony/phpunit-bridge": "^5.4|^6.4|^7.0", - "symfony/config": "^5.4|^6.4|^7.0", - "symfony/dependency-injection": "^5.4|^6.4|^7.0", - "symfony/yaml": "^5.4|^6.4|^7.0", + "symfony/phpunit-bridge": "^6.4|^7.0", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/yaml": "^6.4|^7.0", "doctrine/doctrine-bundle": "^2.5.5", "whatwedo/php-coding-standard": "^1.0", - "symfony/twig-bundle": "^5.4|^6.4|^7.0", + "symfony/twig-bundle": "^6.4|^7.0", "phpstan/phpstan": "^1.7" }, "autoload": { diff --git a/docs/index.html b/docs/index.html index 288fd95..dc92634 100644 --- a/docs/index.html +++ b/docs/index.html @@ -79,7 +79,19 @@ diff --git a/src/DependencyInjection/araiseCoreExtension.php b/src/DependencyInjection/araiseCoreExtension.php index 13247c2..4243d3c 100644 --- a/src/DependencyInjection/araiseCoreExtension.php +++ b/src/DependencyInjection/araiseCoreExtension.php @@ -6,8 +6,8 @@ use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Extension\Extension; use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; -use Symfony\Component\HttpKernel\DependencyInjection\Extension; /** * This is the class that loads and manages your bundle configuration. diff --git a/src/Exception/FlashBagExecption.php b/src/Exception/FlashBagExecption.php new file mode 100644 index 0000000..f5a487d --- /dev/null +++ b/src/Exception/FlashBagExecption.php @@ -0,0 +1,25 @@ +flashType = $flashType; + parent::__construct($message !== '' ? $message : $flashMessage, $code, $previous); + } + + public function getFlashType(): string + { + return $this->flashType; + } + + public function getFlashMessage(): string + { + return $this->message; + } +} diff --git a/src/Formatter/BadgeFormatter.php b/src/Formatter/BadgeFormatter.php new file mode 100644 index 0000000..0a187ca --- /dev/null +++ b/src/Formatter/BadgeFormatter.php @@ -0,0 +1,60 @@ +processOptions(($this->options[self::OPT_CONFIGURATION])($value, $this->options)); + return $this->twig->render($this->options[self::OPT_TEMPLATE], [ + 'value' => $this->getString($value), + 'type' => $this->options[self::OPT_TYPE], + 'background_color_class' => $this->options[self::OPT_BACKGROUND_COLOR_CLASS], + 'background_color_hex' => $this->options[self::OPT_BACKGROUND_COLOR_HEX], + 'link' => $this->options[self::OPT_LINK], + ]); + } + + protected function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefault(self::OPT_TEMPLATE, '@araiseCore/formatter/badge.html.twig'); + $resolver->setDefaults([ + self::OPT_BACKGROUND_COLOR_CLASS => null, + self::OPT_BACKGROUND_COLOR_HEX => null, + self::OPT_TYPE => 'neutral', + self::OPT_LINK => null, + self::OPT_CONFIGURATION => static fn (mixed $value, array $options): array => $options, + ]); + $resolver->setAllowedTypes(self::OPT_BACKGROUND_COLOR_CLASS, ['string', 'null']); + $resolver->setAllowedTypes(self::OPT_BACKGROUND_COLOR_HEX, ['string', 'null']); + $resolver->setAllowedTypes(self::OPT_TYPE, 'string'); + $resolver->setAllowedTypes(self::OPT_LINK, ['string', 'null']); + $resolver->setAllowedTypes(self::OPT_CONFIGURATION, 'callable'); + $resolver->setAllowedValues(self::OPT_TYPE, [ + 'primary', + 'neutral', + 'error', + 'warning', + 'success', + ]); + } +} diff --git a/src/Manager/FormatterManager.php b/src/Manager/FormatterManager.php index ec2dce2..986026a 100644 --- a/src/Manager/FormatterManager.php +++ b/src/Manager/FormatterManager.php @@ -30,7 +30,7 @@ namespace araise\CoreBundle\Manager; use araise\CoreBundle\Formatter\FormatterInterface; -use Symfony\Component\DependencyInjection\Attribute\TaggedIterator; +use Symfony\Component\DependencyInjection\Attribute\AutowireIterator; class FormatterManager { @@ -43,7 +43,8 @@ class FormatterManager * @param FormatterInterface[] $formatters */ public function __construct( - #[TaggedIterator('araise_core.formatter')] iterable $formatters + #[AutowireIterator('araise_core.formatter')] + iterable $formatters ) { foreach ($formatters as $formatter) { $this->formatters[$formatter::class] = $formatter; diff --git a/src/Resources/assets/controllers/datetime_controller.js b/src/Resources/assets/controllers/datetime_controller.js index ea32785..59140b1 100644 --- a/src/Resources/assets/controllers/datetime_controller.js +++ b/src/Resources/assets/controllers/datetime_controller.js @@ -4,8 +4,16 @@ import easepickStyle from '!!raw-loader!@easepick/bundle/dist/index.css' export default class extends Controller { static values = { - lang: String + /** + * The language to use within the date picker (translation of months and days) + */ + lang: String, + /** + * The format passed from the server to correctly parse the date + */ + format: String } + connect() { if (this.element.tagName !== 'INPUT') { return; @@ -14,7 +22,7 @@ export default class extends Controller { const enableTime = type === 'time' || type === 'datetime-local'; let plugins = [KbdPlugin]; - if(enableTime) { + if (enableTime) { plugins.push(TimePlugin); } @@ -22,6 +30,7 @@ export default class extends Controller { element: this.element, css: easepickStyle, lang: this.langValue || 'de-DE', + format: this.formatValue || (enableTime ? "YYYY-MM-DDTHH:mm" : "YYYY-MM-DD"), readonly: false, plugins: plugins, calendars: type === 'time' ? 0 : 1, diff --git a/src/Resources/assets/controllers/dropdown_controller.js b/src/Resources/assets/controllers/dropdown_controller.js index 6b0d48b..acc224e 100644 --- a/src/Resources/assets/controllers/dropdown_controller.js +++ b/src/Resources/assets/controllers/dropdown_controller.js @@ -17,10 +17,9 @@ export default class extends Controller { } toggle (event) { - event.stopPropagation(); - const dropdownDiv = this.menuTarget; if (this.hasMenuTarget && dropdownDiv.classList.contains('hidden')) { + window.dispatchEvent(new Event('araise-dropdown:open')); dropdownDiv.classList.remove('hidden'); } else { dropdownDiv.classList.add('hidden'); diff --git a/src/Resources/assets/controllers/input-mask_controller.js b/src/Resources/assets/controllers/input-mask_controller.js new file mode 100644 index 0000000..0641ddc --- /dev/null +++ b/src/Resources/assets/controllers/input-mask_controller.js @@ -0,0 +1,31 @@ +import { Controller } from "@hotwired/stimulus" + +const typeMapping = { + 'Number': Number, +}; +export default class extends Controller { + static values = { + mask: String, + scale: { type: Number, default: 2 }, + radix: { type: String, default: '.' }, + thousandsSeparator: { type: String, default: '\'' }, + normalizeZeros: { type: Boolean, default: false }, + min: Number, + max: Number, + }; + async connect() { + const { default: IMask } = await import('imask'); + this.mask = IMask(this.element, { + mask: typeMapping[this.maskValue] || this.maskValue, + scale: this.scaleValue, + radix: this.radixValue, + thousandsSeparator: this.thousandsSeparatorValue, + normalizeZeros: this.normalizeZerosValue, + min: this.hasMinValue ? this.minValue : null, + max: this.hasMaxValue ? this.maxValue : null, + }); + } + disconnect() { + this.mask?.destroy(); + } +} diff --git a/src/Resources/assets/package.json b/src/Resources/assets/package.json index f57a6f0..e31c0bd 100644 --- a/src/Resources/assets/package.json +++ b/src/Resources/assets/package.json @@ -6,13 +6,19 @@ "combobox": { "main": "controllers/combobox_controller.js", "webpackMode": "eager", - "fetch": "eager", + "fetch": "lazy", "enabled": true }, "datetime": { "main": "controllers/datetime_controller.js", "webpackMode": "eager", - "fetch": "eager", + "fetch": "lazy", + "enabled": true + }, + "input-mask": { + "main": "controllers/input-mask_controller.js", + "webpackMode": "eager", + "fetch": "lazy", "enabled": true }, "dropdown": { @@ -24,7 +30,7 @@ "modal-form": { "main": "controllers/modal-form_controller.js", "webpackMode": "eager", - "fetch": "eager", + "fetch": "lazy", "enabled": true }, "reload-content": { @@ -36,7 +42,7 @@ "modal": { "main": "controllers/modal_controller.js", "webpackMode": "eager", - "fetch": "eager", + "fetch": "lazy", "enabled": true } } @@ -51,6 +57,7 @@ "dependencies": { "@easepick/bundle": "^1.2.1", "flatpickr": "^4.6.9", + "imask": "^7.6.1", "raw-loader": "^4.0.2", "stimulus-dropdown": "^2.0.0", "stimulus-use": "^0.51.1", diff --git a/src/Resources/assets/styles/_tailwind.scss b/src/Resources/assets/styles/_tailwind.scss index 0ef6bdd..07f6915 100644 --- a/src/Resources/assets/styles/_tailwind.scss +++ b/src/Resources/assets/styles/_tailwind.scss @@ -36,9 +36,31 @@ @apply text-base font-semibold text-neutral-500; } + .whatwedo-utility-content { + @extend .whatwedo-utility-paragraph; + + a { + text-decoration: underline solid currentColor; + text-underline-offset: 0.2em; + text-decoration-thickness: 1px; + + &:hover { + @apply text-neutral-900; + } + } + } + .whatwedo-utility-link { @extend .whatwedo-utility-heading-4; @apply text-neutral-800; + text-decoration: underline solid transparent; + text-underline-offset: 0.2em; + text-decoration-thickness: 1px; + transition: text-decoration 0.2s ease; + + &:hover { + text-decoration-color: currentColor; + } } .whatwedo-utility-button { @@ -58,6 +80,45 @@ @apply text-neutral-500; } + // Badges + .whatwedo-utility-badge { + @apply inline-flex items-center rounded-full px-2 py-1 text-xs font-medium bg-primary-500 text-white; + text-wrap: nowrap; + + &--success { + @apply bg-success-500; + } + + &--warning { + @apply bg-warning-500; + } + + &--error { + @apply bg-error-500; + } + + &--neutral { + @apply bg-neutral-500; + } + + &--primary { + @apply bg-primary-500; + } + } + + .whatwedo-utility-button-badge { + @apply inline-flex items-center rounded-full px-2 py-0.5 ml-2 text-xs font-medium bg-primary-500 text-white; + text-wrap: nowrap; + + &--primary { + @apply bg-primary-500 text-white; + } + + &--white { + @apply bg-white text-primary-500; + } + } + // Topbar .whatwedo-utility-topbar { @apply flex flex-grow-0 flex-auto px-4 py-2 md:py-4 md:min-h-[71px] justify-end items-center lg:justify-between; @@ -96,6 +157,10 @@ min-height: 2.2rem; } +.whatwedo_core-checkbox { + @apply text-primary-500 focus:ring-1 focus:ring-primary-500 rounded cursor-pointer; +} + .whatwedo_core-input--rounded-left { @extend .whatwedo_core-input; @apply rounded-none rounded-l-md; @@ -106,6 +171,11 @@ @apply inline-flex items-center bg-primary-500 hover:bg-primary-700; } +.whatwedo-crud-button--rounded { + @extend .whatwedo-utility-button; + @apply rounded-full p-1; +} + .whatwedo-crud-button--action-danger { @extend .whatwedo-utility-button; @apply inline-flex items-center bg-error-500 hover:bg-red-700; @@ -156,6 +226,10 @@ @apply text-neutral-700 bg-transparent hover:bg-neutral-200 font-semibold; } } + + .whatwedo-utility-button-badge--white { + @extend .whatwedo-utility-button-badge--primary; + } } /* TomSelect */ diff --git a/src/Resources/assets/yarn.lock b/src/Resources/assets/yarn.lock index abbcf16..1fab689 100644 --- a/src/Resources/assets/yarn.lock +++ b/src/Resources/assets/yarn.lock @@ -2,6 +2,14 @@ # yarn lockfile v1 +"@babel/runtime-corejs3@^7.24.4": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.24.8.tgz#c0ae5a1c380f8442920866d0cc51de8024507e28" + integrity sha512-DXG/BhegtMHhnN7YPIvxWd303/9aXvYFD1TjNL3CD6tUrhI2LVsg3Lck0aql5TRH29n4sj3emcROypkZVUfSuA== + dependencies: + core-js-pure "^3.30.2" + regenerator-runtime "^0.14.0" + "@easepick/amp-plugin@^1.1.0": version "1.2.1" resolved "https://registry.yarnpkg.com/@easepick/amp-plugin/-/amp-plugin-1.2.1.tgz#6d189d0a429721da082d8e8fe8d45dd0df3b76d1" @@ -121,6 +129,11 @@ big.js@^5.2.2: resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== +core-js-pure@^3.30.2: + version "3.37.1" + resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.37.1.tgz#2b4b34281f54db06c9a9a5bd60105046900553bd" + integrity sha512-J/r5JTHSmzTxbiYYrzXg9w1VpqrYt+gexenBE9pugeyhwPZTAEJddyiReJWsLO6uNQ8xJZFbod6XC7KKwatCiA== + emojis-list@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" @@ -146,6 +159,13 @@ hotkeys-js@>=3: resolved "https://registry.yarnpkg.com/hotkeys-js/-/hotkeys-js-3.9.5.tgz#8314d0522bf2601e36003213047e9dc7d56d19fe" integrity sha512-T0G8CUZ6Q1IOgPnLK1hDXR8DqgKF/VWEsHvZgi6CM7Ub9oOAzvWuJ3Qhc/9nFQaR26MfFOZSwULHrtCnsUX7zA== +imask@^7.6.1: + version "7.6.1" + resolved "https://registry.yarnpkg.com/imask/-/imask-7.6.1.tgz#04fa4693bf47a4a71bbf7325408e0d058a74dcad" + integrity sha512-sJlIFM7eathUEMChTh9Mrfw/IgiWgJqBKq2VNbyXvBZ7ev/IlO6/KQTKlV/Fm+viQMLrFLG/zCuudrLIwgK2dg== + dependencies: + "@babel/runtime-corejs3" "^7.24.4" + json-schema-traverse@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" @@ -178,6 +198,11 @@ raw-loader@^4.0.2: loader-utils "^2.0.0" schema-utils "^3.0.0" +regenerator-runtime@^0.14.0: + version "0.14.1" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" + integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== + schema-utils@^3.0.0: version "3.1.1" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.1.1.tgz#bc74c4b6b6995c1d88f76a8b77bea7219e0c8281" diff --git a/src/Resources/views/formatter/badge.html.twig b/src/Resources/views/formatter/badge.html.twig new file mode 100644 index 0000000..48e4ef8 --- /dev/null +++ b/src/Resources/views/formatter/badge.html.twig @@ -0,0 +1,18 @@ +{% set classes = [] %} +{% if background_color_hex|default %} + {% set classes = classes|merge(["bg-[#{background_color_hex}]"]) %} +{% endif %} + +{% if background_color_class|default %} + {% set classes = classes|merge([background_color_class]) %} +{% endif %} + +{% if link != '' %} + + {{ value }} + +{% else %} +
+ {{ value }} +
+{% endif %} diff --git a/src/Resources/views/includes/_action.html.twig b/src/Resources/views/includes/_action.html.twig index 81dce91..462f7c1 100644 --- a/src/Resources/views/includes/_action.html.twig +++ b/src/Resources/views/includes/_action.html.twig @@ -65,7 +65,12 @@ class: 'inline h-4 w-4'~(not actionLabel is empty ? ' mr-2' : '') }) }} {% endif %} - {{ actionLabel }} + + {{ actionLabel }} + {% if action.option('attr')['count'] is defined %} + + {% endif %} + {% endif %} {% endblock %} @@ -191,7 +196,7 @@ {{ stimulus_action('araise/core-bundle/modal-form', 'close', 'click') | stimulus_action('araise/core-bundle/modal-form', 'close', 'keydown.esc@window') }} > - {# This element is to trick the browser into centering the modal contents. #} + {# This element is to trick the browser into centering the modal contents. Code is coming from Tailwind UI #}+ test +
'; + + $this->assertSame(trim($expected), trim($html)); + } + + public function testGetHtmlWithLink(): void + { + $badgeFormatter = $this->getFormatter(BadgeFormatter::class); + $badgeFormatter->processOptions([ + BadgeFormatter::OPT_TYPE => 'error', + BadgeFormatter::OPT_LINK => 'https://www.whatwedo.ch', + ]); + $html = $badgeFormatter->getHtml('test'); + + $expected = ' + test + '; + + $this->assertSame(trim($expected), trim($html)); + } + + public function testGetHtmlWithColorClass(): void + { + $badgeFormatter = $this->getFormatter(BadgeFormatter::class); + $badgeFormatter->processOptions([ + BadgeFormatter::OPT_TYPE => 'error', + BadgeFormatter::OPT_BACKGROUND_COLOR_CLASS => 'bg-red-500', + ]); + $html = $badgeFormatter->getHtml('test'); + + $expected = '+ test +
'; + + $this->assertSame(trim($expected), trim($html)); + } + + public function testGetHtmlWithColorHex(): void + { + $badgeFormatter = $this->getFormatter(BadgeFormatter::class); + $badgeFormatter->processOptions([ + BadgeFormatter::OPT_TYPE => 'error', + BadgeFormatter::OPT_BACKGROUND_COLOR_HEX => '#ff0000', + ]); + $html = $badgeFormatter->getHtml('test'); + + $expected = '+ test +
'; + + $this->assertSame(trim($expected), trim($html)); + } + + public function testGetHtmlWithColorClassAndHex(): void + { + $badgeFormatter = $this->getFormatter(BadgeFormatter::class); + $badgeFormatter->processOptions([ + BadgeFormatter::OPT_TYPE => 'error', + BadgeFormatter::OPT_BACKGROUND_COLOR_CLASS => 'bg-red-500', + BadgeFormatter::OPT_BACKGROUND_COLOR_HEX => '#ff0000', + ]); + $html = $badgeFormatter->getHtml('test'); + + $expected = '+ test +
'; + + $this->assertSame(trim($expected), trim($html)); + } +}