Repository: optimistdigital/nova-settings Branch: main Commit: b0ad22ac34c8 Files: 58 Total size: 81.9 KB Directory structure: gitextract_ete0td8v/ ├── .editorconfig ├── .gitattributes ├── .github/ │ └── FUNDING.yml ├── .gitignore ├── .prettierrc ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json ├── config/ │ └── nova-settings.php ├── database/ │ └── migrations/ │ ├── 2019_08_13_000000_create_nova_settings_table.php │ └── 2021_02_15_000000_update_nova_settings_value_column.php ├── dist/ │ ├── js/ │ │ ├── entry.js │ │ └── entry.js.LICENSE.txt │ └── mix-manifest.json ├── lang/ │ ├── ar.json │ ├── de.json │ ├── en.json │ ├── es.json │ ├── et.json │ ├── fa.json │ ├── fr.json │ ├── it.json │ ├── nl.json │ ├── pt-BR.json │ ├── ru.json │ ├── sk.json │ ├── tr.json │ └── uz.json ├── package.json ├── phpunit.dusk.xml.dist ├── phpunit.xml.dist ├── resources/ │ └── js/ │ ├── entry.js │ └── views/ │ └── Settings.vue ├── routes/ │ └── api.php ├── src/ │ ├── Http/ │ │ ├── Controllers/ │ │ │ └── SettingsController.php │ │ └── Middleware/ │ │ ├── Authorize.php │ │ └── SettingsPathExists.php │ ├── Models/ │ │ └── Settings.php │ ├── Nova/ │ │ └── Resources/ │ │ └── Settings.php │ ├── NovaSettings.php │ ├── NovaSettingsCacheStore.php │ ├── NovaSettingsInMemoryStore.php │ ├── NovaSettingsNoCacheStore.php │ ├── NovaSettingsServiceProvider.php │ ├── NovaSettingsStore.php │ └── helpers.php ├── testbench.yaml ├── tests/ │ ├── Browser/ │ │ └── DetailTest.php │ ├── DuskTestCase.php │ ├── Feature/ │ │ ├── NavigationTest.php │ │ ├── SettingsCastTest.php │ │ ├── SettingsHelpersTest.php │ │ ├── SettingsRetrieveTest.php │ │ └── SettingsSaveTest.php │ ├── IntegrationTestCase.php │ └── bootstrap.php └── webpack.mix.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ root = true [*] charset = utf-8 end_of_line = lf insert_final_newline = true indent_style = space indent_size = 2 trim_trailing_whitespace = true [*.php] charset = utf-8 end_of_line = lf insert_final_newline = true indent_style = space indent_size = 4 trim_trailing_whitespace = true ================================================ FILE: .gitattributes ================================================ # Path-based git attributes # https://www.kernel.org/pub/software/scm/git/docs/gitattributes.html # Ignore all test and documentation with "export-ignore". /.gitattributes export-ignore /.gitignore export-ignore /.travis.yml export-ignore /phpunit.xml.dist export-ignore /.scrutinizer.yml export-ignore /.styleci.yml export-ignore /tests export-ignore /.editorconfig export-ignore /docs export-ignore ================================================ FILE: .github/FUNDING.yml ================================================ github: outl1ne ================================================ FILE: .gitignore ================================================ /.idea /vendor /node_modules composer.phar composer.lock phpunit.xml .phpunit.result.cache .DS_Store Thumbs.db .env.dusk tests/Browser/console tests/Browser/screenshots auth.json ================================================ FILE: .prettierrc ================================================ { "printWidth": 120, "singleQuote": true, "trailingComma": "es5", "arrowParens": "avoid" } ================================================ FILE: CHANGELOG.md ================================================ # Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [6.0.2] - 01-09-2025 ### Added - Added support for Markdown field preview ## [6.0.0] - 18-12-2024 ### Added - Nova 5 initial support ## [5.3.0] - 18-12-2024 ### Added - Added hugely improved caching store with options (thanks to [@manuel-watchenterprise](https://github.com/manuel-watchenterprise)) ## [5.2.4] - 03-02-2024 ### Added - Added Spanish localization (thanks to [@dualklip](https://github.com/dualklip)) ## [5.2.3] - 09-10-2023 ### Fixed - Fixed save button missing in Nova 4.28 (thanks to [@alancolant](https://github.com/alancolant)) ## [5.2.2] - 09-10-2023 ### Fixed - Fixed casting of date and datetime objects when passing them into field ([see issue](https://github.com/outl1ne/nova-settings/issues/172)) ## [5.2.1] - 10-08-2023 ### Added - Added Nova ->domain() support to routes (thanks to [@RonMelkhior](https://github.com/RonMelkhior)) ### Changed - Fixed null values not being persisted (thanks to [@Senexis](https://github.com/Senexis)) ## [5.2.0] - 29-06-2023 ### Added - Added Nova 4.26 support (thanks to [@puzzledmonkey](https://github.com/puzzledmonkey)) ## [5.1.0] - 20-03-2023 ### Added - Added Slovak language (thanks to [@wamesro](https://github.com/wamesro)) - Added resource-loaded and resource-updated Nova events ### Changed - Allow encoding of JsonSerializable objects (thanks to [@miagg](https://github.com/miagg)) - Settings submenu is now hidden if there is only 1 menu element (thanks to [@johnpuddephatt](https://github.com/johnpuddephatt)) - Fixed image deletion when the image is inside a \Nova\Panel or \Eminiarts\Tabs (thanks to [@marttinnotta](https://github.com/marttinnotta)) - Updated packages ## [5.0.8] - 04-01-2023 ### Changed - Fixed `nova_get_settings()` not casting as expected ## [5.0.7] - 04-01-2023 ### Added - Added dusk identifier to update button (thanks to [@chrillep](https://github.com/chrillep)) ### Changed - Fixed `nova_get_settings()` not working as expected with default values - Updated packages ## [5.0.6] - 21-10-2022 ### Changed - Added translations for French language (thanks to [@shaffe-fr](https://github.com/shaffe-fr)) ## [5.0.5] - 08-09-2022 ### Changed - Fixed help text not rendering (thanks to [@mberatsanli](https://github.com/mberatsanli)) ## [5.0.4] - 19-08-2022 ### Changed - Fixed nova-tabs support (thanks to [@Gertiozuni](https://github.com/Gertiozuni)) - Updated packages ## [5.0.3] - 19-07-2022 ### Changed - Fixed File and Image fields not deleting files from disk ## [5.0.2] - 24-05-2022 ### Added - Added Turkish translations (thanks to [@suleymanozev](https://github.com/suleymanozev)) ### Changed - Fixed not being redirected to login when accessing settings while unauthenticated (thanks to [@ianrobertsFF](https://github.com/ianrobertsFF)) ## [5.0.1] - 14-05-2022 ### Changed - Fixed migrations (thanks to [@AndreasFurster](https://github.com/AndreasFurster)) ## [5.0.0] - 13-05-2022 ### Changed - NB! Changed namespace from OptimistDigital to Outl1ne - Allow redirections as a result of settings updates (thanks to [@ianrobertsFF](https://github.com/ianrobertsFF)) - Fixed sidebar subpages titles (thanks to [@faab007nl](https://github.com/faab007nl)) - Updated packages ## [4.0.4] - 29-04-2022 ### Changed - Removed loadViewsFrom() call from ServiceProvider - Fixed memory cache not clearing after settings update - Updated packages ## [4.0.3] - 25-04-2022 ### Changed - Changed `empty` check to `isset` when loading settings to allow negative but defined values ## [4.0.2] - 08-04-2022 ### Changed - Reworked routing logic ## [4.0.1] - 08-04-2022 ### Changed - Fixed page titles ## [4.0.0] - 08-04-2022 ### Added - Nova 4 support - Fully compatible with light and dark modes ### Changed - Dropped Laravel 7 and 8 support - Dropped PHP 7.X support - Dropped Nova 3 support ================================================ FILE: LICENSE.md ================================================ MIT License Copyright (c) 2019 Outl1ne Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Nova Settings [![Latest Version on Packagist](https://img.shields.io/packagist/v/outl1ne/nova-settings.svg?style=flat-square)](https://packagist.org/packages/outl1ne/nova-settings) [![Total Downloads](https://img.shields.io/packagist/dt/outl1ne/nova-settings.svg?style=flat-square)](https://packagist.org/packages/outl1ne/nova-settings) This [Laravel Nova](https://nova.laravel.com) package allows you to create custom settings in code (using Nova's native fields) and creates a UI for the users where the settings can be edited. ## Requirements - `php: >=8.0` - `laravel/nova: ^4.26` ## Features - Settings fields management in code - UI for editing settings - Helpers for accessing settings - Rule validation support ## Screenshots ![Settings View](docs/index.png) ## Installation Install the package in a Laravel Nova project via Composer and run migrations: ```bash # Install nova-settings composer require outl1ne/nova-settings # Run migrations php artisan migrate ``` Register the tool with Nova in the `tools()` method of the `NovaServiceProvider`: ```php // in app/Providers/NovaServiceProvider.php public function tools() { return [ // ... new \Outl1ne\NovaSettings\NovaSettings ]; } ``` ## Usage ### Registering fields Define the fields in your `NovaServiceProvider`'s `boot()` function by calling `NovaSettings::addSettingsFields()`. ```php // Using an array \Outl1ne\NovaSettings\NovaSettings::addSettingsFields([ Text::make('Some setting', 'some_setting'), Number::make('A number', 'a_number'), ]); // OR // Using a callable \Outl1ne\NovaSettings\NovaSettings::addSettingsFields(function() { return [ Text::make('Some setting', 'some_setting'), Number::make('A number', 'a_number'), ]; }); ``` #### Registering field panels ```php // Using an array \Outl1ne\NovaSettings\NovaSettings::addSettingsFields([ Panel::make('Panel Title', [ Text::make('Some setting', 'some_setting'), Number::make('A number', 'a_number'), ]), ]); ``` ### Casts If you want the value of the setting to be formatted before it's returned, pass an array similar to `Eloquent`'s `$casts` property as the second parameter. ```php \Outl1ne\NovaSettings\NovaSettings::addSettingsFields([ // ... fields ], [ 'some_boolean_value' => 'boolean', 'some_float' => 'float', 'some_collection' => 'collection', // ... ]); ``` ### Subpages Add a settings page name as a third argument to list those settings in a custom subpage. ```php \Outl1ne\NovaSettings\NovaSettings::addSettingsFields([ Text::make('Some setting', 'some_setting'), Number::make('A number', 'a_number'), ], [], 'Subpage'); ``` If you leave the custom name empty, the field(s) will be listed under "General". To translate the page name, publish the translations and add a new key `novaSettings.$subpage` to the respective translations file, where `$subpage` is the name of the page (full lowercase, slugified). ### Authorization #### Show/hide all settings If you want to hide the whole `Settings` area from the sidebar, you can authorize the `NovaSettings` tool like so: ```php public function tools(): array { return [ NovaSettings::make()->canSee(fn () => user()->isAdmin()), ]; } ``` #### Show/hide specific setting fields If you want to hide only some settings, you can use `->canSee(fn () => ...)` per field. Like so: ```php Text::make('A text field') ->canSee(fn () => user()->isAdmin()), ``` ### Helper functions #### nova_get_settings(\$keys = null, \$defaults = []) Call `nova_get_settings()` to get all the settings formated as a regular array. Additionally, you can pass a `key => value` array as a second argument: `nova_get_settings(['some_key], ['some_key' => 'default_value'])`. #### nova_get_setting(\$key, \$default = null) To get a single setting's value, call `nova_get_setting('some_setting_key')`. It will return either a value or null if there's no setting with such key. You can also pass default value as a second argument `nova_get_setting('some_setting_key', 'default_value')`, which will be returned, if no setting was found with given key. #### nova_set_setting_value(\$key, \$value = null) Sets a setting value for the given key. ## Configuration The config file can be published using the following command: ```bash php artisan vendor:publish --provider="Outl1ne\NovaSettings\NovaSettingsServiceProvider" --tag="config" ``` | Name | Type | Default | Description | |-----------------------|---------|-------------------|--------------------------------------------------------------------------------------------------| | `base_path` | String | `nova-settings` | URL path of settings page. | | `reload_page_on_save` | Boolean | false | Reload the entire page on save. Useful when updating any Nova UI related settings. | | `models.settings` | Model | `Settings::class` | Optionally override the Settings model. | | `cache` | String | `:memory:` | Cache store name to use that cache, ":memory:" for singleton class, or null to turn off caching. | The migration can also be published and overwritten using: ```bash php artisan vendor:publish --provider="Outl1ne\NovaSettings\NovaSettingsServiceProvider" --tag="migrations" ``` ## Localization The translation file(s) can be published by using the following command: ```bash php artisan vendor:publish --provider="Outl1ne\NovaSettings\NovaSettingsServiceProvider" --tag="translations" ``` You can add your translations to `resources/lang/vendor/nova-settings/` by creating a new translations file with the locale name (ie `et.json`) and copying the JSON from the existing `en.json`. ## Credits - [Tarvo Reinpalu](https://github.com/Tarpsvo) ## License Nova Settings is open-sourced software licensed under the [MIT license](LICENSE.md). ================================================ FILE: composer.json ================================================ { "name": "outl1ne/nova-settings", "description": "A Laravel Nova tool for editing custom settings using native Nova fields.", "keywords": [ "laravel", "nova", "settings" ], "authors": [ { "name": "Tarvo Reinpalu", "email": "tarvo@outl1ne.com", "role": "Developer" }, { "name": "Outl1ne", "email": "info@outl1ne.com", "role": "Maintainer" } ], "license": "MIT", "require": { "php": ">=8.1", "laravel/nova": "^5.0", "outl1ne/nova-translations-loader": "^5.0" }, "require-dev": { "laravel/nova-devtool": "^1.0", "nunomaduro/collision": "^7.8", "orchestra/testbench": "^8.30|^9.8" }, "autoload": { "psr-4": { "Outl1ne\\NovaSettings\\": "src/" }, "files": [ "./src/helpers.php" ] }, "autoload-dev": { "psr-4": { "Outl1ne\\NovaSettings\\Tests\\": "tests" } }, "extra": { "laravel": { "providers": [ "Outl1ne\\NovaSettings\\NovaSettingsServiceProvider" ] } }, "config": { "sort-packages": true, "allow-plugins": { "php-http/discovery": true } }, "minimum-stability": "dev", "prefer-stable": true, "repositories": [ { "type": "composer", "url": "https://nova.laravel.com" } ], "scripts": { "dusk:prepare": [ "./vendor/bin/dusk-updater detect --auto-update" ], "dusk:assets": [ "npm ci", "npm run prod", "./vendor/bin/testbench-dusk nova:publish" ], "dusk:test": [ "./vendor/bin/phpunit -c phpunit.dusk.xml.dist" ] } } ================================================ FILE: config/nova-settings.php ================================================ 'nova_settings', /** * URL path of settings page */ 'base_path' => 'nova-settings', /** * Reload the entire page on save. Useful when updating any Nova UI related settings. */ 'reload_page_on_save' => false, /** * We need to know which eloquent model should be used to retrieve your permissions. * Of course, it is often just the default model but you may use whatever you like. * * The model you want to use as a model needs to extend the original model. */ 'models' => [ 'settings' => \Outl1ne\NovaSettings\Models\Settings::class, ], /** * Show the sidebar menu */ 'show_in_sidebar' => true, /* |-------------------------------------------------------------------------- | Cache settings |-------------------------------------------------------------------------- | | Here you may specify which of the cache connection should be used to | cache the settings. `:memory:` is the default which is a simple | in-memory cache through a singleton service class property. | `null` will disable caching. | */ 'cache' => [ 'store' => env('NOVA_SETTINGS_CACHE_DRIVER', ':memory:'), 'prefix' => 'nova-settings:', ], ]; ================================================ FILE: database/migrations/2019_08_13_000000_create_nova_settings_table.php ================================================ string('key')->unique()->primary(); $table->text('value')->nullable(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists(NovaSettings::getSettingsTableName()); } }; ================================================ FILE: database/migrations/2021_02_15_000000_update_nova_settings_value_column.php ================================================ text('value')->nullable()->change(); }); } /** * Reverse the migrations. * * @return void */ public function down() { // No down because previous migration was also modified } }; ================================================ FILE: dist/js/entry.js ================================================ /*! For license information please see entry.js.LICENSE.txt */ (()=>{var t={262:(t,e)=>{"use strict";e.A=(t,e)=>{const r=t.__vccOpts||t;for(const[t,n]of e)r[t]=n;return r}},189:(t,e,r)=>{"use strict";r.d(e,{A:()=>v});const n=Vue;var o={key:0,class:"flex items-center"},a={key:1,class:"bg-white dark:bg-gray-800 rounded-lg shadow p-3"},i={class:"flex flex-col justify-center align-center"},c={class:"w-3/4 py-4 text-center"},s={class:"text-90"};const u=LaravelNova;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)}function f(){f=function(){return e};var t,e={},r=Object.prototype,n=r.hasOwnProperty,o=Object.defineProperty||function(t,e,r){t[e]=r.value},a="function"==typeof Symbol?Symbol:{},i=a.iterator||"@@iterator",c=a.asyncIterator||"@@asyncIterator",s=a.toStringTag||"@@toStringTag";function u(t,e,r){return Object.defineProperty(t,e,{value:r,enumerable:!0,configurable:!0,writable:!0}),t[e]}try{u({},"")}catch(t){u=function(t,e,r){return t[e]=r}}function p(t,e,r,n){var a=e&&e.prototype instanceof w?e:w,i=Object.create(a.prototype),c=new I(n||[]);return o(i,"_invoke",{value:O(t,r,c)}),i}function d(t,e,r){try{return{type:"normal",arg:t.call(e,r)}}catch(t){return{type:"throw",arg:t}}}e.wrap=p;var h="suspendedStart",v="suspendedYield",g="executing",y="completed",m={};function w(){}function b(){}function x(){}var E={};u(E,i,(function(){return this}));var k=Object.getPrototypeOf,L=k&&k(k(F([])));L&&L!==r&&n.call(L,i)&&(E=L);var _=x.prototype=w.prototype=Object.create(E);function N(t){["next","throw","return"].forEach((function(e){u(t,e,(function(t){return this._invoke(e,t)}))}))}function S(t,e){function r(o,a,i,c){var s=d(t[o],t,a);if("throw"!==s.type){var u=s.arg,f=u.value;return f&&"object"==l(f)&&n.call(f,"__await")?e.resolve(f.__await).then((function(t){r("next",t,i,c)}),(function(t){r("throw",t,i,c)})):e.resolve(f).then((function(t){u.value=t,i(u)}),(function(t){return r("throw",t,i,c)}))}c(s.arg)}var a;o(this,"_invoke",{value:function(t,n){function o(){return new e((function(e,o){r(t,n,e,o)}))}return a=a?a.then(o,o):o()}})}function O(e,r,n){var o=h;return function(a,i){if(o===g)throw Error("Generator is already running");if(o===y){if("throw"===a)throw i;return{value:t,done:!0}}for(n.method=a,n.arg=i;;){var c=n.delegate;if(c){var s=T(c,n);if(s){if(s===m)continue;return s}}if("next"===n.method)n.sent=n._sent=n.arg;else if("throw"===n.method){if(o===h)throw o=y,n.arg;n.dispatchException(n.arg)}else"return"===n.method&&n.abrupt("return",n.arg);o=g;var u=d(e,r,n);if("normal"===u.type){if(o=n.done?y:v,u.arg===m)continue;return{value:u.arg,done:n.done}}"throw"===u.type&&(o=y,n.method="throw",n.arg=u.arg)}}}function T(e,r){var n=r.method,o=e.iterator[n];if(o===t)return r.delegate=null,"throw"===n&&e.iterator.return&&(r.method="return",r.arg=t,T(e,r),"throw"===r.method)||"return"!==n&&(r.method="throw",r.arg=new TypeError("The iterator does not provide a '"+n+"' method")),m;var a=d(o,e.iterator,r.arg);if("throw"===a.type)return r.method="throw",r.arg=a.arg,r.delegate=null,m;var i=a.arg;return i?i.done?(r[e.resultName]=i.value,r.next=e.nextLoc,"return"!==r.method&&(r.method="next",r.arg=t),r.delegate=null,m):i:(r.method="throw",r.arg=new TypeError("iterator result is not an object"),r.delegate=null,m)}function j(t){var e={tryLoc:t[0]};1 in t&&(e.catchLoc=t[1]),2 in t&&(e.finallyLoc=t[2],e.afterLoc=t[3]),this.tryEntries.push(e)}function B(t){var e=t.completion||{};e.type="normal",delete e.arg,t.completion=e}function I(t){this.tryEntries=[{tryLoc:"root"}],t.forEach(j,this),this.reset(!0)}function F(e){if(e||""===e){var r=e[i];if(r)return r.call(e);if("function"==typeof e.next)return e;if(!isNaN(e.length)){var o=-1,a=function r(){for(;++o=0;--a){var i=this.tryEntries[a],c=i.completion;if("root"===i.tryLoc)return o("end");if(i.tryLoc<=this.prev){var s=n.call(i,"catchLoc"),u=n.call(i,"finallyLoc");if(s&&u){if(this.prev=0;--r){var o=this.tryEntries[r];if(o.tryLoc<=this.prev&&n.call(o,"finallyLoc")&&this.prev=0;--e){var r=this.tryEntries[e];if(r.finallyLoc===t)return this.complete(r.completion,r.afterLoc),B(r),m}},catch:function(t){for(var e=this.tryEntries.length-1;e>=0;--e){var r=this.tryEntries[e];if(r.tryLoc===t){var n=r.completion;if("throw"===n.type){var o=n.arg;B(r)}return o}}throw Error("illegal catch attempt")},delegateYield:function(e,r,n){return this.delegate={iterator:F(e),resultName:r,nextLoc:n},"next"===this.method&&(this.arg=t),m}},e}function p(t,e,r,n,o,a,i){try{var c=t[a](i),s=c.value}catch(t){return void r(t)}c.done?e(s):Promise.resolve(s).then(n,o)}function d(t){return function(){var e=this,r=arguments;return new Promise((function(n,o){var a=t.apply(e,r);function i(t){p(a,n,o,i,c,"next",t)}function c(t){p(a,n,o,i,c,"throw",t)}i(void 0)}))}}const h={components:{Button:LaravelNovaUi.Button},data:function(){return{pageId:!1,loading:!1,isUpdating:!1,fields:[],panels:[],authorizations:[],validationErrors:new u.Errors}},created:function(){var t=this;return d(f().mark((function e(){return f().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:t.pageId=t.$page.props.pageId||"general",t.getFields();case 2:case"end":return e.stop()}}),e)})))()},methods:{getFields:function(){var t=this;return d(f().mark((function e(){var r,n,o,a,i,c,s;return f().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return t.loading=!0,t.fields=[],r={editing:!0,editMode:"update"},t.pageId&&(r.path=t.pageId),e.next=6,Nova.request().get("/nova-vendor/nova-settings/settings",{params:r}).catch((function(t){t.response.status}));case 6:n=e.sent,o=n.data,a=o.fields,i=o.panels,c=o.authorizations,t.fields=a,t.panels=i,t.authorizations=c,t.loading=!1,s=t.isUpdating?"resource-updated":"resource-loaded",Nova.$emit(s,{resourceName:"nova-settings"});case 17:case"end":return e.stop()}}),e)})))()},update:function(){var t=this;return d(f().mark((function e(){var r;return f().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.prev=0,t.isUpdating=!0,e.next=4,t.updateRequest();case 4:if(!(r=e.sent)||!r.data){e.next=14;break}if(!0!==r.data.reload){e.next=11;break}return location.reload(),e.abrupt("return");case 11:if(!(r.data.redirect&&r.data.redirect.length>0)){e.next=14;break}return location.replace(r.data.redirect),e.abrupt("return");case 14:return Nova.success(t.__("novaSettings.settingsSuccessToast")),e.next=17,t.getFields();case 17:t.isUpdating=!1,t.validationErrors=new u.Errors,e.next=26;break;case 21:e.prev=21,e.t0=e.catch(0),console.error(e.t0),t.isUpdating=!1,e.t0&&e.t0.response&&422==e.t0.response.status&&(t.validationErrors=new u.Errors(e.t0.response.data.errors),Nova.error(t.__("There was a problem submitting the form.")));case 26:case"end":return e.stop()}}),e,null,[[0,21]])})))()},updateRequest:function(){return Nova.request().post("/nova-vendor/nova-settings/settings",this.formData)}},computed:{formData:function(){var t=new FormData;return this.fields.forEach((function(e){return e.fill(t)})),t.append("_method","POST"),this.pageId&&t.append("path",this.pageId),t},panelsWithFields:function(){var t=this;return this.panels.map((function(e){return{name:e.name,component:e.component,helpText:e.helpText,fields:t.fields.filter((function(t){return t.panel===e.name})),showTitle:e.showTitle}}))}}};const v=(0,r(262).A)(h,[["render",function(t,e,r,u,l,f){var p=(0,n.resolveComponent)("Head"),d=(0,n.resolveComponent)("Button"),h=(0,n.resolveComponent)("LoadingView");return(0,n.openBlock)(),(0,n.createBlock)(h,{loading:l.loading,key:l.pageId},{default:(0,n.withCtx)((function(){return[(0,n.createVNode)(p,{title:t.__("novaSettings.navigationItemTitle")+("general"!==l.pageId?" (".concat(l.pageId,")"):"")},null,8,["title"]),l.fields&&l.fields.length?((0,n.openBlock)(),(0,n.createElementBlock)("form",{key:0,onSubmit:e[0]||(e[0]=(0,n.withModifiers)((function(){return f.update&&f.update.apply(f,arguments)}),["prevent"])),autocomplete:"off",dusk:"nova-settings-form"},[((0,n.openBlock)(!0),(0,n.createElementBlock)(n.Fragment,null,(0,n.renderList)(f.panelsWithFields,(function(t){return(0,n.openBlock)(),(0,n.createBlock)((0,n.resolveDynamicComponent)("form-"+t.component),{key:t.name,panel:t,name:t.name,fields:t.fields,"resource-name":"nova-settings","resource-id":l.pageId,mode:"form",class:"mb-6","validation-errors":l.validationErrors,"show-help-text":!0},null,8,["panel","name","fields","resource-id","validation-errors"])})),128)),l.authorizations.authorizedToUpdate?((0,n.openBlock)(),(0,n.createElementBlock)("div",o,[(0,n.createVNode)(d,{dusk:"update-button",type:"submit",class:"ml-auto",disabled:l.isUpdating,loading:l.isUpdating},{default:(0,n.withCtx)((function(){return[(0,n.createTextVNode)((0,n.toDisplayString)(t.__("novaSettings.saveButtonText")),1)]})),_:1},8,["disabled","loading"])])):(0,n.createCommentVNode)("",!0)],32)):((0,n.openBlock)(),(0,n.createElementBlock)("div",a,[(0,n.createElementVNode)("div",i,[(0,n.createElementVNode)("div",c,[(0,n.createElementVNode)("p",s,(0,n.toDisplayString)(t.__("novaSettings.noSettingsFieldsText")),1)])])]))]})),_:1},8,["loading"])}]])}},e={};function r(n){var o=e[n];if(void 0!==o)return o.exports;var a=e[n]={exports:{}};return t[n](a,a.exports,r),a.exports}r.d=(t,e)=>{for(var n in e)r.o(e,n)&&!r.o(t,n)&&Object.defineProperty(t,n,{enumerable:!0,get:e[n]})},r.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e),Nova.booting((function(t,e,n){Nova.inertia("NovaSettings",r(189).A)}))})(); ================================================ FILE: dist/js/entry.js.LICENSE.txt ================================================ /*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */ ================================================ FILE: dist/mix-manifest.json ================================================ { "/js/entry.js": "/js/entry.js" } ================================================ FILE: lang/ar.json ================================================ { "novaSettings.navigationItemTitle": "الإعدادات", "novaSettings.saveButtonText": "حفظ الإعدادات", "novaSettings.noSettingsFieldsText": "لم يتم اضافة حقول للإعدادات.", "novaSettings.settingsSuccessToast": "تم حفظ الإعدادات", "novaSettings.general": "الإعدادات العامة" } ================================================ FILE: lang/de.json ================================================ { "novaSettings.navigationItemTitle": "Einstellungen", "novaSettings.saveButtonText": "Einstellungen speichern", "novaSettings.noSettingsFieldsText": "Es wurden keine Einstellungs-Felder definiert.", "novaSettings.settingsSuccessToast": "Einstellungen erfolgreich geändert." } ================================================ FILE: lang/en.json ================================================ { "novaSettings.navigationItemTitle": "Settings", "novaSettings.saveButtonText": "Save settings", "novaSettings.noSettingsFieldsText": "No settings fields have been defined.", "novaSettings.settingsSuccessToast": "Settings successfully updated!", "novaSettings.general": "General" } ================================================ FILE: lang/es.json ================================================ { "novaSettings.navigationItemTitle": "Configuración", "novaSettings.saveButtonText": "Guardar configuración", "novaSettings.noSettingsFieldsText": "No se ha definido ningún campo de configuración.", "novaSettings.settingsSuccessToast": "¡La configuración se ha actualizado correctamente!", "novaSettings.general": "General" } ================================================ FILE: lang/et.json ================================================ { "novaSettings.navigationItemTitle": "Seaded", "novaSettings.saveButtonText": "Salvesta seaded", "novaSettings.noSettingsFieldsText": "Ühtegi seadevälja ei ole defineeritud.", "novaSettings.settingsSuccessToast": "Seaded uuendatud!", "novaSettings.general": "Üldine" } ================================================ FILE: lang/fa.json ================================================ { "novaSettings.navigationItemTitle": "تنظیمات", "novaSettings.saveButtonText": "ذخیره تنظیمات", "novaSettings.noSettingsFieldsText": "هیچ تنظیمی تعریف نشده است.", "novaSettings.settingsSuccessToast": "تنظیمات ذخیره شد.", "novaSettings.general": "عمومی" } ================================================ FILE: lang/fr.json ================================================ { "novaSettings.navigationItemTitle": "Paramètres", "novaSettings.saveButtonText": "Enregistrer paramètres", "novaSettings.noSettingsFieldsText": "Aucun champ paramètres n'a été défini.", "novaSettings.settingsSuccessToast": "Paramètres mis à jour !", "novaSettings.general": "Paramètres généraux" } ================================================ FILE: lang/it.json ================================================ { "novaSettings.navigationItemTitle": "Impostazioni", "novaSettings.saveButtonText": "Salva impostazioni", "novaSettings.noSettingsFieldsText": "Nessun campo Impostazioni è stato definito.", "novaSettings.settingsSuccessToast": "Impostazioni aggiornate con successo", "novaSettings.general": "Generale" } ================================================ FILE: lang/nl.json ================================================ { "novaSettings.navigationItemTitle": "Instellingen", "novaSettings.saveButtonText": "Instellingen opslaan", "novaSettings.noSettingsFieldsText": "Geen veld gedefineerd.", "novaSettings.settingsSuccessToast": "Instellingen succesvol geupdatet", "novaSettings.general": "Algemeen" } ================================================ FILE: lang/pt-BR.json ================================================ { "novaSettings.navigationItemTitle": "Configurações", "novaSettings.saveButtonText": "Salvar configurações", "novaSettings.noSettingsFieldsText": "Nenhum campo de configuração foi definido.", "novaSettings.settingsSuccessToast": "Configurações atualizadas com sucesso", "novaSettings.general": "Geral" } ================================================ FILE: lang/ru.json ================================================ { "novaSettings.navigationItemTitle": "Настройки", "novaSettings.saveButtonText": "Сохранить", "novaSettings.noSettingsFieldsText": "Поля настроек не определены.", "novaSettings.settingsSuccessToast": "Настройки успешно обновлены", "novaSettings.general": "Основные" } ================================================ FILE: lang/sk.json ================================================ { "novaSettings.navigationItemTitle": "Nastavenia", "novaSettings.saveButtonText": "Uložiť nastavenia", "novaSettings.noSettingsFieldsText": "Neboli definované žiadne polia nastavení.", "novaSettings.settingsSuccessToast": "Nastavenia boli úspešne aktualizované!", "novaSettings.general": "Všeobecné" } ================================================ FILE: lang/tr.json ================================================ { "novaSettings.navigationItemTitle": "Ayarlar", "novaSettings.saveButtonText": "Ayarları Kaydet", "novaSettings.noSettingsFieldsText": "Henüz bir ayar alanı tanımlamadınız.", "novaSettings.settingsSuccessToast": "Ayarlar başarıyla güncellendi!", "novaSettings.general": "Genel" } ================================================ FILE: lang/uz.json ================================================ { "novaSettings.navigationItemTitle": "Sozlamalar", "novaSettings.saveButtonText": "Saqlash", "novaSettings.noSettingsFieldsText": "Sozlamalar maydonlari belgilanmagan.", "novaSettings.settingsSuccessToast": "Sozlamalar muvaffaqiyatli saqlandi", "novaSettings.general": "Umumiy" } ================================================ FILE: package.json ================================================ { "private": true, "scripts": { "dev": "mix", "watch": "mix watch", "hot": "mix watch --hot", "prod": "mix --production", "format": "prettier --write 'resources/**/*.{css,js,vue}'" }, "devDependencies": { "laravel-mix": "^6.0.49", "@vue/babel-plugin-jsx": "^1.2.5", "cross-env": "^7.0.3", "laravel-nova-devtool": "file:vendor/laravel/nova-devtool", "prettier": "^3.4.2", "resolve-url-loader": "^5.0.0", "sass": "^1.83.0", "sass-loader": "^16.0.4", "terser-webpack-plugin": "^5.3.11", "vue-loader": "^16.8.3", "vue-template-compiler": "^2.7.16", "vuex": "^4.1.0" }, "dependencies": { "vue": "^3.5.13" } } ================================================ FILE: phpunit.dusk.xml.dist ================================================ ./src ./tests/Browser ================================================ FILE: phpunit.xml.dist ================================================ src/ ./tests/Feature ================================================ FILE: resources/js/entry.js ================================================ Nova.booting((Vue, router, store) => { Nova.inertia('NovaSettings', require('./views/Settings').default); }); ================================================ FILE: resources/js/views/Settings.vue ================================================ ================================================ FILE: routes/api.php ================================================ group(function () { Route::prefix('nova-vendor/nova-settings')->group(function () { Route::get('/settings', 'SettingsController@get')->name('nova-settings.get'); Route::post('/settings', 'SettingsController@save')->name('nova-settings.save'); }); Route::delete('/nova-api/nova-settings/{path}/field/{fieldName}', 'SettingsController@deleteImage'); Route::post('/nova-api/nova-settings/{path}/field/{attribute}/preview', 'SettingsController@fieldPreview')->name('nova-settings.field-preview'); }); ================================================ FILE: src/Http/Controllers/SettingsController.php ================================================ unauthorized(); $path = $request->get('path', 'general'); $label = NovaSettings::getPageName($path); $fields = $this->assignToPanels($label, $this->availableFields($path)); $panels = $this->panelsWithDefaultLabel($label, app(NovaRequest::class)); $addResolveCallback = function (&$field) { if (!empty($field->attribute)) { $setting = NovaSettings::getSettingsModel()::firstOrNew(['key' => $field->attribute]); $fakeResource = $this->makeFakeResource($field->attribute, isset($setting) ? $setting->value : ''); $field->resolve($fakeResource); } if (!empty($field->meta['fields'])) { foreach ($field->meta['fields'] as $_field) { $setting = NovaSettings::getSettingsModel()::where('key', $_field->attribute)->first(); $fakeResource = $this->makeFakeResource($_field->attribute, isset($setting) ? $setting->value : null); $_field->resolve($fakeResource); } } }; $fields->each(function (&$field) use ($addResolveCallback) { $addResolveCallback($field); }); return response()->json([ 'panels' => $panels, 'fields' => $fields, 'authorizations' => NovaSettings::getAuthorizations(), ], 200); } public function save(NovaRequest $request) { if (!NovaSettings::getAuthorizations('authorizedToUpdate')) return $this->unauthorized(); $fields = $this->availableFields($request->get('path', 'general')); // NovaDependencyContainer support $fields = $fields->map(function ($field) { if (!empty($field->attribute)) return $field; if (!empty($field->meta['fields'])) return $field->meta['fields']; return null; })->filter()->flatten(); $rules = []; foreach ($fields as $field) { $fakeResource = $this->makeFakeResource($field->attribute, nova_get_setting($field->attribute)); $field->resolve($fakeResource, $field->attribute); // For nova-translatable support $rules = array_merge($rules, $field->getUpdateRules($request)); } Validator::make($request->all(), $rules)->validate(); $fields->whereInstanceOf(Resolvable::class)->each(function ($field) use ($request) { if (empty($field->attribute)) return; if ($field->isReadonly(app(NovaRequest::class))) return; $settingsClass = NovaSettings::getSettingsModel(); // For nova-translatable support if (!empty($field->meta['translatable']['original_attribute'])) $field->attribute = $field->meta['translatable']['original_attribute']; $existingRow = $settingsClass::where('key', $field->attribute)->first(); $tempResource = new \Laravel\Nova\Support\Fluent; $field->fill($request, $tempResource); if (!array_key_exists($field->attribute, $tempResource->getAttributes())) return; if (isset($existingRow)) { $existingRow->value = $tempResource->{$field->attribute}; $existingRow->save(); } else { $newRow = new $settingsClass; $newRow->key = $field->attribute; $newRow->value = $tempResource->{$field->attribute}; $newRow->save(); } }); if (config('nova-settings.reload_page_on_save', false) === true) { return response()->json(['reload' => true]); } return response('', 204); } public function deleteImage(Request $request, $pathName, $fieldName) { if (!NovaSettings::getAuthorizations('authorizedToUpdate')) return $this->unauthorized(); $existingRow = NovaSettings::getSettingsModel()::where('key', $fieldName)->first(); if (isset($existingRow)) { $field = $this->findField(collect(NovaSettings::getFields($pathName)), $fieldName); // Delete file if exists if (isset($field) && $field instanceof \Laravel\Nova\Fields\File) { $disk = $field->getStorageDisk(); Storage::disk($disk)->delete($existingRow->value); } $existingRow->value = null; $existingRow->save(); } return response('', 204); } /** * Handle field preview requests for markdown and other previewable fields. * * @param \Illuminate\Http\Request $request * @param string $path * @param string $attribute * @return \Illuminate\Http\JsonResponse */ public function fieldPreview(Request $request, $path, $attribute) { if (!NovaSettings::canSeeSettings()) return $this->unauthorized(); // Find the field in the settings $field = $this->findField(collect(NovaSettings::getFields($path)), $attribute); if (!$field) { return response()->json(['error' => 'Field not found'], 404); } // Get the content to preview from the request $content = $request->input('value', ''); return response()->json([ 'preview' => $field->previewFor($content) ]); } protected function findField($fields, $fieldName) { if (empty($fields)) return null; $field = $fields->firstWhere('attribute', $fieldName); // Target field might be inside container field if (empty($field)) { foreach ($fields as $value) { if ($value instanceof \Laravel\Nova\Panel) { $field = $this->findField(collect($value->data), $fieldName); if (!empty($field)) return $field; } if (class_exists('\Eminiarts\Tabs\Tabs') && $value instanceof \Eminiarts\Tabs\Tabs) { $field = $this->findField(collect($value->data), $fieldName); if (!empty($field)) return $field; } } } return $field; } protected function availableFields($path = 'general') { return (new FieldCollection($this->filter(NovaSettings::getFields($path))))->authorized(request()); } protected function fields(Request $request, $path = 'general') { return NovaSettings::getFields($path); } protected function makeFakeResource(string $fieldName, $fieldValue) { $fakeResource = new \Laravel\Nova\Support\Fluent; $fakeResource->{$fieldName} = $fieldValue; return $fakeResource; } /** * Return the panels for this request with the default label. * * @param string $label * @param \Laravel\Nova\Http\Requests\NovaRequest $request * @return array */ protected function panelsWithDefaultLabel($label, NovaRequest $request) { $method = $this->fieldsMethod($request); return with( collect(array_values($this->{$method}($request, $request->get('path', 'general'))))->whereInstanceOf(Panel::class)->unique('name')->values(), function ($panels) use ($label) { return $panels->when($panels->where('name', $label)->isEmpty(), function ($panels) use ($label) { return $panels->prepend((new Panel($label))->withToolbar()); })->all(); } ); } protected function unauthorized() { return response()->json(['error' => 'Unauthorized'], 403); } protected function assignToPanels($label, FieldCollection $fields) { return $fields->map(function ($field) use ($label) { if (!$field->panel) $field->panel = Panel::make($label); return $field; }); } } ================================================ FILE: src/Http/Middleware/Authorize.php ================================================ first([$this, 'matchesTool']); return optional($tool)->authorize($request) ? $next($request) : abort(403); } /** * Determine whether this tool belongs to the package. * * @param \Laravel\Nova\Tool $tool * @return bool */ public function matchesTool($tool) { return $tool instanceof NovaSettings; } } ================================================ FILE: src/Http/Middleware/SettingsPathExists.php ================================================ get('path') ?: $request->route('path'); $path = !empty($path) ? trim($path) : 'general'; return NovaSettings::doesPathExist($path) ? $next($request) : abort(404); } } ================================================ FILE: src/Models/Settings.php ================================================ setTable(NovaSettings::getSettingsTableName()); } protected static function booted() { static::updated(function ($setting) { NovaSettings::getStore()->clearCache($setting->key); }); } public function setValueAttribute($value) { $this->casts = NovaSettings::getCasts(); $castType = null; if ($this->hasCast($this->key)) $castType = $this->getCastType($this->key); switch ($castType) { case 'datetime': case 'date': $this->attributes['value'] = $value; return; default: $this->attributes['value'] = is_array($value) || $value instanceof \JsonSerializable ? json_encode($value) : $value; } } public function getValueAttribute($value) { $originalCasts = $this->casts; $this->casts = NovaSettings::getCasts(); if ($this->hasCast($this->key)) { $value = $this->castAttribute($this->key, $value); } $this->casts = $originalCasts; return $value; } public static function getValueForKey($key) { $setting = static::where('key', $key)->get()->first(); return isset($setting) ? $setting->value : null; } } ================================================ FILE: src/Nova/Resources/Settings.php ================================================ path($basePath . '/' . array_key_first($fields)) ->icon('adjustments-vertical'); } else { $menuItems = []; foreach ($fields as $key => $fields) { $menuItems[] = MenuItem::link(self::getPageName($key), "{$basePath}/{$key}"); } return MenuSection::make(__('novaSettings.navigationItemTitle'), $menuItems) ->icon('adjustments-vertical') ->collapsable(); } } public static function getSettingsTableName(): string { return config('nova-settings.table', 'nova_settings'); } public static function getPageName($key): string { if (__("novaSettings.$key") === "novaSettings.$key") { return Str::title(str_replace('-', ' ', $key)); } else { return __("novaSettings.$key"); } } public static function getAuthorizations($key = null) { $request = request(); $fakeResource = new \Outl1ne\NovaSettings\Nova\Resources\Settings(NovaSettings::getSettingsModel()::make()); $authorizations = [ 'authorizedToView' => $fakeResource->authorizedToView($request), 'authorizedToCreate' => $fakeResource->authorizedToCreate($request), 'authorizedToUpdate' => $fakeResource->authorizedToUpdate($request), 'authorizedToDelete' => $fakeResource->authorizedToDelete($request), ]; return $key ? $authorizations[$key] : $authorizations; } public static function canSeeSettings() { $auths = static::getAuthorizations(); return $auths['authorizedToView'] || $auths['authorizedToUpdate']; } /** * Define settings fields and an optional casts. * * @param array|callable $fields Array of fields/panels to be displayed or callable that returns an array. * @param array $casts Associative array same as Laravel's $casts on models. **/ public static function addSettingsFields($fields = [], $casts = [], $path = 'general') { return static::getStore()->addSettingsFields($fields, $casts, $path); } /** * Define casts. * * @param array $casts Casts same as Laravel's casts on a model. **/ public static function addCasts($casts = []) { return static::getStore()->addCasts($casts); } public static function getFields($path = null) { if (!$path) return static::getStore()->getRawFields(); return static::getStore()->getFields($path); } public static function clearFields() { return static::getStore()->clearFields(); } public static function getCasts() { return static::getStore()->getCasts(); } public static function getSetting($settingKey, $default = null) { return static::getStore()->getSetting($settingKey, $default); } public static function getSettings(?array $settingKeys = null, array $defaults = []) { return static::getStore()->getSettings($settingKeys, $defaults); } public static function setSettingValue($settingKey, $value = null) { return static::getStore()->setSettingValue($settingKey, $value); } public static function getSettingsModel(): string { return config('nova-settings.models.settings', Settings::class); } public static function doesPathExist($path) { return array_key_exists($path, static::getFields()); } public static function getStore(): NovaSettingsStore { return app()->make(NovaSettingsStore::class); } } ================================================ FILE: src/NovaSettingsCacheStore.php ================================================ cache = Cache::store(config('nova-settings.cache.store')); } public function clearCache($keyNames = null) { // Clear whole cache if (empty($keyNames)) { $this->getSettingsModelClass()::all(['key'])->each(function ($setting) { $this->cache->forget($this->getCacheKey($setting->key)); }); return; } // Clear specific keys if (is_string($keyNames)) $keyNames = [$keyNames]; foreach ($keyNames as $key) { $this->cache->forget($this->getCacheKey($key)); } } protected function getCached($keyNames = null) { if (is_string($keyNames)) { return $this->cache->get($this->getCacheKey($keyNames)); } if (is_array($keyNames)) { return collect($keyNames) ->mapWithKeys(function ($key) { if (!$this->cache->has($this->getCacheKey($key))) return []; return [$key => $this->getCached($key)]; }) ->toArray(); } return []; } protected function setCached($keyName, $value) { $this->cache->forever($this->getCacheKey($keyName), $value); } private function getCacheKey($key) { return config('nova-settings.cache.prefix', 'nova-settings:') . $key; } } ================================================ FILE: src/NovaSettingsInMemoryStore.php ================================================ cache = []; return; } // Clear specific keys if (is_string($keyNames)) $keyNames = [$keyNames]; foreach ($keyNames as $key) { unset($this->cache[$key]); } } protected function getCached($keyNames = null) { if (is_string($keyNames)) return $this->cache[$keyNames] ?? null; return is_array($keyNames) && !empty($keyNames) ? collect($this->cache)->only($keyNames)->toArray() : $this->cache; } protected function setCached($keyName, $value) { $this->cache[$keyName] = $value; } } ================================================ FILE: src/NovaSettingsNoCacheStore.php ================================================ loadMigrationsFrom(__DIR__ . '/../database/migrations'); $this->loadTranslations(__DIR__ . '/../lang', 'nova-settings', true); if ($this->app->runningInConsole()) { // Publish migrations $this->publishes([ __DIR__ . '/../database/migrations' => database_path('migrations'), ], 'migrations'); // Publish config $this->publishes([ __DIR__ . '/../config/' => config_path(), ], 'config'); } } public function register() { $this->registerRoutes(); $this->mergeConfigFrom( __DIR__ . '/../config/nova-settings.php', 'nova-settings' ); $this->registerSettingsStore(); } protected function registerSettingsStore() { $caching = config('nova-settings.cache.store'); if (is_array(config('cache.stores')) && in_array($caching, array_keys(config('cache.stores')))) { $this->app->singleton(NovaSettingsStore::class, function () { return new NovaSettingsCacheStore(); }); } else if ($caching === ':memory:') { $this->app->singleton(NovaSettingsStore::class, function () { return new NovaSettingsInMemoryStore(); }); } else { $this->app->singleton(NovaSettingsStore::class, function () { return new NovaSettingsNoCacheStore(); }); } } protected function registerRoutes() { // Register nova routes Nova::router()->group(function ($router) { $path = config('nova-settings.base_path', 'nova-settings'); $router ->get("{$path}/{pageId?}", fn ($pageId = 'general') => inertia('NovaSettings', ['basePath' => $path, 'pageId' => $pageId])) ->middleware(['nova', Authenticate::class]) ->domain(config('nova.domain', null)); }); if ($this->app->routesAreCached()) return; Route::middleware(['nova', Authorize::class, SettingsPathExists::class]) ->domain(config('nova.domain', null)) ->group(__DIR__ . '/../routes/api.php'); } } ================================================ FILE: src/NovaSettingsStore.php ================================================ fields[$path] = array_merge($this->fields[$path] ?? [], $fields ?? []); $this->casts = array_merge($this->casts, $casts ?? []); return $this; } public function addCasts($casts = []) { $this->casts = array_merge($this->casts, $casts); return $this; } public function getRawFields() { return $this->fields; } public function getFields($path = 'general') { $rawFields = array_map(function ($fieldItem) { return is_callable($fieldItem) ? call_user_func($fieldItem) : $fieldItem; }, $this->fields[$path] ?? $this->fields); $fields = []; foreach ($rawFields as $rawField) { if (is_array($rawField)) $fields = array_merge($fields, $rawField); else $fields[] = $rawField; } return $fields; } public function getCasts() { return $this->casts; } public function getSetting($settingKey, $default = null) { if ($cached = $this->getCached($settingKey)) return $cached; $settingValue = $this->getSettingsModelClass()::getValueForKey($settingKey) ?? $default; $this->setCached($settingKey, $settingValue); return $settingValue; } public function getSettings(?array $settingKeys = null, array $defaults = []) { if (!empty($settingKeys)) { $cached = $this->getCached($settingKeys); $hasMissingKeys = !empty(array_diff($settingKeys, array_keys($cached))); if (!$hasMissingKeys) return $cached; $settings = $this->getSettingsModelClass()::whereIn('key', $settingKeys) ->get() ->pluck('value', 'key'); return collect($settingKeys)->flatMap(function ($settingKey) use ($settings, $defaults) { $settingValue = $settings[$settingKey] ?? null; if (isset($settingValue)) { $this->setCached($settingKey, $settingValue); return [$settingKey => $settingValue]; } else { $defaultValue = $defaults[$settingKey] ?? null; return [$settingKey => $defaultValue]; } })->toArray(); } return $this->getSettingsModelClass()::all() ->tap(function ($settings) { $settings->each(function ($setting) { $this->setCached($setting->key, $setting->value); }); }) ->pluck('value', 'key') ->toArray(); } public function setSettingValue($settingKey, $value = null) { $setting = $this->getSettingsModelClass()::firstOrCreate(['key' => $settingKey]); $setting->value = $value; $setting->save(); $this->setCached($settingKey, $setting->value); return $setting; } public abstract function clearCache($keyNames = null); public function clearFields() { $this->fields = []; $this->casts = []; $this->clearCache(); } protected abstract function getCached($keyNames = null); protected abstract function setCached($keyName, $value); /** * @return class-string<\Outl1ne\NovaSettings\Models\Settings> */ protected function getSettingsModelClass() { return NovaSettings::getSettingsModel(); } } ================================================ FILE: src/helpers.php ================================================ setupLaravel(); $this->browse(function (Browser $browser) { $browser->loginAs(User::find(1)) ->visit('nova'); dump($browser->element('*')->getAttribute('innerHTML')); $browser ->assertSee('Settings'); $browser->blank(); }); } public function test_settings_appears_in_sidebar_with_fields() { $this->setupLaravel(); $this->browse(function (Browser $browser) { $browser->loginAs(User::find(1)) ->visit('nova') ->assertSee('Settings'); $browser->blank(); }); } public function test_can_navigate_into_and_render_settings() { $this->browse(function (Browser $browser) { $browser->loginAs(User::find(1)) ->visit('nova') ->assertVisible('@nova-settings') ->pause(1500) ->click('@nova-settings') ->waitFor('@nova-settings-form') ->assertSee('Hello Field'); $browser->blank(); }); } } ================================================ FILE: tests/DuskTestCase.php ================================================ app->make('config'), function ($config) { $config->set('app.url', static::baseServeUrl()); $config->set('filesystems.disks.public.url', static::baseServeUrl() . '/storage'); }); } /** * Get base path. * * @return string */ protected function getBasePath() { return realpath(__DIR__ . '/../vendor/laravel/nova-dusk-suite'); } /** * Get package providers. * * @param \Illuminate\Foundation\Application $app * * @return array */ protected function getPackageProviders($app) { return [ 'Fideloper\Proxy\TrustedProxyServiceProvider', 'Laravel\Nova\NovaCoreServiceProvider', 'Carbon\Laravel\ServiceProvider', 'Outl1ne\NovaSettings\NovaSettingsServiceProvider', ]; } /** * Get application aliases. * * @param \Illuminate\Foundation\Application $app * * @return array */ protected function getApplicationAliases($app) { return $app['config']['app.aliases']; } /** * Get application providers. * * @param \Illuminate\Foundation\Application $app * * @return array */ protected function getApplicationProviders($app) { return $app['config']['app.providers']; } /** * Resolve application implementation. * * @return \Illuminate\Foundation\Application */ protected function resolveApplication() { return tap(new Application($this->getBasePath()), function ($app) { $app->detectEnvironment(function () { return 'testing'; }); }); } /** * Resolve application Console Kernel implementation. * * @param \Illuminate\Foundation\Application $app * * @return void */ protected function resolveApplicationConsoleKernel($app) { $app->singleton('Illuminate\Contracts\Console\Kernel', 'App\Console\Kernel'); } /** * Resolve application HTTP Kernel implementation. * * @param \Illuminate\Foundation\Application $app * * @return void */ protected function resolveApplicationHttpKernel($app) { $app->singleton('Illuminate\Contracts\Http\Kernel', 'App\Http\Kernel'); } /** * Resolve application HTTP exception handler. * * @param \Illuminate\Foundation\Application $app * * @return void */ protected function resolveApplicationExceptionHandler($app) { $app->singleton('Illuminate\Contracts\Debug\ExceptionHandler', 'App\Exceptions\Handler'); } /** * Setup Laravel for the test. * * @param callable|null $callback * @return void */ protected function setupLaravel(callable $callback = null) { $this->artisan('migrate:fresh')->run(); $this->artisan('db:seed', ['--class' => \Database\Seeders\DatabaseSeeder::class])->run(); if (is_callable($callback)) { $callback($this->app); } } /** * Run the given callback with searchable functionality enabled. * * @param callable $callback * @return void */ protected function whileSearchable(callable $callback) { touch(base_path('.searchable')); try { $callback(); } finally { @unlink(base_path('.searchable')); } } /** * Run the given callback with inline-create functionality enabled. * * @param callable $callback * @return void */ protected function whileInlineCreate(callable $callback) { touch(base_path('.inline-create')); try { $callback(); } finally { @unlink(base_path('.inline-create')); } } /** * Create a new Browser instance. * * @param \Facebook\WebDriver\Remote\RemoteWebDriver $driver * @return \Laravel\Dusk\Browser */ protected function newBrowser($driver) { return tap(new Browser($driver), function ($browser) { $browser->resize(env('DUSK_WIDTH'), env('DUSK_HEIGHT')); }); } protected function captureFailuresFor($browsers) { $browsers->each(function (Browser $browser, $key) { $name = str_replace('\\', '_', get_class($this)) . '_' . $this->getName(false); $browser->screenshot('failure-' . $this->getName() . '-' . $key); }); } } ================================================ FILE: tests/Feature/NavigationTest.php ================================================ renderNavigation()->render(); $this->assertStringContainsString('dusk="nova-settings"', $navigationView); } public function test_general_navigation_renders_with_fields() { NovaSettings::addSettingsFields([ Text::make('Test'), ]); $settingsTool = new NovaSettings; $navigationView = $settingsTool->renderNavigation()->render(); $this->assertStringContainsString('dusk="nova-settings"', $navigationView); } public function test_multiple_navigation_renders() { NovaSettings::addSettingsFields([ Text::make('Test'), ]); NovaSettings::addSettingsFields([ Text::make('TestTwo'), ], [], 'Other'); $settingsTool = new NovaSettings; $navigationView = $settingsTool->renderNavigation()->render(); $this->assertStringContainsString('dusk="nova-settings-general"', $navigationView); $this->assertStringContainsString('dusk="nova-settings-other"', $navigationView); } } ================================================ FILE: tests/Feature/SettingsCastTest.php ================================================ 'int']); Settings::create(['key' => 'test', 'value' => '555']); $settingValue = nova_get_setting('test'); $this->assertIsInt($settingValue); $this->assertEquals(555, $settingValue); } public function test_array_casting_works() { NovaSettings::addSettingsFields([ Number::make('Test'), ], ['test' => 'array']); $testValue = ['et' => 'Eesti', 'ru' => 'Russia']; Settings::create(['key' => 'test', 'value' => $testValue]); $settingValue = nova_get_setting('test'); $this->assertIsArray($settingValue); $this->assertEquals($testValue, $settingValue); } public function test_boolean_casting_works() { NovaSettings::addSettingsFields([ Number::make('Test'), ], ['test' => 'boolean']); Settings::create(['key' => 'test', 'value' => 1]); $settingValue = nova_get_setting('test'); $this->assertIsBool($settingValue); $this->assertTrue($settingValue); } } ================================================ FILE: tests/Feature/SettingsHelpersTest.php ================================================ 'test', 'value' => '555']); $this->assertEquals('555', nova_get_setting('test')); } public function test_nova_get_settings_works() { Settings::create(['key' => 'test', 'value' => '555']); Settings::create(['key' => 'testtwo', 'value' => '123']); $this->assertEquals(['test' => '555', 'testtwo' => '123'], nova_get_settings(['test', 'testtwo'])); } } ================================================ FILE: tests/Feature/SettingsRetrieveTest.php ================================================ getJson(route('nova-settings.get')); $request->assertStatus(200); $request->assertJsonCount(2, 'fields'); } public function test_general_fields_are_returned_with_general_path() { NovaSettings::addSettingsFields([ Text::make('Test'), Text::make('TestOne'), ]); NovaSettings::addSettingsFields([ Text::make('TestTwo'), Text::make('TestThree'), Text::make('TestFour'), ], [], 'Other'); $request = $this->getJson(route('nova-settings.get', ['path' => 'general'])); $request->assertStatus(200); $request->assertJsonCount(2, 'fields'); } public function test_other_fields_are_returned_with_other_path() { NovaSettings::addSettingsFields([ Text::make('Test'), Text::make('TestOne'), ]); NovaSettings::addSettingsFields([ Text::make('TestTwo'), Text::make('TestThree'), Text::make('TestFour'), ], [], 'Other'); $request = $this->getJson(route('nova-settings.get', ['path' => 'other'])); $request->assertStatus(200); $request->assertJsonCount(3, 'fields'); } } ================================================ FILE: tests/Feature/SettingsSaveTest.php ================================================ postJson(route('nova-settings.save'), ['test' => 'Test Value']); $request->assertStatus(204); $this->assertEquals('Test Value', Settings::getValueForKey('test')); } public function test_settings_are_saved_with_path() { NovaSettings::addSettingsFields([ Text::make('TestTwo'), Text::make('TestThree'), Text::make('TestFour'), ], [], 'Other'); $request = $this->postJson(route('nova-settings.save'), ['path' => 'other', 'testthree' => 'Test Value']); $request->assertStatus(204); $this->assertEquals('Test Value', Settings::getValueForKey('testthree')); } } ================================================ FILE: tests/IntegrationTestCase.php ================================================ setUpDatabase($this->app); } protected function getPackageProviders($app) { return [ NovaServiceProvider::class, NovaSettingsServiceProvider::class, ]; } protected function setUpDatabase() { $this->artisan('migrate:fresh'); } } ================================================ FILE: tests/bootstrap.php ================================================ safeLoad(); if (isset($_SERVER['CI']) || isset($_ENV['CI'])) { Orchestra\Testbench\Dusk\Options::withoutUI(); } else { Orchestra\Testbench\Dusk\Options::withUI(); } ================================================ FILE: webpack.mix.js ================================================ let mix = require('laravel-mix'); let path = require('path'); mix.extend('nova', new require('laravel-nova-devtool')); mix .setPublicPath('dist') .js('resources/js/entry.js', 'js') .vue({ version: 3 }) .nova('outl1ne/nova-settings') .alias({ 'laravel-nova': path.join(__dirname, 'vendor/laravel/nova/resources/js/mixins/packages.js'), });