├── .eslintrc ├── .github ├── FUNDING.yml └── workflows │ ├── php-cs-fixer.yml │ ├── run-tests.yml │ ├── update-assets.yml │ └── update-changelog.yml ├── .php_cs.dist.php ├── .prettierrc ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json ├── dist └── js │ ├── field.js │ └── field.js.LICENSE.txt ├── package-lock.json ├── package.json ├── resources └── js │ ├── components │ ├── Nova │ │ ├── DetailField.vue │ │ ├── FormField.vue │ │ └── IndexField.vue │ └── Tags │ │ ├── MultiTagsInput.vue │ │ ├── SingleTagsInput.vue │ │ └── TagsInput.vue │ └── field.js ├── routes └── api.php ├── src ├── Http │ └── Controllers │ │ └── TagsFieldController.php ├── Tags.php └── TagsFieldServiceProvider.php ├── testbench.yaml ├── webpack.mix.js └── workbench ├── app ├── Models │ └── User.php ├── Nova │ ├── Resource.php │ ├── Tag.php │ └── User.php └── Providers │ └── NovaServiceProvider.php ├── bootstrap └── .gitkeep ├── database ├── factories │ ├── .gitkeep │ └── UserFactory.php ├── migrations │ └── 2024_12_26_011907_create_tag_tables.php └── seeders │ └── DatabaseSeeder.php ├── resources └── views │ └── .gitkeep ├── routes ├── console.php └── web.php └── storage /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended", 4 | "plugin:vue/essential", 5 | "prettier" 6 | ], 7 | "parserOptions": { 8 | "ecmaVersion": 2017 9 | }, 10 | "globals": { 11 | "Nova": true 12 | }, 13 | "env": { 14 | "browser": true, 15 | "node": true 16 | }, 17 | "rules": { 18 | "vue/html-indent": ["error", 4] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: https://spatie.be/open-source/support-us 2 | -------------------------------------------------------------------------------- /.github/workflows/php-cs-fixer.yml: -------------------------------------------------------------------------------- 1 | name: Check & fix styling 2 | 3 | on: [push] 4 | 5 | jobs: 6 | php-cs-fixer: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - name: Checkout code 11 | uses: actions/checkout@v2 12 | with: 13 | ref: ${{ github.head_ref }} 14 | 15 | - name: Run PHP CS Fixer 16 | uses: docker://oskarstark/php-cs-fixer-ga 17 | with: 18 | args: --config=.php_cs.dist.php --allow-risky=yes 19 | 20 | - name: Commit changes 21 | uses: stefanzweifel/git-auto-commit-action@v4 22 | with: 23 | commit_message: Fix styling 24 | -------------------------------------------------------------------------------- /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: "Run Tests" 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | fail-fast: true 10 | matrix: 11 | os: [ubuntu-latest] 12 | php: [8.4, 8.3, 8.2, 8.1, 8.0] 13 | laravel: [11.*, 10.*] 14 | dependency-version: [prefer-stable] 15 | exclude: 16 | - php: 8.4 17 | laravel: 10.* 18 | - php: 8.0 19 | laravel: 11.* 20 | 21 | name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }} - ${{ matrix.os }} 22 | 23 | services: 24 | mysql: 25 | image: mysql:5.7 26 | env: 27 | MYSQL_ALLOW_EMPTY_PASSWORD: yes 28 | MYSQL_DATABASE: nova_tags_field 29 | ports: 30 | - 3306 31 | options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 32 | 33 | steps: 34 | - name: Checkout code 35 | uses: actions/checkout@v2 36 | 37 | - name: Setup PHP 38 | uses: shivammathur/setup-php@v2 39 | with: 40 | php-version: ${{ matrix.php }} 41 | extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo 42 | coverage: none 43 | 44 | - name: Setup problem matchers 45 | run: | 46 | echo "::add-matcher::${{ runner.tool_cache }}/php.json" 47 | echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" 48 | 49 | - name: Install dependencies 50 | run: | 51 | composer config "http-basic.nova.laravel.com" "${{ secrets.NOVA_USERNAME }}" "${{ secrets.NOVA_PASSWORD }}" 52 | composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction --with="laravel/framework:${{ matrix.laravel }}" 53 | env: 54 | COMPOSER_AUTH: ${{ secrets.COMPOSER_AUTH }} 55 | 56 | - name: Execute tests 57 | run: vendor/bin/phpunit 58 | env: 59 | DB_CONNECTION: 'mysql' 60 | DB_USERNAME: 'root' 61 | DB_DATABASE: 'nova_tags_field' 62 | DB_HOST: '127.0.0.1' 63 | DB_PORT: ${{ job.services.mysql.ports[3306] }} 64 | -------------------------------------------------------------------------------- /.github/workflows/update-assets.yml: -------------------------------------------------------------------------------- 1 | name: "Update Assets" 2 | 3 | on: [workflow_dispatch] 4 | 5 | jobs: 6 | update: 7 | name: Update assets 8 | 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v3 14 | 15 | - name: Setup Node 16 16 | uses: actions/setup-node@v2 17 | with: 18 | node-version: '16.x' 19 | 20 | - name: Compile Asset 21 | run: | 22 | yarn install 23 | yarn run production 24 | env: 25 | TAILWIND_MODE: build 26 | 27 | - name: Commit changes 28 | uses: stefanzweifel/git-auto-commit-action@v4 29 | with: 30 | commit_message: Update Assets 31 | -------------------------------------------------------------------------------- /.github/workflows/update-changelog.yml: -------------------------------------------------------------------------------- 1 | name: "Update Changelog" 2 | 3 | on: 4 | release: 5 | types: [released] 6 | 7 | jobs: 8 | update: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v2 14 | with: 15 | ref: main 16 | 17 | - name: Update Changelog 18 | uses: stefanzweifel/changelog-updater-action@v1 19 | with: 20 | latest-version: ${{ github.event.release.name }} 21 | release-notes: ${{ github.event.release.body }} 22 | 23 | - name: Commit updated CHANGELOG 24 | uses: stefanzweifel/git-auto-commit-action@v4 25 | with: 26 | branch: main 27 | commit_message: Update CHANGELOG 28 | file_pattern: CHANGELOG.md 29 | -------------------------------------------------------------------------------- /.php_cs.dist.php: -------------------------------------------------------------------------------- 1 | in([ 5 | __DIR__ . '/src', 6 | __DIR__ . '/tests', 7 | ]) 8 | ->name('*.php') 9 | ->notName('*.blade.php') 10 | ->ignoreDotFiles(true) 11 | ->ignoreVCS(true); 12 | 13 | return (new PhpCsFixer\Config()) 14 | ->setRules([ 15 | '@PSR2' => true, 16 | 'array_syntax' => ['syntax' => 'short'], 17 | 'ordered_imports' => ['sort_algorithm' => 'alpha'], 18 | 'no_unused_imports' => true, 19 | 'not_operator_with_successor_space' => true, 20 | 'trailing_comma_in_multiline' => true, 21 | 'phpdoc_scalar' => true, 22 | 'unary_operator_spaces' => true, 23 | 'binary_operator_spaces' => true, 24 | 'blank_line_before_statement' => [ 25 | 'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'], 26 | ], 27 | 'phpdoc_single_line_var_spacing' => true, 28 | 'phpdoc_var_without_name' => true, 29 | 'class_attributes_separation' => [ 30 | 'elements' => [ 31 | 'method' => 'one', 32 | ], 33 | ], 34 | 'method_argument_space' => [ 35 | 'on_multiline' => 'ensure_fully_multiline', 36 | 'keep_multiple_spaces_after_comma' => true, 37 | ], 38 | 'single_trait_insert_per_statement' => true, 39 | ]) 40 | ->setFinder($finder); 41 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "singleQuote": true, 4 | "tabWidth": 4, 5 | "trailingComma": "es5" 6 | } 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `nova-tags-field` will be documented in this file 4 | 5 | ## 5.0.0 - 2024-12-23 6 | 7 | ### What's Changed 8 | 9 | * update to be compatible to nova 5 by @stephanus-tantiono in https://github.com/spatie/nova-tags-field/pull/186 10 | 11 | ### New Contributors 12 | 13 | * @stephanus-tantiono made their first contribution in https://github.com/spatie/nova-tags-field/pull/186 14 | 15 | **Full Changelog**: https://github.com/spatie/nova-tags-field/compare/4.1.0...5.0.0 16 | 17 | ## 4.1.0 - 2024-12-23 18 | 19 | ### What's Changed 20 | 21 | * Support Nova 5 by @ziming in https://github.com/spatie/nova-tags-field/pull/185 22 | 23 | ### New Contributors 24 | 25 | * @ziming made their first contribution in https://github.com/spatie/nova-tags-field/pull/185 26 | 27 | **Full Changelog**: https://github.com/spatie/nova-tags-field/compare/4.0.8...4.1.0 28 | 29 | ## 4.0.8 - 2024-05-01 30 | 31 | ### What's Changed 32 | 33 | * Fix field styles by @scramatte in https://github.com/spatie/nova-tags-field/pull/183 34 | 35 | ### New Contributors 36 | 37 | * @scramatte made their first contribution in https://github.com/spatie/nova-tags-field/pull/183 38 | 39 | **Full Changelog**: https://github.com/spatie/nova-tags-field/compare/4.0.7...4.0.8 40 | 41 | ## 4.0.7 - 2023-07-27 42 | 43 | ### What's Changed 44 | 45 | - Fix badge for `run-tests` workflow in README.md by @gomzyakov in https://github.com/spatie/nova-tags-field/pull/178 46 | - stop passing null values to explode() by @Gavrisimo in https://github.com/spatie/nova-tags-field/pull/179 47 | 48 | ### New Contributors 49 | 50 | - @gomzyakov made their first contribution in https://github.com/spatie/nova-tags-field/pull/178 51 | - @Gavrisimo made their first contribution in https://github.com/spatie/nova-tags-field/pull/179 52 | 53 | **Full Changelog**: https://github.com/spatie/nova-tags-field/compare/4.0.6...4.0.7 54 | 55 | ## 4.0.6 - 2023-04-24 56 | 57 | ### What's Changed 58 | 59 | - Update Request to NovaRequest by @trippo in https://github.com/spatie/nova-tags-field/pull/177 60 | 61 | ### New Contributors 62 | 63 | - @trippo made their first contribution in https://github.com/spatie/nova-tags-field/pull/177 64 | 65 | **Full Changelog**: https://github.com/spatie/nova-tags-field/compare/4.0.5...4.0.6 66 | 67 | ## 4.0.5 - 2022-11-15 68 | 69 | ### What's Changed 70 | 71 | - Updating routes to use configurable nova:api group. by @tommulroy in https://github.com/spatie/nova-tags-field/pull/170 72 | 73 | ### New Contributors 74 | 75 | - @tommulroy made their first contribution in https://github.com/spatie/nova-tags-field/pull/170 76 | 77 | **Full Changelog**: https://github.com/spatie/nova-tags-field/compare/4.0.4...4.0.5 78 | 79 | ## 4.0.4 - 2022-09-01 80 | 81 | ### What's Changed 82 | 83 | - recompiled with the latest laravel-nova js source instead of npm package by @max-zu in https://github.com/spatie/nova-tags-field/pull/153 84 | - Follow `laravel/nova` Testbench dependencies by @crynobone in https://github.com/spatie/nova-tags-field/pull/165 85 | - Add workflow to automatically update assets, fixes `displayUsing` and reduce `lodash` dist by @crynobone in https://github.com/spatie/nova-tags-field/pull/164 86 | 87 | ### New Contributors 88 | 89 | - @max-zu made their first contribution in https://github.com/spatie/nova-tags-field/pull/153 90 | - @crynobone made their first contribution in https://github.com/spatie/nova-tags-field/pull/165 91 | 92 | **Full Changelog**: https://github.com/spatie/nova-tags-field/compare/4.0.3...4.0.4 93 | 94 | ## 4.0.3 - 2022-04-13 95 | 96 | ## What's Changed 97 | 98 | - added TW classes to tags items and suggested list by @sp4r74cus in https://github.com/spatie/nova-tags-field/pull/151 99 | - Added lodash as a dev dependency by @jtkendall in https://github.com/spatie/nova-tags-field/pull/152 100 | 101 | ## New Contributors 102 | 103 | - @sp4r74cus made their first contribution in https://github.com/spatie/nova-tags-field/pull/151 104 | - @jtkendall made their first contribution in https://github.com/spatie/nova-tags-field/pull/152 105 | 106 | **Full Changelog**: https://github.com/spatie/nova-tags-field/compare/4.0.1...4.0.3 107 | 108 | ## 4.0.2 - 2022-04-10 109 | 110 | ## What's Changed 111 | 112 | - added TW classes to tags items and suggested list by @sp4r74cus in https://github.com/spatie/nova-tags-field/pull/151 113 | 114 | ## New Contributors 115 | 116 | - @sp4r74cus made their first contribution in https://github.com/spatie/nova-tags-field/pull/151 117 | 118 | **Full Changelog**: https://github.com/spatie/nova-tags-field/compare/4.0.1...4.0.2 119 | 120 | ## 4.0.1 - 2022-04-07 121 | 122 | ## What's Changed 123 | 124 | - Dark mode support by @saundefined in https://github.com/spatie/nova-tags-field/pull/149 125 | - Remove unused sass 126 | 127 | ## New Contributors 128 | 129 | - @saundefined made their first contribution in https://github.com/spatie/nova-tags-field/pull/149 130 | 131 | **Full Changelog**: https://github.com/spatie/nova-tags-field/compare/4.0.0...4.0.1 132 | 133 | ## 4.0.0 - 2022-04-07 134 | 135 | ## What's Changed 136 | 137 | - v4 by @freekmurze in https://github.com/spatie/nova-tags-field/pull/148 138 | 139 | **Full Changelog**: https://github.com/spatie/nova-tags-field/compare/3.4.5...4.0.0 140 | 141 | ## 3.4.5 - 2022-03-10 142 | 143 | ## What's Changed 144 | 145 | - Remove help-text component by @LintonAchmad in https://github.com/spatie/nova-tags-field/pull/141 146 | 147 | ## New Contributors 148 | 149 | - @LintonAchmad made their first contribution in https://github.com/spatie/nova-tags-field/pull/141 150 | 151 | **Full Changelog**: https://github.com/spatie/nova-tags-field/compare/3.4.4...3.4.5 152 | 153 | ## 3.4.4 - 2022-01-28 154 | 155 | ## What's Changed 156 | 157 | - Ran yarn command for compiling the components by @aliozgurr in https://github.com/spatie/nova-tags-field/pull/139 158 | 159 | **Full Changelog**: https://github.com/spatie/nova-tags-field/compare/3.4.3...3.4.4 160 | 161 | ## 3.4.3 - 2022-01-25 162 | 163 | ## What's Changed 164 | 165 | - Added help-text component to form field by @aliozgurr in https://github.com/spatie/nova-tags-field/pull/138 166 | 167 | ## New Contributors 168 | 169 | - @aliozgurr made their first contribution in https://github.com/spatie/nova-tags-field/pull/138 170 | 171 | **Full Changelog**: https://github.com/spatie/nova-tags-field/compare/3.4.2...3.4.3 172 | 173 | ## 3.4.2 - 2022-01-08 174 | 175 | ## What's Changed 176 | 177 | - Don't call containing scope when containing input is null by @t1sh0o in https://github.com/spatie/nova-tags-field/pull/137 178 | 179 | ## New Contributors 180 | 181 | - @t1sh0o made their first contribution in https://github.com/spatie/nova-tags-field/pull/137 182 | 183 | **Full Changelog**: https://github.com/spatie/nova-tags-field/compare/3.4.1...3.4.2 184 | 185 | ## 3.4.1 - 2021-12-15 186 | 187 | ## What's Changed 188 | 189 | - remove escape from tag by @leMaur in https://github.com/spatie/nova-tags-field/pull/134 190 | 191 | ## New Contributors 192 | 193 | - @leMaur made their first contribution in https://github.com/spatie/nova-tags-field/pull/134 194 | 195 | **Full Changelog**: https://github.com/spatie/nova-tags-field/compare/3.4.0...3.4.1 196 | 197 | ## 3.4.0 - 2021-10-04 198 | 199 | - add limit option (#132) 200 | 201 | ## 3.3.0 - 2021-08-01 202 | 203 | - add the ability to deselect a single tag (#127) 204 | 205 | ## 3.2.0 - 2021-07-02 206 | 207 | - make field accept optional placeholder (#125) 208 | 209 | ## 3.1.1 -2021-03-25 210 | 211 | - add support for spatie/laravel-tags 4.0 (#110) 212 | 213 | ## 3.1.0 - 2020-12-08 214 | 215 | - allow PHP 8 216 | 217 | ## 3.0.4 - 2020-10-01 218 | 219 | - fix for "Call to a member function map() on null" (#94) 220 | 221 | ## 3.0.3 - 2020-09-12 222 | 223 | - escape tags (#99) 224 | 225 | ## 3.0.2 - 2020-09-09 226 | 227 | - allow spatie/laravel-tags v3 228 | 229 | ## 3.0.1 - 2020-09-09 230 | 231 | - Support Laravel 8.0 232 | 233 | ## 3.0.0 - 2020-03-03 234 | 235 | - Support Laravel 7.0 and Nova 3.0 236 | 237 | ## 2.3.4 - 2020-01-31 238 | 239 | - fix small layout issue 240 | 241 | ## 2.3.3 - 2020-01-31 242 | 243 | - encode unicode characters from inputs (#76) 244 | 245 | ## 2.3.2 - 2019-12-15 246 | 247 | - fix withLinkToTagResource function (#73) 248 | - fix resource url if nova.path to is `/` (#63) 249 | 250 | ## 2.3.1 - 2019-11-13 251 | 252 | - consistent styling (#68) 253 | 254 | ## 2.3.0 - 2019-09-04 255 | 256 | - add support for Laravel 6 257 | 258 | ## 2.2.1 - 2019-08-05 259 | 260 | - sort the tags by name 261 | 262 | ## 2.2.0 - 2019-06-01 263 | 264 | - add `withLinkToTagResource` function 265 | 266 | ## 2.1.0 - 2019-04-10 267 | 268 | - select suggestion on arrowdown if there is exactly one suggestion available 269 | 270 | ## 2.0.1 - 2019-02-27 271 | 272 | - fix deps 273 | 274 | ## 2.0.0 - 2019-02-27 275 | 276 | - support Laravel 5.8 and Nova 2.0 277 | 278 | ## 1.1.3 - 2019-02-10 279 | 280 | - fix for displaying errors 281 | 282 | ## 1.1.2 - 2018-12-10 283 | 284 | - fix `limitSuggestions` 285 | 286 | ## 1.1.1 - 2018-10-10 287 | 288 | - the `Add Tag...` label is now translatable 289 | 290 | ## 1.1.0 - 2018-09-17 291 | 292 | - add limiting options 293 | 294 | ## 1.0.0 - 2018-09-14 295 | 296 | - initial release 297 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Spatie bvba 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A tags field for Nova apps 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/spatie/nova-tags-field.svg?style=flat-square)](https://packagist.org/packages/spatie/nova-tags-field) 4 | ![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/spatie/nova-tags-field/run-tests.yml) 5 | [![Total Downloads](https://img.shields.io/packagist/dt/spatie/nova-tags-field.svg?style=flat-square)](https://packagist.org/packages/spatie/nova-tags-field) 6 | 7 | This package contains a Nova field to add tags to resources. Under the hood it uses the [spatie/laravel-tags](https://docs.spatie.be/laravel-tags) package. 8 | 9 | ## Support us 10 | 11 | [](https://spatie.be/github-ad-click/nova-tags-field) 12 | 13 | We invest a lot of resources into creating [best in class open source packages](https://spatie.be/open-source). You can support us by [buying one of our paid products](https://spatie.be/open-source/support-us). 14 | 15 | We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on [our contact page](https://spatie.be/about-us). We publish all received postcards on [our virtual postcard wall](https://spatie.be/open-source/postcards). 16 | 17 | ## Requirements 18 | 19 | This Nova field requires MySQL 5.7.8 or higher. 20 | 21 | ## Installation 22 | 23 | First you must install [spatie/laravel-tags](https://github.com/spatie/laravel-tags) into your Laravel app. Here are [the installation instructions](https://spatie.be/docs/laravel-tags/v4/installation-and-setup) for that package. 24 | 25 | Next, you can install this package in to a Laravel app that uses [Nova](https://nova.laravel.com) via composer: 26 | 27 | ```bash 28 | composer require spatie/nova-tags-field 29 | ``` 30 | 31 | ## Usage 32 | 33 | To make an Eloquent model taggable just add the `\Spatie\Tags\HasTags` trait to it: 34 | 35 | ```php 36 | class BlogPost extends \Illuminate\Database\Eloquent\Model 37 | { 38 | use \Spatie\Tags\HasTags; 39 | 40 | ... 41 | } 42 | ``` 43 | 44 | Next you can use the `Spatie\TagsField\Tags` field in your Nova resource: 45 | 46 | ```php 47 | namespace App\Nova; 48 | 49 | use Spatie\TagsField\Tags; 50 | 51 | class BlogPost extends Resource 52 | { 53 | // ... 54 | 55 | public function fields(NovaRequest $request) 56 | { 57 | return [ 58 | // ... 59 | 60 | Tags::make('Tags'), 61 | 62 | // ... 63 | ]; 64 | } 65 | } 66 | ``` 67 | 68 | Now you can view and add tags on the blog posts screen in your Nova app. All tags will be saved in the `tags` table. 69 | 70 | ## Limiting suggestions 71 | 72 | By default a tags field will display a maximum of 5 suggestions when typing into it. If you don't want to display any suggestions, tag on `withoutSuggestions()`. 73 | 74 | ```php 75 | Tags::make('Tags')->withoutSuggestions(), 76 | ``` 77 | 78 | You can change the number of suggestions with `limitSuggestions()`. 79 | 80 | ```php 81 | Tags::make('Tags')->limitSuggestions($maxNumberOfSuggestions), 82 | ``` 83 | 84 | ## Limiting tags 85 | 86 | By default a tags field will let you add unlimited tags. You can limit tags with `limit()`. 87 | 88 | ```php 89 | Tags::make('Tags')->limit($maxNumberOfTags), 90 | ``` 91 | 92 | ## Using types 93 | 94 | The [underlying tags package](https://github.com/spatie/laravel-tags) has support for [tag types](https://docs.spatie.be/laravel-tags/v2/advanced-usage/using-types). To make your tags field save tags of a certain type just tack on the name of type when adding the field to your Nova resource. 95 | 96 | ```php 97 | // in your Nova resource 98 | 99 | public function fields(NovaRequest $request) 100 | { 101 | return [ 102 | // ... 103 | 104 | Tags::make('Tags')->type('my-special-type'), 105 | 106 | // ... 107 | ]; 108 | } 109 | ``` 110 | 111 | ## Allowing only one tag 112 | 113 | If the user is only allowed to select one tag for your resource you can call the `single` method. 114 | 115 | ```php 116 | // in your Nova resource 117 | 118 | public function fields(NovaRequest $request) 119 | { 120 | return [ 121 | // ... 122 | 123 | Tags::make('Tags')->single(), 124 | 125 | // ... 126 | ]; 127 | } 128 | ``` 129 | 130 | The field will be rendered as a select form element. It will be populated by the names of the tags already saved. 131 | 132 | If you want to be able to deselect a tag, you can call the `canBeDeselected` method. 133 | 134 | ```php 135 | // in your Nova resource 136 | 137 | public function fields(NovaRequest $request) 138 | { 139 | return [ 140 | // ... 141 | 142 | Tags::make('Tags')->single()->canBeDeselected(), 143 | 144 | // ... 145 | ]; 146 | } 147 | ``` 148 | 149 | ## Use a different placeholder 150 | 151 | You can use the `withMeta` method [documented here](https://nova.laravel.com/docs/3.0/customization/fields.html#field-options) to change the placeholder from the default, which is `__('Add tags...')`. 152 | 153 | ```php 154 | // in your Nova resource 155 | 156 | public function fields(NovaRequest $request) 157 | { 158 | return [ 159 | // ... 160 | 161 | Tags::make('Categories', 'tags')->withMeta(['placeholder' => 'Add categories...']), 162 | 163 | // ... 164 | ]; 165 | } 166 | ``` 167 | 168 | ## Working with tags 169 | 170 | For more info on how to work with the saved tags, head over to [the docs of spatie/laravel-tags](https://docs.spatie.be/laravel-tags/). 171 | 172 | ## Administering tags in Nova 173 | 174 | If you want to perform crud actions on the save tags, just create a Nova resource for it. Here's an example. 175 | 176 | ```php 177 | namespace App\Nova; 178 | 179 | use Laravel\Nova\Fields\Text; 180 | use Laravel\Nova\Http\Requests\NovaRequest; 181 | use Spatie\Tags\Tag as TagModel; 182 | 183 | class Tag extends Resource 184 | { 185 | public static $model = TagModel::class; 186 | 187 | public static $title = 'name'; 188 | 189 | public static $search = [ 190 | 'name', 191 | ]; 192 | 193 | public function fields(NovaRequest $request) 194 | { 195 | return [ 196 | Text::make('Name')->sortable(), 197 | ]; 198 | } 199 | } 200 | ``` 201 | 202 | ### Show tags with a link to a Nova resource 203 | 204 | When creating the field, you can use the `withLinkToTagResource` method. 205 | Example: 206 | ```php 207 | Tags::make('Tags')->withLinkToTagResource() // The resource App\Nova\Tag will be used 208 | Tags::make('Tags')->withLinkToTagResource(\Custom\CustomTag::class) // The resource \Custom\CustomTag will be used 209 | ``` 210 | 211 | ### Testing 212 | 213 | ``` bash 214 | composer test 215 | ``` 216 | 217 | ### Changelog 218 | 219 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 220 | 221 | ## Contributing 222 | 223 | Please see [CONTRIBUTING](https://github.com/spatie/.github/blob/main/CONTRIBUTING.md) for details. 224 | 225 | ### Security 226 | 227 | If you've found a bug regarding security please mail [security@spatie.be](mailto:security@spatie.be) instead of using the issue tracker. 228 | 229 | ## Credits 230 | 231 | - [Freek Van der Herten](https://github.com/freekmurze) 232 | 233 | The Vue components that render the tags are based upon the tag Vue components created by [Adam Wathan](https://twitter.com/adamwathan) as shown in [his excellent Advanced Vue Component Design course](https://adamwathan.me/advanced-vue-component-design/). 234 | 235 | ## License 236 | 237 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 238 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spatie/nova-tags-field", 3 | "description": "A tags field for Nova apps", 4 | "keywords": [ 5 | "laravel", 6 | "nova" 7 | ], 8 | "repositories": [ 9 | { 10 | "type": "composer", 11 | "url": "https://nova.laravel.com" 12 | } 13 | ], 14 | "homepage": "https://github.com/spatie/nova-tags-field", 15 | "license": "MIT", 16 | "authors": [ 17 | { 18 | "name": "Freek Van der Herten", 19 | "email": "freek@spatie.be", 20 | "role": "Developer" 21 | } 22 | ], 23 | "require": { 24 | "php": "^8.0", 25 | "laravel/nova": "^5.0.6", 26 | "spatie/laravel-tags": "^4.0" 27 | }, 28 | "require-dev": { 29 | "laravel/nova-devtool": "^1.2", 30 | "orchestra/testbench": "^8.30|^9.8" 31 | }, 32 | "autoload": { 33 | "psr-4": { 34 | "Spatie\\TagsField\\": "src/" 35 | } 36 | }, 37 | "autoload-dev": { 38 | "psr-4": { 39 | "Spatie\\TagsField\\Tests\\": "tests", 40 | "Workbench\\App\\": "workbench/app/", 41 | "Workbench\\Database\\Factories\\": "workbench/database/factories/", 42 | "Workbench\\Database\\Seeders\\": "workbench/database/seeders/" 43 | } 44 | }, 45 | "extra": { 46 | "laravel": { 47 | "providers": [ 48 | "Spatie\\TagsField\\TagsFieldServiceProvider" 49 | ] 50 | } 51 | }, 52 | "config": { 53 | "sort-packages": true 54 | }, 55 | "minimum-stability": "dev", 56 | "prefer-stable": true, 57 | "scripts": { 58 | "post-autoload-dump": [ 59 | "@clear", 60 | "@prepare" 61 | ], 62 | "clear": "@php vendor/bin/testbench package:purge-skeleton --ansi", 63 | "prepare": "@php vendor/bin/testbench package:discover --ansi", 64 | "build": "@php vendor/bin/testbench workbench:build --ansi", 65 | "serve": [ 66 | "Composer\\Config::disableProcessTimeout", 67 | "@build", 68 | "@php vendor/bin/testbench serve --ansi" 69 | ], 70 | "test": [ 71 | "@clear", 72 | "@php vendor/bin/testbench package:create-sqlite-db", 73 | "@php vendor/bin/phpunit" 74 | ] 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /dist/js/field.js: -------------------------------------------------------------------------------- 1 | (()=>{var t={873:(t,e,n)=>{var o=n(325).Symbol;t.exports=o},552:(t,e,n)=>{var o=n(873),r=n(659),i=n(969),a=o?o.toStringTag:void 0;t.exports=function(t){return null==t?void 0===t?"[object Undefined]":"[object Null]":a&&a in Object(t)?r(t):i(t)}},128:(t,e,n)=>{var o=n(800),r=/^\s+/;t.exports=function(t){return t?t.slice(0,o(t)+1).replace(r,""):t}},840:(t,e,n)=>{var o="object"==typeof n.g&&n.g&&n.g.Object===Object&&n.g;t.exports=o},659:(t,e,n)=>{var o=n(873),r=Object.prototype,i=r.hasOwnProperty,a=r.toString,s=o?o.toStringTag:void 0;t.exports=function(t){var e=i.call(t,s),n=t[s];try{t[s]=void 0;var o=!0}catch(t){}var r=a.call(t);return o&&(e?t[s]=n:delete t[s]),r}},969:t=>{var e=Object.prototype.toString;t.exports=function(t){return e.call(t)}},325:(t,e,n)=>{var o=n(840),r="object"==typeof self&&self&&self.Object===Object&&self,i=o||r||Function("return this")();t.exports=i},800:t=>{var e=/\s/;t.exports=function(t){for(var n=t.length;n--&&e.test(t.charAt(n)););return n}},221:(t,e,n)=>{var o=n(805),r=n(124),i=n(374),a=Math.max,s=Math.min;t.exports=function(t,e,n){var u,l,c,p,d,f,g=0,m=!1,h=!1,v=!0;if("function"!=typeof t)throw new TypeError("Expected a function");function y(e){var n=u,o=l;return u=l=void 0,g=e,p=t.apply(o,n)}function b(t){var n=t-f;return void 0===f||n>=e||n<0||h&&t-g>=c}function T(){var t=r();if(b(t))return k(t);d=setTimeout(T,function(t){var n=e-(t-f);return h?s(n,c-(t-g)):n}(t))}function k(t){return d=void 0,v&&u?y(t):(u=l=void 0,p)}function x(){var t=r(),n=b(t);if(u=arguments,l=this,f=t,n){if(void 0===d)return function(t){return g=t,d=setTimeout(T,e),m?y(t):p}(f);if(h)return clearTimeout(d),d=setTimeout(T,e),y(f)}return void 0===d&&(d=setTimeout(T,e)),p}return e=i(e)||0,o(n)&&(m=!!n.leading,c=(h="maxWait"in n)?a(i(n.maxWait)||0,e):c,v="trailing"in n?!!n.trailing:v),x.cancel=function(){void 0!==d&&clearTimeout(d),g=0,u=f=l=d=void 0},x.flush=function(){return void 0===d?p:k(r())},x}},805:t=>{t.exports=function(t){var e=typeof t;return null!=t&&("object"==e||"function"==e)}},346:t=>{t.exports=function(t){return null!=t&&"object"==typeof t}},394:(t,e,n)=>{var o=n(552),r=n(346);t.exports=function(t){return"symbol"==typeof t||r(t)&&"[object Symbol]"==o(t)}},124:(t,e,n)=>{var o=n(325);t.exports=function(){return o.Date.now()}},350:(t,e,n)=>{var o=n(221),r=n(805);t.exports=function(t,e,n){var i=!0,a=!0;if("function"!=typeof t)throw new TypeError("Expected a function");return r(n)&&(i="leading"in n?!!n.leading:i,a="trailing"in n?!!n.trailing:a),o(t,e,{leading:i,maxWait:e,trailing:a})}},374:(t,e,n)=>{var o=n(128),r=n(805),i=n(394),a=/^[-+]0x[0-9a-f]+$/i,s=/^0b[01]+$/i,u=/^0o[0-7]+$/i,l=parseInt;t.exports=function(t){if("number"==typeof t)return t;if(i(t))return NaN;if(r(t)){var e="function"==typeof t.valueOf?t.valueOf():t;t=r(e)?e+"":e}if("string"!=typeof t)return 0===t?t:+t;t=o(t);var n=s.test(t);return n||u.test(t)?l(t.slice(2),n?2:8):a.test(t)?NaN:+t}},262:(t,e)=>{"use strict";e.A=(t,e)=>{const n=t.__vccOpts||t;for(const[t,o]of e)n[t]=o;return n}}},e={};function n(o){var r=e[o];if(void 0!==r)return r.exports;var i=e[o]={exports:{}};return t[o](i,i.exports,n),i.exports}n.n=t=>{var e=t&&t.__esModule?()=>t.default:()=>t;return n.d(e,{a:e}),e},n.d=(t,e)=>{for(var o in e)n.o(e,o)&&!n.o(t,o)&&Object.defineProperty(t,o,{enumerable:!0,get:e[o]})},n.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(t){if("object"==typeof window)return window}}(),n.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e),(()=>{"use strict";const t=Vue;var e=["innerHTML"];const o=LaravelNova;function r(t){return r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},r(t)}const i={mixins:[o.FieldValue],props:["resourceName","field"],computed:{tagFieldValue:function(){var t=this.fieldValue;return"object"===r(t)?t.join(", "):t}}};var a=n(262);const s=(0,a.A)(i,[["render",function(n,o,r,i,a,s){return(0,t.openBlock)(),(0,t.createElementBlock)("div",{class:"py-3 tags-index-field",innerHTML:s.tagFieldValue},null,8,e)}]]);var u=["innerHTML"];function l(t){return l="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},l(t)}const c={mixins:[o.FieldValue],props:["index","resource","resourceName","resourceId","field"],computed:{tagFieldValue:function(){var t=this.fieldValue;return"object"===l(t)?t.join(", "):t}}},p=(0,a.A)(c,[["render",function(e,n,o,r,i,a){var s=(0,t.resolveComponent)("PanelItem");return(0,t.openBlock)(),(0,t.createBlock)(s,{index:o.index,field:o.field},{value:(0,t.withCtx)((function(){return[(0,t.createElementVNode)("p",{innerHTML:a.tagFieldValue},null,8,u)]})),_:1},8,["index","field"])}]]);var d=["onClick"],f=["placeholder"],g={key:0,class:"flex mt-2 p-0"},m=["onClick"];var h=n(350),v=n.n(h);function y(t){return function(t){if(Array.isArray(t))return b(t)}(t)||function(t){if("undefined"!=typeof Symbol&&null!=t[Symbol.iterator]||null!=t["@@iterator"])return Array.from(t)}(t)||function(t,e){if(t){if("string"==typeof t)return b(t,e);var n={}.toString.call(t).slice(8,-1);return"Object"===n&&t.constructor&&(n=t.constructor.name),"Map"===n||"Set"===n?Array.from(t):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?b(t,e):void 0}}(t)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function b(t,e){(null==e||e>t.length)&&(e=t.length);for(var n=0,o=Array(e);n 2 | 3 | 6 | 7 | 8 | 9 | 26 | -------------------------------------------------------------------------------- /resources/js/components/Nova/FormField.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 60 | -------------------------------------------------------------------------------- /resources/js/components/Nova/IndexField.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 22 | -------------------------------------------------------------------------------- /resources/js/components/Tags/MultiTagsInput.vue: -------------------------------------------------------------------------------- 1 | 60 | 61 | 117 | -------------------------------------------------------------------------------- /resources/js/components/Tags/SingleTagsInput.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 51 | -------------------------------------------------------------------------------- /resources/js/components/Tags/TagsInput.vue: -------------------------------------------------------------------------------- 1 | 182 | -------------------------------------------------------------------------------- /resources/js/field.js: -------------------------------------------------------------------------------- 1 | import IndexField from './components/Nova/IndexField'; 2 | import DetailField from './components/Nova/DetailField'; 3 | import FormField from './components/Nova/FormField'; 4 | 5 | Nova.booting(app => { 6 | app.component('index-nova-tags-field', IndexField); 7 | app.component('detail-nova-tags-field', DetailField); 8 | app.component('form-nova-tags-field', FormField); 9 | }); 10 | -------------------------------------------------------------------------------- /routes/api.php: -------------------------------------------------------------------------------- 1 | query(); 14 | 15 | if ($request->input('filter.containing') !== null) { 16 | $query->containing($request['filter']['containing']); 17 | } 18 | 19 | if ($request->has('filter.type')) { 20 | $query->where('type', $request['filter']['type']); 21 | } 22 | 23 | if ($request->has('limit')) { 24 | $query->limit($request['limit']); 25 | } 26 | 27 | $sorted = $query->get()->sortBy(fn (Tag $tag) => strtolower($tag->name))->values(); 28 | 29 | return $sorted->map(fn (Tag $tag) => $tag->name); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Tags.php: -------------------------------------------------------------------------------- 1 | multiple(); 20 | } 21 | 22 | public function type(string $type) 23 | { 24 | return $this->withMeta(['type' => $type]); 25 | } 26 | 27 | public function withLinkToTagResource(string $tagResource = null, string $class = 'no-underline dim text-primary font-bold') 28 | { 29 | if (is_null($tagResource)) { 30 | $tagResource = 'App\Nova\Tag'; 31 | } 32 | 33 | $uriKey = $tagResource::uriKey(); 34 | 35 | return $this->displayUsing(function ($value, $resource, $attribute) use ($class, $uriKey) { 36 | $tags = $resource->tags()->where(function ($query) { 37 | if (Arr::has($this->meta(), 'type')) { 38 | $query->where('type', $this->meta()['type']); 39 | } 40 | })->get(); 41 | 42 | return $tags->map(function (Tag $tag) use ($class, $uriKey) { 43 | $href = rtrim(Nova::path(), '/').'/resources/'.$uriKey.'/'.$tag->id; 44 | 45 | return "$tag->name"; 46 | }); 47 | }); 48 | } 49 | 50 | public function multiple(bool $multiple = true) 51 | { 52 | $this->withMeta([ 53 | 'multiple' => $multiple, 54 | 'suggestionLimit' => 5, 55 | 'limit' => null, 56 | ]); 57 | 58 | if (! $this->meta['multiple']) { 59 | $this->doNotLimitSuggestions(); 60 | } 61 | 62 | return $this; 63 | } 64 | 65 | public function single(bool $single = true) 66 | { 67 | $this->withMeta(['multiple' => ! $single]); 68 | 69 | if (! $this->meta['multiple']) { 70 | $this->doNotLimitSuggestions(); 71 | } 72 | 73 | return $this; 74 | } 75 | 76 | public function canBeDeselected() 77 | { 78 | if ($this->meta['multiple']) { 79 | return $this; 80 | } 81 | 82 | $this->withMeta(['canBeDeselected' => true]); 83 | 84 | return $this; 85 | } 86 | 87 | public function withoutSuggestions() 88 | { 89 | return $this->limitSuggestions(0); 90 | } 91 | 92 | public function limitSuggestions(int $maxNumberOfSuggestions) 93 | { 94 | return $this->withMeta(['suggestionLimit' => $maxNumberOfSuggestions]); 95 | } 96 | 97 | public function doNotLimitSuggestions() 98 | { 99 | return $this->limitSuggestions(9999); 100 | } 101 | 102 | public function limit(?int $limit) 103 | { 104 | $this->withMeta(['limit' => $limit]); 105 | 106 | return $this; 107 | } 108 | 109 | protected function fillAttributeFromRequest(NovaRequest $request, $requestAttribute, $model, $attribute) 110 | { 111 | $requestValue = $request[$requestAttribute]; 112 | $tagNames = is_null($requestValue) ? [] : array_filter(explode('-----', $requestValue)); 113 | 114 | $class = get_class($model); 115 | 116 | $class::saved(function ($model) use ($tagNames) { 117 | $model->syncTagsWithType($tagNames, $this->meta()['type'] ?? null); 118 | }); 119 | } 120 | 121 | public function resolveAttribute($resource, string $attribute): mixed 122 | { 123 | $tags = $resource->tags; 124 | 125 | if (Arr::has($this->meta(), 'type')) { 126 | $tags = $tags->where('type', $this->meta()['type']); 127 | } 128 | 129 | return $tags->map(function (Tag $tag) { 130 | return $tag->name; 131 | })->values(); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/TagsFieldServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->booted(function () { 20 | $this->routes(); 21 | }); 22 | } 23 | 24 | protected function routes() 25 | { 26 | if ($this->app->routesAreCached()) { 27 | return; 28 | } 29 | 30 | Route::middleware('nova:api') 31 | ->prefix('nova-vendor/spatie/nova-tags-field') 32 | ->group(__DIR__.'/../routes/api.php'); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /testbench.yaml: -------------------------------------------------------------------------------- 1 | laravel: '@testbench' 2 | 3 | providers: 4 | - Laravel\Nova\NovaServiceProvider 5 | - Laravel\Nova\NovaCoreServiceProvider 6 | - Workbench\App\Providers\NovaServiceProvider 7 | - Spatie\Tags\TagsServiceProvider 8 | - Spatie\TagsField\TagsFieldServiceProvider 9 | 10 | migrations: 11 | - workbench/database/migrations 12 | 13 | seeders: 14 | - Workbench\Database\Seeders\DatabaseSeeder 15 | 16 | workbench: 17 | start: /nova 18 | build: 19 | - package:discover 20 | - asset-publish 21 | - create-sqlite-db 22 | - db:wipe 23 | - migrate:refresh 24 | assets: 25 | - nova-assets 26 | sync: 27 | - from: storage 28 | to: workbench/storage 29 | reverse: true 30 | 31 | purge: 32 | directories: 33 | - lang/* 34 | - public/vendor/* 35 | -------------------------------------------------------------------------------- /webpack.mix.js: -------------------------------------------------------------------------------- 1 | let mix = require('laravel-mix') 2 | mix.extend('nova', new require('laravel-nova-devtool')) 3 | 4 | mix.setPublicPath('dist') 5 | .js('resources/js/field.js', 'js') 6 | .vue({version: 3 }) 7 | .nova('spatie/nova-tags-field'); 8 | -------------------------------------------------------------------------------- /workbench/app/Models/User.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | protected $fillable = [ 22 | 'name', 23 | 'email', 24 | 'password', 25 | ]; 26 | 27 | /** 28 | * The attributes that should be hidden for serialization. 29 | * 30 | * @var array 31 | */ 32 | protected $hidden = [ 33 | 'password', 34 | 'remember_token', 35 | ]; 36 | 37 | /** 38 | * The attributes that should be cast. 39 | * 40 | * @var array 41 | */ 42 | protected $casts = [ 43 | 'email_verified_at' => 'datetime', 44 | 'password' => 'hashed', 45 | ]; 46 | } 47 | -------------------------------------------------------------------------------- /workbench/app/Nova/Resource.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | public static $model = \Spatie\Tags\Tag::class; 17 | 18 | /** 19 | * The single value that should be used to represent the resource when being displayed. 20 | * 21 | * @var string 22 | */ 23 | public static $title = 'name'; 24 | 25 | /** 26 | * The columns that should be searched. 27 | * 28 | * @var array 29 | */ 30 | public static $search = [ 31 | 'name', 32 | ]; 33 | 34 | /** 35 | * Get the fields displayed by the resource. 36 | * 37 | * @return array 38 | */ 39 | public function fields(NovaRequest $request): array 40 | { 41 | return [ 42 | Text::make('Name')->sortable(), 43 | ]; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /workbench/app/Nova/User.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | public static $model = \Workbench\App\Models\User::class; 23 | 24 | /** 25 | * The single value that should be used to represent the resource when being displayed. 26 | * 27 | * @var string 28 | */ 29 | public static $title = 'name'; 30 | 31 | /** 32 | * The columns that should be searched. 33 | * 34 | * @var array 35 | */ 36 | public static $search = [ 37 | 'id', 'name', 'email', 38 | ]; 39 | 40 | /** 41 | * Get the fields displayed by the resource. 42 | * 43 | * @return array 44 | */ 45 | public function fields(NovaRequest $request): array 46 | { 47 | return [ 48 | ID::make()->sortable(), 49 | 50 | Text::make('Name') 51 | ->sortable() 52 | ->rules('required', 'max:255'), 53 | 54 | Text::make('Email') 55 | ->sortable() 56 | ->rules('required', 'email', 'max:254') 57 | ->creationRules('unique:users,email') 58 | ->updateRules('unique:users,email,{{resourceId}}'), 59 | 60 | Password::make('Password') 61 | ->onlyOnForms() 62 | ->creationRules($this->passwordRules()) 63 | ->updateRules($this->optionalPasswordRules()), 64 | 65 | Tags::make('Tags'), 66 | ]; 67 | } 68 | 69 | /** 70 | * Get the cards available for the request. 71 | * 72 | * @return array 73 | */ 74 | public function cards(NovaRequest $request): array 75 | { 76 | return []; 77 | } 78 | 79 | /** 80 | * Get the filters available for the resource. 81 | * 82 | * @return array 83 | */ 84 | public function filters(NovaRequest $request): array 85 | { 86 | return []; 87 | } 88 | 89 | /** 90 | * Get the lenses available for the resource. 91 | * 92 | * @return array 93 | */ 94 | public function lenses(NovaRequest $request): array 95 | { 96 | return []; 97 | } 98 | 99 | /** 100 | * Get the actions available for the resource. 101 | * 102 | * @return array 103 | */ 104 | public function actions(NovaRequest $request): array 105 | { 106 | return []; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /workbench/app/Providers/NovaServiceProvider.php: -------------------------------------------------------------------------------- 1 | features([ 29 | Features::updatePasswords(), 30 | // Features::emailVerification(), 31 | // Features::twoFactorAuthentication(['confirm' => true, 'confirmPassword' => true]), 32 | ]) 33 | ->register(); 34 | } 35 | 36 | /** 37 | * Register the Nova routes. 38 | */ 39 | protected function routes(): void 40 | { 41 | Nova::routes() 42 | ->withAuthenticationRoutes(default: true) 43 | ->withPasswordResetRoutes() 44 | ->withoutEmailVerificationRoutes() 45 | ->register(); 46 | } 47 | 48 | /** 49 | * Register the Nova gate. 50 | * 51 | * This gate determines who can access Nova in non-local environments. 52 | */ 53 | protected function gate(): void 54 | { 55 | Gate::define('viewNova', function ($user) { 56 | return true; 57 | }); 58 | } 59 | 60 | /** 61 | * Get the dashboards that should be listed in the Nova sidebar. 62 | * 63 | * @return array 64 | */ 65 | protected function dashboards(): array 66 | { 67 | return [ 68 | new \Laravel\Nova\Dashboards\Main, 69 | ]; 70 | } 71 | 72 | /** 73 | * Get the tools that should be listed in the Nova sidebar. 74 | * 75 | * @return array 76 | */ 77 | public function tools(): array 78 | { 79 | return []; 80 | } 81 | 82 | /** 83 | * Register the application's Nova resources. 84 | */ 85 | protected function resources(): void 86 | { 87 | Nova::resourcesInWorkbench(); 88 | } 89 | 90 | /** 91 | * Register any application services. 92 | */ 93 | public function register(): void 94 | { 95 | parent::register(); 96 | 97 | // 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /workbench/bootstrap/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spatie/nova-tags-field/5b8c6e0928e708a8da291acd840993b282a297de/workbench/bootstrap/.gitkeep -------------------------------------------------------------------------------- /workbench/database/factories/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spatie/nova-tags-field/5b8c6e0928e708a8da291acd840993b282a297de/workbench/database/factories/.gitkeep -------------------------------------------------------------------------------- /workbench/database/factories/UserFactory.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class UserFactory extends Factory 16 | { 17 | /** 18 | * The current password being used by the factory. 19 | */ 20 | protected static ?string $password; 21 | 22 | /** 23 | * The name of the factory's corresponding model. 24 | * 25 | * @var class-string 26 | */ 27 | protected $model = User::class; 28 | 29 | /** 30 | * Define the model's default state. 31 | * 32 | * @return array 33 | */ 34 | public function definition(): array 35 | { 36 | return [ 37 | 'name' => fake()->name(), 38 | 'email' => fake()->unique()->safeEmail(), 39 | 'email_verified_at' => now(), 40 | 'password' => static::$password ??= Hash::make('password'), 41 | 'remember_token' => Str::random(10), 42 | ]; 43 | } 44 | 45 | /** 46 | * Indicate that the model's email address should be unverified. 47 | */ 48 | public function unverified(): static 49 | { 50 | return $this->state(fn (array $attributes) => [ 51 | 'email_verified_at' => null, 52 | ]); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /workbench/database/migrations/2024_12_26_011907_create_tag_tables.php: -------------------------------------------------------------------------------- 1 | id(); 13 | 14 | $table->json('name'); 15 | $table->json('slug'); 16 | $table->string('type')->nullable(); 17 | $table->integer('order_column')->nullable(); 18 | 19 | $table->timestamps(); 20 | }); 21 | 22 | Schema::create('taggables', function (Blueprint $table) { 23 | $table->foreignId('tag_id')->constrained()->cascadeOnDelete(); 24 | 25 | $table->morphs('taggable'); 26 | 27 | $table->unique(['tag_id', 'taggable_id', 'taggable_type']); 28 | }); 29 | } 30 | 31 | public function down(): void 32 | { 33 | Schema::dropIfExists('taggables'); 34 | Schema::dropIfExists('tags'); 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /workbench/database/seeders/DatabaseSeeder.php: -------------------------------------------------------------------------------- 1 | create([ 19 | 'name' => 'Laravel Nova', 20 | 'email' => 'nova@laravel.com', 21 | ]); 22 | 23 | UserFactory::new()->times(10)->create(); 24 | 25 | Tag::findOrCreateFromString('one'); 26 | Tag::findOrCreateFromString('another-one'); 27 | Tag::findOrCreateFromString('Another-ONE-with-different-casing'); 28 | Tag::findOrCreateFromString('two'); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /workbench/resources/views/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spatie/nova-tags-field/5b8c6e0928e708a8da291acd840993b282a297de/workbench/resources/views/.gitkeep -------------------------------------------------------------------------------- /workbench/routes/console.php: -------------------------------------------------------------------------------- 1 | comment(Inspiring::quote()); 8 | })->purpose('Display an inspiring quote')->hourly(); 9 | -------------------------------------------------------------------------------- /workbench/routes/web.php: -------------------------------------------------------------------------------- 1 |