diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..7f7281a
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,15 @@
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+insert_final_newline = true
+indent_style = space
+indent_size = 4
+trim_trailing_whitespace = true
+
+[*.md]
+trim_trailing_whitespace = false
+
+[*.{js,json,yml,scss,stylelintrc}]
+indent_size = 2
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..8e4af26
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,11 @@
+/.idea
+/vendor
+/node_modules
+package-lock.json
+composer.phar
+composer.lock
+phpunit.xml
+.phpunit.result.cache
+.DS_Store
+Thumbs.db
+.php_cs.cache
diff --git a/.php_cs b/.php_cs
new file mode 100644
index 0000000..e9505aa
--- /dev/null
+++ b/.php_cs
@@ -0,0 +1,87 @@
+ true,
+ 'blank_line_after_opening_tag' => true,
+ 'concat_space' => ['spacing' => 'none'],
+ 'no_multiline_whitespace_around_double_arrow' => true,
+ 'no_empty_statement' => true,
+ 'simplified_null_return' => false,
+ 'encoding' => true,
+ 'single_blank_line_at_eof' => true,
+ 'no_extra_consecutive_blank_lines' => true,
+ 'no_spaces_after_function_name' => true,
+ 'function_declaration' => true,
+ 'include' => true,
+ 'indentation_type' => true,
+ 'no_alias_functions' => true,
+ 'blank_line_after_namespace' => true,
+ 'line_ending' => true,
+ 'no_trailing_comma_in_list_call' => true,
+ 'not_operator_with_successor_space' => false,
+ 'lowercase_constants' => true,
+ 'lowercase_keywords' => true,
+ 'method_argument_space' => true,
+ 'trailing_comma_in_multiline_array' => true,
+ 'no_multiline_whitespace_before_semicolons' => true,
+ 'single_import_per_statement' => true,
+ 'no_leading_namespace_whitespace' => true,
+ 'no_blank_lines_after_class_opening' => true,
+ 'no_blank_lines_after_phpdoc' => true,
+ 'object_operator_without_whitespace' => true,
+ 'no_spaces_inside_parenthesis' => true,
+ 'phpdoc_indent' => true,
+ 'phpdoc_inline_tag' => true,
+ 'phpdoc_no_access' => true,
+ 'phpdoc_scalar' => true,
+ 'phpdoc_to_comment' => false,
+ 'phpdoc_trim' => true,
+ 'phpdoc_no_alias_tag' => true,
+ 'phpdoc_var_without_name' => true,
+ 'no_leading_import_slash' => true,
+ 'braces' => false,
+ 'blank_line_before_return' => true,
+ 'self_accessor' => true,
+ 'array_syntax' => ['syntax' => 'short'],
+ 'no_short_echo_tag' => true,
+ 'full_opening_tag' => true,
+ 'no_trailing_comma_in_singleline_array' => true,
+ 'single_blank_line_before_namespace' => true,
+ 'single_line_after_imports' => true,
+ 'single_quote' => true,
+ 'no_singleline_whitespace_before_semicolons' => true,
+ 'cast_spaces' => true,
+ 'standardize_not_equals' => true,
+ 'ternary_operator_spaces' => true,
+ 'no_trailing_whitespace' => true,
+ 'trim_array_spaces' => true,
+ 'binary_operator_spaces' => ['align_equals' => false],
+ 'unary_operator_spaces' => true,
+ 'no_unused_imports' => true,
+ 'visibility_required' => true,
+ 'no_whitespace_in_blank_line' => true,
+];
+
+$finder = PhpCsFixer\Finder::create()
+ ->name('*.php')
+ ->exclude($excludes)
+ ->ignoreDotFiles(true)
+ ->in(__DIR__);
+
+return PhpCsFixer\Config::create()
+ ->setRules($rules)
+ ->setFinder($finder)
+ ->setUsingCache(true);
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..54b360f
--- /dev/null
+++ b/README.md
@@ -0,0 +1,123 @@
+# Nova Order Field nestedset
+
+A field that make your resources orderable using [the laravel nestedset package](https://github.com/lazychaser/laravel-nestedset).
+
+## Requirements
+
+* PHP >= 7.1
+* Laravel Nova >= 2.0
+* Laravel Framework 5.8+
+
+## Installation
+
+```sh
+composer require novius/laravel-nova-order-nestedset-field
+```
+
+## Usage
+
+**Step 1**
+
+Use Kalnoy\Nestedset `NodeTrait` and Novius\LaravelNovaOrderNestedsetField `Orderable` trait on your model.
+
+Example :
+
+```php
+use Kalnoy\Nestedset\NodeTrait;
+use Novius\LaravelNovaOrderNestedsetField\Traits\Orderable;
+
+class Foo extends Model {
+ use NodeTrait;
+ use Orderable;
+
+ public function getLftName()
+ {
+ return 'left';
+ }
+
+ public function getRgtName()
+ {
+ return 'right';
+ }
+
+ public function getParentIdName()
+ {
+ return 'parent';
+ }
+}
+
+```
+
+**Step 2**
+
+Add the field to your resource and specify order for your resources.
+
+
+```php
+use Novius\LaravelNovaOrderNestedsetField\OrderNestedsetField;
+
+class FooResource extends Resource
+{
+ public function fields(Request $request)
+ {
+ return [
+ OrderNestedsetField::make('Order'),
+ ];
+ }
+
+ /**
+ * @param \Illuminate\Database\Eloquent\Builder $query
+ * @param array $orderings
+ * @return \Illuminate\Database\Eloquent\Builder
+ */
+ protected static function applyOrderings($query, array $orderings)
+ {
+ return $query->orderBy('left', 'asc');
+ }
+}
+
+```
+
+**Scoping**
+
+Imagine you have `Menu` model and `MenuItems`. There is a one-to-many relationship
+set up between these models. `MenuItem` has `menu_id` attribute for joining models
+together. `MenuItem` incorporates nested sets. It is obvious that you would want to
+process each tree separately based on `menu_id` attribute. In order to do so, you
+need to specify this attribute as scope attribute:
+
+```php
+ protected function getScopeAttributes()
+ {
+ return ['menu_id'];
+ }
+```
+
+[Retrieve more information about usage on official doc](https://github.com/lazychaser/laravel-nestedset#scoping).
+
+
+## Override default languages files
+
+Run:
+
+```sh
+php artisan vendor:publish --provider="Novius\LaravelNovaOrderNestedsetField\OrderNestedsetFieldServiceProvider" --tag="lang"
+```
+
+## Lint
+
+Run php-cs with:
+
+```sh
+composer run-script lint
+```
+
+## Contributing
+
+Contributions are welcome!
+Leave an issue on Github, or create a Pull Request.
+
+
+## Licence
+
+This package is under [GNU Affero General Public License v3](http://www.gnu.org/licenses/agpl-3.0.html) or (at your option) any later version.
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..6109ea5
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,46 @@
+{
+ "name": "novius/laravel-nova-order-nestedset-field",
+ "description": "A Laravel Nova field that make your resources orderable",
+ "keywords": [
+ "laravel",
+ "nova",
+ "nestedset",
+ "trees",
+ "hierarchies"
+ ],
+ "license": "AGPL-3.0-or-later",
+ "authors": [
+ {
+ "name": "Novius Agency",
+ "email": "team-developpeurs@novius.com",
+ "homepage": "https://www.novius.com"
+ }
+ ],
+ "require": {
+ "php": ">=7.1.0",
+ "kalnoy/nestedset": "^4.3.5"
+ },
+ "require-dev": {
+ "friendsofphp/php-cs-fixer": "~2.15.0"
+ },
+ "autoload": {
+ "psr-4": {
+ "Novius\\LaravelNovaOrderNestedsetField\\": "src/"
+ }
+ },
+ "scripts": {
+ "lint": [
+ "php-cs-fixer fix --dry-run --config .php_cs -vv --diff --allow-risky=yes"
+ ]
+ },
+ "extra": {
+ "laravel": {
+ "providers": [
+ "Novius\\LaravelNovaOrderNestedsetField\\OrderNestedsetFieldServiceProvider"
+ ]
+ }
+ },
+ "config": {
+ "sort-packages": true
+ }
+}
diff --git a/dist/css/field.css b/dist/css/field.css
new file mode 100644
index 0000000..e69de29
diff --git a/dist/js/field.js b/dist/js/field.js
new file mode 100644
index 0000000..f8f9ac8
--- /dev/null
+++ b/dist/js/field.js
@@ -0,0 +1,401 @@
+/******/ (function(modules) { // webpackBootstrap
+/******/ // The module cache
+/******/ var installedModules = {};
+/******/
+/******/ // The require function
+/******/ function __webpack_require__(moduleId) {
+/******/
+/******/ // Check if module is in cache
+/******/ if(installedModules[moduleId]) {
+/******/ return installedModules[moduleId].exports;
+/******/ }
+/******/ // Create a new module (and put it into the cache)
+/******/ var module = installedModules[moduleId] = {
+/******/ i: moduleId,
+/******/ l: false,
+/******/ exports: {}
+/******/ };
+/******/
+/******/ // Execute the module function
+/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
+/******/
+/******/ // Flag the module as loaded
+/******/ module.l = true;
+/******/
+/******/ // Return the exports of the module
+/******/ return module.exports;
+/******/ }
+/******/
+/******/
+/******/ // expose the modules object (__webpack_modules__)
+/******/ __webpack_require__.m = modules;
+/******/
+/******/ // expose the module cache
+/******/ __webpack_require__.c = installedModules;
+/******/
+/******/ // define getter function for harmony exports
+/******/ __webpack_require__.d = function(exports, name, getter) {
+/******/ if(!__webpack_require__.o(exports, name)) {
+/******/ Object.defineProperty(exports, name, {
+/******/ configurable: false,
+/******/ enumerable: true,
+/******/ get: getter
+/******/ });
+/******/ }
+/******/ };
+/******/
+/******/ // getDefaultExport function for compatibility with non-harmony modules
+/******/ __webpack_require__.n = function(module) {
+/******/ var getter = module && module.__esModule ?
+/******/ function getDefault() { return module['default']; } :
+/******/ function getModuleExports() { return module; };
+/******/ __webpack_require__.d(getter, 'a', getter);
+/******/ return getter;
+/******/ };
+/******/
+/******/ // Object.prototype.hasOwnProperty.call
+/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
+/******/
+/******/ // __webpack_public_path__
+/******/ __webpack_require__.p = "";
+/******/
+/******/ // Load entry module and return exports
+/******/ return __webpack_require__(__webpack_require__.s = 0);
+/******/ })
+/************************************************************************/
+/******/ ([
+/* 0 */
+/***/ (function(module, exports, __webpack_require__) {
+
+__webpack_require__(1);
+module.exports = __webpack_require__(6);
+
+
+/***/ }),
+/* 1 */
+/***/ (function(module, exports, __webpack_require__) {
+
+Nova.booting(function (Vue, router, store) {
+ Vue.component('index-order-nestedset-field', __webpack_require__(2));
+});
+
+/***/ }),
+/* 2 */
+/***/ (function(module, exports, __webpack_require__) {
+
+var disposed = false
+var normalizeComponent = __webpack_require__(3)
+/* script */
+var __vue_script__ = __webpack_require__(4)
+/* template */
+var __vue_template__ = __webpack_require__(5)
+/* template functional */
+var __vue_template_functional__ = false
+/* styles */
+var __vue_styles__ = null
+/* scopeId */
+var __vue_scopeId__ = null
+/* moduleIdentifier (server only) */
+var __vue_module_identifier__ = null
+var Component = normalizeComponent(
+ __vue_script__,
+ __vue_template__,
+ __vue_template_functional__,
+ __vue_styles__,
+ __vue_scopeId__,
+ __vue_module_identifier__
+)
+Component.options.__file = "resources/js/components/IndexField.vue"
+
+/* hot reload */
+if (false) {(function () {
+ var hotAPI = require("vue-hot-reload-api")
+ hotAPI.install(require("vue"), false)
+ if (!hotAPI.compatible) return
+ module.hot.accept()
+ if (!module.hot.data) {
+ hotAPI.createRecord("data-v-9e63f81a", Component.options)
+ } else {
+ hotAPI.reload("data-v-9e63f81a", Component.options)
+ }
+ module.hot.dispose(function (data) {
+ disposed = true
+ })
+})()}
+
+module.exports = Component.exports
+
+
+/***/ }),
+/* 3 */
+/***/ (function(module, exports) {
+
+/* globals __VUE_SSR_CONTEXT__ */
+
+// IMPORTANT: Do NOT use ES2015 features in this file.
+// This module is a runtime utility for cleaner component module output and will
+// be included in the final webpack user bundle.
+
+module.exports = function normalizeComponent (
+ rawScriptExports,
+ compiledTemplate,
+ functionalTemplate,
+ injectStyles,
+ scopeId,
+ moduleIdentifier /* server only */
+) {
+ var esModule
+ var scriptExports = rawScriptExports = rawScriptExports || {}
+
+ // ES6 modules interop
+ var type = typeof rawScriptExports.default
+ if (type === 'object' || type === 'function') {
+ esModule = rawScriptExports
+ scriptExports = rawScriptExports.default
+ }
+
+ // Vue.extend constructor export interop
+ var options = typeof scriptExports === 'function'
+ ? scriptExports.options
+ : scriptExports
+
+ // render functions
+ if (compiledTemplate) {
+ options.render = compiledTemplate.render
+ options.staticRenderFns = compiledTemplate.staticRenderFns
+ options._compiled = true
+ }
+
+ // functional template
+ if (functionalTemplate) {
+ options.functional = true
+ }
+
+ // scopedId
+ if (scopeId) {
+ options._scopeId = scopeId
+ }
+
+ var hook
+ if (moduleIdentifier) { // server build
+ hook = function (context) {
+ // 2.3 injection
+ context =
+ context || // cached call
+ (this.$vnode && this.$vnode.ssrContext) || // stateful
+ (this.parent && this.parent.$vnode && this.parent.$vnode.ssrContext) // functional
+ // 2.2 with runInNewContext: true
+ if (!context && typeof __VUE_SSR_CONTEXT__ !== 'undefined') {
+ context = __VUE_SSR_CONTEXT__
+ }
+ // inject component styles
+ if (injectStyles) {
+ injectStyles.call(this, context)
+ }
+ // register component module identifier for async chunk inferrence
+ if (context && context._registeredComponents) {
+ context._registeredComponents.add(moduleIdentifier)
+ }
+ }
+ // used by ssr in case component is cached and beforeCreate
+ // never gets called
+ options._ssrRegister = hook
+ } else if (injectStyles) {
+ hook = injectStyles
+ }
+
+ if (hook) {
+ var functional = options.functional
+ var existing = functional
+ ? options.render
+ : options.beforeCreate
+
+ if (!functional) {
+ // inject component registration as beforeCreate hook
+ options.beforeCreate = existing
+ ? [].concat(existing, hook)
+ : [hook]
+ } else {
+ // for template-only hot-reload because in that case the render fn doesn't
+ // go through the normalizer
+ options._injectStyles = hook
+ // register for functioal component in vue file
+ options.render = function renderWithStyleInjection (h, context) {
+ hook.call(context)
+ return existing(h, context)
+ }
+ }
+ }
+
+ return {
+ esModule: esModule,
+ exports: scriptExports,
+ options: options
+ }
+}
+
+
+/***/ }),
+/* 4 */
+/***/ (function(module, __webpack_exports__, __webpack_require__) {
+
+"use strict";
+Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+
+/* harmony default export */ __webpack_exports__["default"] = ({
+ props: ['resourceName', 'field'],
+ computed: {
+ resourceId: function resourceId() {
+ return this.$parent.resource.id.value;
+ }
+ },
+ methods: {
+ reorderResource: function reorderResource(direction) {
+ var _this = this;
+
+ Nova.request().post('/nova-vendor/nova-order-nestedset-field/' + this.resourceName, {
+ direction: direction,
+ resource: this.resourceName,
+ resourceId: this.resourceId
+ }).then(function () {
+ _this.$toasted.show(_this.__('nova-order-nestedset-field::messages.order_updated'), { type: 'success' });
+
+ _this.$router.go(_this.$router.currentRoute);
+ });
+ }
+ }
+});
+
+/***/ }),
+/* 5 */
+/***/ (function(module, exports, __webpack_require__) {
+
+var render = function() {
+ var _vm = this
+ var _h = _vm.$createElement
+ var _c = _vm._self._c || _h
+ return _c("div", { staticClass: "flex items-center" }, [
+ _vm.field.last != _vm.resourceId
+ ? _c(
+ "button",
+ {
+ staticClass: "cursor-pointer text-70 hover:text-primary mr-3",
+ on: {
+ click: function($event) {
+ return _vm.reorderResource("down")
+ }
+ }
+ },
+ [
+ _c(
+ "svg",
+ {
+ staticClass: "fill-white",
+ attrs: {
+ xmlns: "http://www.w3.org/2000/svg",
+ height: "22",
+ width: "22",
+ stroke: "currentColor",
+ "stroke-linecap": "round",
+ "stroke-linejoin": "round",
+ "stroke-width": "2",
+ viewBox: "0 0 24 24"
+ }
+ },
+ [
+ _c("circle", { attrs: { cx: "12", cy: "12", r: "10" } }),
+ _vm._v(" "),
+ _c("polyline", { attrs: { points: "8 12 12 16 16 12" } }),
+ _vm._v(" "),
+ _c("line", { attrs: { x1: "12", x2: "12", y1: "8", y2: "16" } })
+ ]
+ )
+ ]
+ )
+ : _vm._e(),
+ _vm._v(" "),
+ _vm.field.first != _vm.resourceId
+ ? _c(
+ "button",
+ {
+ staticClass: "cursor-pointer text-70 hover:text-primary",
+ on: {
+ click: function($event) {
+ return _vm.reorderResource("up")
+ }
+ }
+ },
+ [
+ _c(
+ "svg",
+ {
+ staticClass: "fill-white",
+ attrs: {
+ xmlns: "http://www.w3.org/2000/svg",
+ height: "22",
+ width: "22",
+ stroke: "currentColor",
+ "stroke-linecap": "round",
+ "stroke-linejoin": "round",
+ "stroke-width": "2",
+ viewBox: "0 0 24 24"
+ }
+ },
+ [
+ _c("circle", { attrs: { cx: "12", cy: "12", r: "10" } }),
+ _vm._v(" "),
+ _c("polyline", { attrs: { points: "16 12 12 8 8 12" } }),
+ _vm._v(" "),
+ _c("line", { attrs: { x1: "12", x2: "12", y1: "16", y2: "8" } })
+ ]
+ )
+ ]
+ )
+ : _vm._e()
+ ])
+}
+var staticRenderFns = []
+render._withStripped = true
+module.exports = { render: render, staticRenderFns: staticRenderFns }
+if (false) {
+ module.hot.accept()
+ if (module.hot.data) {
+ require("vue-hot-reload-api") .rerender("data-v-9e63f81a", module.exports)
+ }
+}
+
+/***/ }),
+/* 6 */
+/***/ (function(module, exports) {
+
+// removed by extract-text-webpack-plugin
+
+/***/ })
+/******/ ]);
\ No newline at end of file
diff --git a/dist/mix-manifest.json b/dist/mix-manifest.json
new file mode 100644
index 0000000..5cd31d2
--- /dev/null
+++ b/dist/mix-manifest.json
@@ -0,0 +1,4 @@
+{
+ "/js/field.js": "/js/field.js",
+ "/css/field.css": "/css/field.css"
+}
\ No newline at end of file
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..6f91618
--- /dev/null
+++ b/package.json
@@ -0,0 +1,20 @@
+{
+ "private": true,
+ "scripts": {
+ "dev": "npm run development",
+ "development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
+ "watch": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --watch --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
+ "watch-poll": "npm run watch -- --watch-poll",
+ "hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js",
+ "prod": "npm run production",
+ "production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js"
+ },
+ "devDependencies": {
+ "cross-env": "^5.0.0",
+ "laravel-mix": "^1.0",
+ "laravel-nova": "^1.0"
+ },
+ "dependencies": {
+ "vue": "^2.5.0"
+ }
+}
diff --git a/resources/js/components/IndexField.vue b/resources/js/components/IndexField.vue
new file mode 100644
index 0000000..fcbb743
--- /dev/null
+++ b/resources/js/components/IndexField.vue
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+
diff --git a/resources/js/field.js b/resources/js/field.js
new file mode 100644
index 0000000..f2721b8
--- /dev/null
+++ b/resources/js/field.js
@@ -0,0 +1,3 @@
+Nova.booting((Vue, router, store) => {
+ Vue.component('index-order-nestedset-field', require('./components/IndexField'))
+});
diff --git a/resources/lang/en/errors.php b/resources/lang/en/errors.php
new file mode 100644
index 0000000..b196ff7
--- /dev/null
+++ b/resources/lang/en/errors.php
@@ -0,0 +1,6 @@
+ 'Error : model :model should implements following trait :class',
+ 'bad_direction' => 'Error : bad direction',
+];
diff --git a/resources/lang/en/messages.php b/resources/lang/en/messages.php
new file mode 100644
index 0000000..290cb9d
--- /dev/null
+++ b/resources/lang/en/messages.php
@@ -0,0 +1,5 @@
+ 'Position successfully updated.',
+];
diff --git a/resources/lang/fr/errors.php b/resources/lang/fr/errors.php
new file mode 100644
index 0000000..a2786f1
--- /dev/null
+++ b/resources/lang/fr/errors.php
@@ -0,0 +1,6 @@
+ 'Erreur : le modèle :model doit implémenter le trait :class',
+ 'bad_direction' => 'Erreur : mauvaise direction spécifiée',
+];
diff --git a/resources/lang/fr/messages.php b/resources/lang/fr/messages.php
new file mode 100644
index 0000000..eafb9b4
--- /dev/null
+++ b/resources/lang/fr/messages.php
@@ -0,0 +1,5 @@
+ 'L\'ordre a bien été modifié.',
+];
diff --git a/resources/sass/field.scss b/resources/sass/field.scss
new file mode 100644
index 0000000..f85ad40
--- /dev/null
+++ b/resources/sass/field.scss
@@ -0,0 +1 @@
+// Nova Tool CSS
diff --git a/routes/api.php b/routes/api.php
new file mode 100644
index 0000000..b182e23
--- /dev/null
+++ b/routes/api.php
@@ -0,0 +1,5 @@
+get('resourceId');
+ $model = $request->findModelOrFail($resourceId);
+
+ if (!in_array(Orderable::class, class_uses($model))) {
+ abort(500, trans('nova-order-nestedset-field::errors.model_should_use_trait', [
+ 'class' => Orderable::class,
+ 'model' => get_class($model),
+ ]));
+ }
+
+ if (!in_array(NodeTrait::class, class_uses($model))) {
+ abort(500, trans('nova-order-nestedset-field::errors.model_should_use_trait', [
+ 'class' => NodeTrait::class,
+ 'model' => get_class($model),
+ ]));
+ }
+
+ $direction = (string) $request->get('direction', '');
+ if (!in_array($direction, ['up', 'down'])) {
+ abort(500, trans('nova-order-nestedset-field::errors.bad_direction'));
+ }
+
+ if ($direction === 'up') {
+ $model->moveOrderUp();
+ } else {
+ $model->moveOrderDown();
+ }
+ }
+}
diff --git a/src/OrderNestedsetField.php b/src/OrderNestedsetField.php
new file mode 100644
index 0000000..cab13a6
--- /dev/null
+++ b/src/OrderNestedsetField.php
@@ -0,0 +1,59 @@
+ Orderable::class,
+ 'model' => get_class($resource),
+ ]));
+ }
+
+ if (!in_array(NodeTrait::class, class_uses($resource))) {
+ abort(500, trans('nova-order-nestedset-field::errors.model_should_use_trait', [
+ 'class' => NodeTrait::class,
+ 'model' => get_class($resource),
+ ]));
+ }
+
+ $first = $resource->buildSortQuery()->ordered()->first();
+ $last = $resource->buildSortQuery()->ordered('desc')->first();
+
+ $this->withMeta([
+ 'first' => is_null($first) ? null : $first->id,
+ 'last' => is_null($last) ? null : $last->id,
+ ]);
+
+ return data_get($resource, $attribute);
+ }
+}
diff --git a/src/OrderNestedsetFieldServiceProvider.php b/src/OrderNestedsetFieldServiceProvider.php
new file mode 100644
index 0000000..f5daf34
--- /dev/null
+++ b/src/OrderNestedsetFieldServiceProvider.php
@@ -0,0 +1,57 @@
+app->booted(function () {
+ $this->routes();
+ });
+
+ Nova::serving(function (ServingNova $event) {
+ Nova::script('laravel-nova-order-nestedset-field', __DIR__.'/../dist/js/field.js');
+ Nova::style('laravel-nova-order-nestedset-field', __DIR__.'/../dist/css/field.css');
+ });
+
+ $this->loadTranslationsFrom(__DIR__.'/../resources/lang', 'nova-order-nestedset-field');
+ $this->publishes([__DIR__.'/../resources/lang' => resource_path('lang/vendor/nova-order-nestedset-field')], 'lang');
+ }
+
+ /**
+ * Register any application services.
+ *
+ * @return void
+ */
+ public function register()
+ {
+ //
+ }
+
+ /**
+ * Register the tool's routes.
+ *
+ * @return void
+ */
+ protected function routes()
+ {
+ if ($this->app->routesAreCached()) {
+ return;
+ }
+
+ Route::middleware(['nova'])
+ ->prefix('nova-vendor/nova-order-nestedset-field')
+ ->group(__DIR__.'/../routes/api.php');
+ }
+}
diff --git a/src/Traits/Orderable.php b/src/Traits/Orderable.php
new file mode 100644
index 0000000..9b61028
--- /dev/null
+++ b/src/Traits/Orderable.php
@@ -0,0 +1,43 @@
+getPrevSibling();
+ if (!empty($prevItem)) {
+ $this->insertBeforeNode($prevItem);
+ }
+ }
+
+ public function moveOrderDown()
+ {
+ $nextItem = $this->getNextSibling();
+ if (!empty($nextItem)) {
+ $this->insertAfterNode($nextItem);
+ }
+ }
+
+ public function buildSortQuery()
+ {
+ $query = static::query()->where($this->getParentIdName(), $this->getParentId());
+ if (!empty($this->getScopeAttributes())) {
+ foreach ($this->getScopeAttributes() as $attributeName) {
+ if (!empty($this->{$attributeName})) {
+ $query->where($attributeName, $this->{$attributeName});
+ }
+ }
+ }
+
+ return $query;
+ }
+
+ public function scopeOrdered(Builder $query, string $direction = 'asc')
+ {
+ return $query->orderBy($this->getLftName(), $direction);
+ }
+}
diff --git a/webpack.mix.js b/webpack.mix.js
new file mode 100644
index 0000000..d9623de
--- /dev/null
+++ b/webpack.mix.js
@@ -0,0 +1,5 @@
+let mix = require('laravel-mix')
+
+mix.setPublicPath('dist')
+ .js('resources/js/field.js', 'js')
+ .sass('resources/sass/field.scss', 'css')