Bladeren bron

Merge branch 'jqhph:2.0' into 2.0

dreamdesky 3 jaren geleden
bovenliggende
commit
cc85774904
55 gewijzigde bestanden met toevoegingen van 1981 en 660 verwijderingen
  1. 18 0
      .editorconfig
  2. 46 0
      .github/workflows/dusk.yml
  3. 1 1
      README.md
  4. 1 1
      composer.json
  5. 8 1
      resources/assets/dcat/js/bootstrappers/Pjax.js
  6. 2 2
      resources/assets/dcat/js/dcat-app.js
  7. 9 2
      resources/assets/dcat/js/extensions/Ajax.js
  8. 1008 0
      resources/assets/dcat/plugins/autocomplete/jquery.autocomplete.js
  9. 7 0
      resources/assets/dcat/plugins/autocomplete/jquery.autocomplete.min.js
  10. 0 0
      resources/dist/adminlte/adminlte-blue-light.css
  11. 0 0
      resources/dist/adminlte/adminlte-blue.css
  12. 0 0
      resources/dist/adminlte/adminlte-green.css
  13. 0 0
      resources/dist/adminlte/adminlte.js
  14. 0 0
      resources/dist/dcat/extra/grid-extend.js
  15. 0 0
      resources/dist/dcat/extra/upload.js
  16. 0 0
      resources/dist/dcat/extra/upload.js.map
  17. 0 0
      resources/dist/dcat/js/dcat-app.js
  18. 0 0
      resources/dist/dcat/js/dcat-app.js.map
  19. 1 1
      resources/views/form/autocomplete.blade.php
  20. 19 18
      resources/views/form/hasmany.blade.php
  21. 18 14
      resources/views/form/hasmanytable.blade.php
  22. 1 1
      resources/views/grid/displayer/switch.blade.php
  23. 539 538
      resources/views/helpers/scaffold.blade.php
  24. 1 1
      src/Admin.php
  25. 1 0
      src/Application.php
  26. 24 9
      src/Console/PublishCommand.php
  27. 1 1
      src/Console/stubs/config.stub
  28. 1 2
      src/Extend/Manager.php
  29. 1 1
      src/Form.php
  30. 4 2
      src/Form/Concerns/HasTabs.php
  31. 3 3
      src/Form/Field.php
  32. 3 1
      src/Form/Field/ArrayField.php
  33. 4 4
      src/Form/Field/Autocomplete.php
  34. 50 2
      src/Form/Field/HasMany.php
  35. 0 12
      src/Form/Field/Image.php
  36. 4 2
      src/Form/Field/ImageField.php
  37. 4 0
      src/Form/Field/UploadField.php
  38. 29 5
      src/Form/NestedForm.php
  39. 4 3
      src/Grid/Exporters/AbstractExporter.php
  40. 3 0
      src/Grid/Filter.php
  41. 1 1
      src/Grid/Filter/AbstractFilter.php
  42. 38 0
      src/Grid/Filter/FindInSet.php
  43. 6 1
      src/Grid/Model.php
  44. 1 1
      src/Http/Controllers/PermissionController.php
  45. 4 1
      src/Http/Controllers/ScaffoldController.php
  46. 1 11
      src/Http/Middleware/Authenticate.php
  47. 2 2
      src/Http/Middleware/Session.php
  48. 3 3
      src/Scaffold/MigrationCreator.php
  49. 17 0
      src/Show/Field.php
  50. 30 0
      src/Support/Helper.php
  51. 38 0
      src/Support/helpers.php
  52. 18 6
      src/Traits/ModelTree.php
  53. 4 4
      src/Widgets/DialogForm.php
  54. 2 2
      tests/Browser/Components/Form/Field/HasMany.php
  55. 1 1
      tests/bin/install-admin.sh

+ 18 - 0
.editorconfig

@@ -0,0 +1,18 @@
+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
+
+[*.{yml,yaml}]
+indent_size = 2
+
+[docker-compose.yml]
+indent_size = 4

+ 46 - 0
.github/workflows/dusk.yml

@@ -190,3 +190,49 @@ jobs:
 
       - name: Run test suite
         run: cd ./laravel-tests && php artisan dusk
+
+  laravel9:
+    runs-on: ubuntu-latest
+
+    steps:
+      - uses: shivammathur/setup-php@b7d1d9c9a92d8d8463ce36d7f60da34d461724f8
+        with:
+          php-version: '8.0'
+
+      - uses: actions/checkout@v2
+
+      - name: Composer Update
+        run: composer self-update --2
+
+      - name: Setup MySQL
+        # You may pin to the exact commit or the version.
+        # uses: mirromutth/mysql-action@de1fba8b3f90ce8db80f663a7043be3cf3231248
+        uses: mirromutth/mysql-action@v1.1
+        with:
+          # Version of MySQL to use
+          mysql version: 5.7
+          # MYSQL_ROOT_PASSWORD - root superuser password
+          mysql root password: 123456
+          # MYSQL_DATABASE - name for the default database that is created
+          mysql database: laravel
+          # MYSQL_USER - create the specified user with superuser power for created database
+          mysql user: root
+          # MYSQL_PASSWORD - specified superuser password which user is power for created database
+          mysql password: 123456
+
+      - name: Install Dependencies
+        run: |
+          composer create-project --prefer-dist laravel/laravel laravel-tests 9.*
+          sh ./tests/bin/install-dep.sh
+
+      - name: Install Admin
+        run: sh ./tests/bin/install-admin.sh
+
+      - name: Install Xvfb
+        run: sudo apt-get install xvfb
+
+      - name: Start Server
+        run: sh ./tests/bin/start.sh
+
+      - name: Run test suite
+        run: cd ./laravel-tests && php artisan dusk

+ 1 - 1
README.md

@@ -62,7 +62,7 @@
 
 ### 环境
  - PHP >= 7.1.0
- - Laravel 5.5.0 ~ 8.*
+ - Laravel 5.5.0 ~ 9.*
  - Fileinfo PHP Extension
 
 ### 安装

+ 1 - 1
composer.json

@@ -13,7 +13,7 @@
     ],
     "require": {
         "php": ">=7.1.0",
-        "laravel/framework": "~5.5|~6.0|~7.0|~8.0",
+        "laravel/framework": "~5.5|~6.0|~7.0|~8.0|~9.0",
         "spatie/eloquent-sortable": "3.*|4.*",
         "doctrine/dbal": "^2.6|^3.0"
     },

+ 8 - 1
resources/assets/dcat/js/bootstrappers/Pjax.js

@@ -46,9 +46,16 @@ export default class Pjax {
                 $(formContainer).find('[type="submit"],.submit').buttonLoading(false)
             }
 
+            var $body = $('body');
+
             // 移除遮罩层
             $(".modal-backdrop").remove();
-            $("body").removeClass("modal-open");
+            $body.removeClass("modal-open");
+
+            // 刷新页面后需要重置modal弹窗设置的间隔
+            if ($body.css('padding-right')) {
+                $body.css('padding-right', '');
+            }
         });
 
         $d.on('pjax:loaded', () => {

+ 2 - 2
resources/assets/dcat/js/dcat-app.js

@@ -95,8 +95,6 @@ function listen(Dcat) {
         new Footer(Dcat);
         // data-action 动作绑定(包括删除、批量删除等操作)
         new DataActions(Dcat);
-        // pjax初始化功能
-        new Pjax(Dcat);
     });
 
     // 每个请求都初始化
@@ -109,6 +107,8 @@ function listen(Dcat) {
                 'X-CSRF-TOKEN': Dcat.token
             }
         });
+        // pjax初始化功能
+        new Pjax(Dcat);
     });
 }
 

+ 9 - 2
resources/assets/dcat/js/extensions/Ajax.js

@@ -74,10 +74,17 @@ export default class Ajax {
             case 403:
                 return Dcat.error(_msg || (Dcat.lang['403'] || 'Permission deny!'));
             case 401:
-                if (json.login) {
-                    return location.href = json.login;
+                if (json.redirect) {
+                    return location.href = json.redirect;
                 }
                 return Dcat.error(Dcat.lang['401'] || 'Unauthorized.');
+            case 301:
+            case 302:
+                console.log('admin redirect', json);
+                if (json.redirect) {
+                    return location.href = json.redirect;
+                }
+                return;
             case 419:
                 return Dcat.error(Dcat.lang['419'] || 'Sorry, your page has expired.');
 

+ 1008 - 0
resources/assets/dcat/plugins/autocomplete/jquery.autocomplete.js

@@ -0,0 +1,1008 @@
+/**
+*  Ajax Autocomplete for jQuery, version 1.4.11
+*  (c) 2017 Tomas Kirda
+*
+*  Ajax Autocomplete for jQuery is freely distributable under the terms of an MIT-style license.
+*  For details, see the web site: https://github.com/devbridge/jQuery-Autocomplete
+*/
+
+/*jslint  browser: true, white: true, single: true, this: true, multivar: true */
+/*global define, window, document, jQuery, exports, require */
+
+// Expose plugin as an AMD module if AMD loader is present:
+(function (factory) {
+    "use strict";
+    if (typeof define === 'function' && define.amd) {
+        // AMD. Register as an anonymous module.
+        define(['jquery'], factory);
+    } else if (typeof exports === 'object' && typeof require === 'function') {
+        // Browserify
+        factory(require('jquery'));
+    } else {
+        // Browser globals
+        factory(jQuery);
+    }
+}(function ($) {
+    'use strict';
+
+    var
+        utils = (function () {
+            return {
+                escapeRegExChars: function (value) {
+                    return value.replace(/[|\\{}()[\]^$+*?.]/g, "\\$&");
+                },
+                createNode: function (containerClass) {
+                    var div = document.createElement('div');
+                    div.className = containerClass;
+                    div.style.position = 'absolute';
+                    div.style.display = 'none';
+                    return div;
+                }
+            };
+        }()),
+
+        keys = {
+            ESC: 27,
+            TAB: 9,
+            RETURN: 13,
+            LEFT: 37,
+            UP: 38,
+            RIGHT: 39,
+            DOWN: 40
+        },
+
+        noop = $.noop;
+
+    function Autocomplete(el, options) {
+        var that = this;
+
+        // Shared variables:
+        that.element = el;
+        that.el = $(el);
+        that.suggestions = [];
+        that.badQueries = [];
+        that.selectedIndex = -1;
+        that.currentValue = that.element.value;
+        that.timeoutId = null;
+        that.cachedResponse = {};
+        that.onChangeTimeout = null;
+        that.onChange = null;
+        that.isLocal = false;
+        that.suggestionsContainer = null;
+        that.noSuggestionsContainer = null;
+        that.options = $.extend(true, {}, Autocomplete.defaults, options);
+        that.classes = {
+            selected: 'autocomplete-selected',
+            suggestion: 'autocomplete-suggestion'
+        };
+        that.hint = null;
+        that.hintValue = '';
+        that.selection = null;
+
+        // Initialize and set options:
+        that.initialize();
+        that.setOptions(options);
+    }
+
+    Autocomplete.utils = utils;
+
+    $.Autocomplete = Autocomplete;
+
+    Autocomplete.defaults = {
+            ajaxSettings: {},
+            autoSelectFirst: false,
+            appendTo: 'body',
+            serviceUrl: null,
+            lookup: null,
+            onSelect: null,
+            width: 'auto',
+            minChars: 1,
+            maxHeight: 300,
+            deferRequestBy: 0,
+            params: {},
+            formatResult: _formatResult,
+            formatGroup: _formatGroup,
+            delimiter: null,
+            zIndex: 9999,
+            type: 'GET',
+            noCache: false,
+            onSearchStart: noop,
+            onSearchComplete: noop,
+            onSearchError: noop,
+            preserveInput: false,
+            containerClass: 'autocomplete-suggestions',
+            tabDisabled: false,
+            dataType: 'text',
+            currentRequest: null,
+            triggerSelectOnValidInput: true,
+            preventBadQueries: true,
+            lookupFilter: _lookupFilter,
+            paramName: 'query',
+            transformResult: _transformResult,
+            showNoSuggestionNotice: false,
+            noSuggestionNotice: 'No results',
+            orientation: 'bottom',
+            forceFixPosition: false
+    };
+
+    function _lookupFilter(suggestion, originalQuery, queryLowerCase) {
+        return suggestion.value.toLowerCase().indexOf(queryLowerCase) !== -1;
+    };
+
+    function _transformResult(response) {
+        return typeof response === 'string' ? $.parseJSON(response) : response;
+    };
+
+    function _formatResult(suggestion, currentValue) {
+        // Do not replace anything if the current value is empty
+        if (!currentValue) {
+            return suggestion.value;
+        }
+
+        var pattern = '(' + utils.escapeRegExChars(currentValue) + ')';
+
+        return suggestion.value
+            .replace(new RegExp(pattern, 'gi'), '<strong>$1<\/strong>')
+            .replace(/&/g, '&amp;')
+            .replace(/</g, '&lt;')
+            .replace(/>/g, '&gt;')
+            .replace(/"/g, '&quot;')
+            .replace(/&lt;(\/?strong)&gt;/g, '<$1>');
+    };
+
+    function _formatGroup(suggestion, category) {
+        return '<div class="autocomplete-group">' + category + '</div>';
+    };
+
+    Autocomplete.prototype = {
+
+        initialize: function () {
+            var that = this,
+                suggestionSelector = '.' + that.classes.suggestion,
+                selected = that.classes.selected,
+                options = that.options,
+                container;
+
+            that.element.setAttribute('autocomplete', 'off');
+
+            // html() deals with many types: htmlString or Element or Array or jQuery
+            that.noSuggestionsContainer = $('<div class="autocomplete-no-suggestion"></div>')
+                                          .html(this.options.noSuggestionNotice).get(0);
+
+            that.suggestionsContainer = Autocomplete.utils.createNode(options.containerClass);
+
+            container = $(that.suggestionsContainer);
+
+            container.appendTo(options.appendTo || 'body');
+
+            // Only set width if it was provided:
+            if (options.width !== 'auto') {
+                container.css('width', options.width);
+            }
+
+            // Listen for mouse over event on suggestions list:
+            container.on('mouseover.autocomplete', suggestionSelector, function () {
+                that.activate($(this).data('index'));
+            });
+
+            // Deselect active element when mouse leaves suggestions container:
+            container.on('mouseout.autocomplete', function () {
+                that.selectedIndex = -1;
+                container.children('.' + selected).removeClass(selected);
+            });
+
+            // Listen for click event on suggestions list:
+            container.on('click.autocomplete', suggestionSelector, function () {
+                that.select($(this).data('index'));
+            });
+
+            container.on('click.autocomplete', function () {
+                clearTimeout(that.blurTimeoutId);
+            })
+
+            that.fixPositionCapture = function () {
+                if (that.visible) {
+                    that.fixPosition();
+                }
+            };
+
+            $(window).on('resize.autocomplete', that.fixPositionCapture);
+
+            that.el.on('keydown.autocomplete', function (e) { that.onKeyPress(e); });
+            that.el.on('keyup.autocomplete', function (e) { that.onKeyUp(e); });
+            that.el.on('blur.autocomplete', function () { that.onBlur(); });
+            that.el.on('focus.autocomplete', function () { that.onFocus(); });
+            that.el.on('change.autocomplete', function (e) { that.onKeyUp(e); });
+            that.el.on('input.autocomplete', function (e) { that.onKeyUp(e); });
+        },
+
+        onFocus: function () {
+            var that = this;
+
+            if (that.disabled) {
+                return;
+            }
+
+            that.fixPosition();
+
+            if (that.el.val().length >= that.options.minChars) {
+                that.onValueChange();
+            }
+        },
+
+        onBlur: function () {
+            var that = this,
+                options = that.options,
+                value = that.el.val(),
+                query = that.getQuery(value);
+
+            // If user clicked on a suggestion, hide() will
+            // be canceled, otherwise close suggestions
+            that.blurTimeoutId = setTimeout(function () {
+                that.hide();
+
+                if (that.selection && that.currentValue !== query) {
+                    (options.onInvalidateSelection || $.noop).call(that.element);
+                }
+            }, 200);
+        },
+
+        abortAjax: function () {
+            var that = this;
+            if (that.currentRequest) {
+                that.currentRequest.abort();
+                that.currentRequest = null;
+            }
+        },
+
+        setOptions: function (suppliedOptions) {
+            var that = this,
+                options = $.extend({}, that.options, suppliedOptions);
+
+            that.isLocal = Array.isArray(options.lookup);
+
+            if (that.isLocal) {
+                options.lookup = that.verifySuggestionsFormat(options.lookup);
+            }
+
+            options.orientation = that.validateOrientation(options.orientation, 'bottom');
+
+            // Adjust height, width and z-index:
+            $(that.suggestionsContainer).css({
+                'max-height': options.maxHeight + 'px',
+                'width': options.width + 'px',
+                'z-index': options.zIndex
+            });
+
+            this.options = options;
+        },
+
+
+        clearCache: function () {
+            this.cachedResponse = {};
+            this.badQueries = [];
+        },
+
+        clear: function () {
+            this.clearCache();
+            this.currentValue = '';
+            this.suggestions = [];
+        },
+
+        disable: function () {
+            var that = this;
+            that.disabled = true;
+            clearTimeout(that.onChangeTimeout);
+            that.abortAjax();
+        },
+
+        enable: function () {
+            this.disabled = false;
+        },
+
+        fixPosition: function () {
+            // Use only when container has already its content
+
+            var that = this,
+                $container = $(that.suggestionsContainer),
+                containerParent = $container.parent().get(0);
+            // Fix position automatically when appended to body.
+            // In other cases force parameter must be given.
+            if (containerParent !== document.body && !that.options.forceFixPosition) {
+                return;
+            }
+
+            // Choose orientation
+            var orientation = that.options.orientation,
+                containerHeight = $container.outerHeight(),
+                height = that.el.outerHeight(),
+                offset = that.el.offset(),
+                styles = { 'top': offset.top, 'left': offset.left };
+
+            if (orientation === 'auto') {
+                var viewPortHeight = $(window).height(),
+                    scrollTop = $(window).scrollTop(),
+                    topOverflow = -scrollTop + offset.top - containerHeight,
+                    bottomOverflow = scrollTop + viewPortHeight - (offset.top + height + containerHeight);
+
+                orientation = (Math.max(topOverflow, bottomOverflow) === topOverflow) ? 'top' : 'bottom';
+            }
+
+            if (orientation === 'top') {
+                styles.top += -containerHeight;
+            } else {
+                styles.top += height;
+            }
+
+            // If container is not positioned to body,
+            // correct its position using offset parent offset
+            if(containerParent !== document.body) {
+                var opacity = $container.css('opacity'),
+                    parentOffsetDiff;
+
+                    if (!that.visible){
+                        $container.css('opacity', 0).show();
+                    }
+
+                parentOffsetDiff = $container.offsetParent().offset();
+                styles.top -= parentOffsetDiff.top;
+                styles.top += containerParent.scrollTop;
+                styles.left -= parentOffsetDiff.left;
+
+                if (!that.visible){
+                    $container.css('opacity', opacity).hide();
+                }
+            }
+
+            if (that.options.width === 'auto') {
+                styles.width = that.el.outerWidth() + 'px';
+            }
+
+            $container.css(styles);
+        },
+
+        isCursorAtEnd: function () {
+            var that = this,
+                valLength = that.el.val().length,
+                selectionStart = that.element.selectionStart,
+                range;
+
+            if (typeof selectionStart === 'number') {
+                return selectionStart === valLength;
+            }
+            if (document.selection) {
+                range = document.selection.createRange();
+                range.moveStart('character', -valLength);
+                return valLength === range.text.length;
+            }
+            return true;
+        },
+
+        onKeyPress: function (e) {
+            var that = this;
+
+            // If suggestions are hidden and user presses arrow down, display suggestions:
+            if (!that.disabled && !that.visible && e.which === keys.DOWN && that.currentValue) {
+                that.suggest();
+                return;
+            }
+
+            if (that.disabled || !that.visible) {
+                return;
+            }
+
+            switch (e.which) {
+                case keys.ESC:
+                    that.el.val(that.currentValue);
+                    that.hide();
+                    break;
+                case keys.RIGHT:
+                    if (that.hint && that.options.onHint && that.isCursorAtEnd()) {
+                        that.selectHint();
+                        break;
+                    }
+                    return;
+                case keys.TAB:
+                    if (that.hint && that.options.onHint) {
+                        that.selectHint();
+                        return;
+                    }
+                    if (that.selectedIndex === -1) {
+                        that.hide();
+                        return;
+                    }
+                    that.select(that.selectedIndex);
+                    if (that.options.tabDisabled === false) {
+                        return;
+                    }
+                    break;
+                case keys.RETURN:
+                    if (that.selectedIndex === -1) {
+                        that.hide();
+                        return;
+                    }
+                    that.select(that.selectedIndex);
+                    break;
+                case keys.UP:
+                    that.moveUp();
+                    break;
+                case keys.DOWN:
+                    that.moveDown();
+                    break;
+                default:
+                    return;
+            }
+
+            // Cancel event if function did not return:
+            e.stopImmediatePropagation();
+            e.preventDefault();
+        },
+
+        onKeyUp: function (e) {
+            var that = this;
+
+            if (that.disabled) {
+                return;
+            }
+
+            switch (e.which) {
+                case keys.UP:
+                case keys.DOWN:
+                    return;
+            }
+
+            clearTimeout(that.onChangeTimeout);
+
+            if (that.currentValue !== that.el.val()) {
+                that.findBestHint();
+                if (that.options.deferRequestBy > 0) {
+                    // Defer lookup in case when value changes very quickly:
+                    that.onChangeTimeout = setTimeout(function () {
+                        that.onValueChange();
+                    }, that.options.deferRequestBy);
+                } else {
+                    that.onValueChange();
+                }
+            }
+        },
+
+        onValueChange: function () {
+            if (this.ignoreValueChange) {
+                this.ignoreValueChange = false;
+                return;
+            }
+
+            var that = this,
+                options = that.options,
+                value = that.el.val(),
+                query = that.getQuery(value);
+
+            if (that.selection && that.currentValue !== query) {
+                that.selection = null;
+                (options.onInvalidateSelection || $.noop).call(that.element);
+            }
+
+            clearTimeout(that.onChangeTimeout);
+            that.currentValue = value;
+            that.selectedIndex = -1;
+
+            // Check existing suggestion for the match before proceeding:
+            if (options.triggerSelectOnValidInput && that.isExactMatch(query)) {
+                that.select(0);
+                return;
+            }
+
+            if (query.length < options.minChars) {
+                that.hide();
+            } else {
+                that.getSuggestions(query);
+            }
+        },
+
+        isExactMatch: function (query) {
+            var suggestions = this.suggestions;
+
+            return (suggestions.length === 1 && suggestions[0].value.toLowerCase() === query.toLowerCase());
+        },
+
+        getQuery: function (value) {
+            var delimiter = this.options.delimiter,
+                parts;
+
+            if (!delimiter) {
+                return value;
+            }
+            parts = value.split(delimiter);
+            return $.trim(parts[parts.length - 1]);
+        },
+
+        getSuggestionsLocal: function (query) {
+            var that = this,
+                options = that.options,
+                queryLowerCase = query.toLowerCase(),
+                filter = options.lookupFilter,
+                limit = parseInt(options.lookupLimit, 10),
+                data;
+
+            data = {
+                suggestions: $.grep(options.lookup, function (suggestion) {
+                    return filter(suggestion, query, queryLowerCase);
+                })
+            };
+
+            if (limit && data.suggestions.length > limit) {
+                data.suggestions = data.suggestions.slice(0, limit);
+            }
+
+            return data;
+        },
+
+        getSuggestions: function (q) {
+            var response,
+                that = this,
+                options = that.options,
+                serviceUrl = options.serviceUrl,
+                params,
+                cacheKey,
+                ajaxSettings;
+
+            options.params[options.paramName] = q;
+
+            if (options.onSearchStart.call(that.element, options.params) === false) {
+                return;
+            }
+
+            params = options.ignoreParams ? null : options.params;
+
+            if ($.isFunction(options.lookup)){
+                options.lookup(q, function (data) {
+                    that.suggestions = data.suggestions;
+                    that.suggest();
+                    options.onSearchComplete.call(that.element, q, data.suggestions);
+                });
+                return;
+            }
+
+            if (that.isLocal) {
+                response = that.getSuggestionsLocal(q);
+            } else {
+                if ($.isFunction(serviceUrl)) {
+                    serviceUrl = serviceUrl.call(that.element, q);
+                }
+                cacheKey = serviceUrl + '?' + $.param(params || {});
+                response = that.cachedResponse[cacheKey];
+            }
+
+            if (response && Array.isArray(response.suggestions)) {
+                that.suggestions = response.suggestions;
+                that.suggest();
+                options.onSearchComplete.call(that.element, q, response.suggestions);
+            } else if (!that.isBadQuery(q)) {
+                that.abortAjax();
+
+                ajaxSettings = {
+                    url: serviceUrl,
+                    data: params,
+                    type: options.type,
+                    dataType: options.dataType
+                };
+
+                $.extend(ajaxSettings, options.ajaxSettings);
+
+                that.currentRequest = $.ajax(ajaxSettings).done(function (data) {
+                    var result;
+                    that.currentRequest = null;
+                    result = options.transformResult(data, q);
+                    that.processResponse(result, q, cacheKey);
+                    options.onSearchComplete.call(that.element, q, result.suggestions);
+                }).fail(function (jqXHR, textStatus, errorThrown) {
+                    options.onSearchError.call(that.element, q, jqXHR, textStatus, errorThrown);
+                });
+            } else {
+                options.onSearchComplete.call(that.element, q, []);
+            }
+        },
+
+        isBadQuery: function (q) {
+            if (!this.options.preventBadQueries){
+                return false;
+            }
+
+            var badQueries = this.badQueries,
+                i = badQueries.length;
+
+            while (i--) {
+                if (q.indexOf(badQueries[i]) === 0) {
+                    return true;
+                }
+            }
+
+            return false;
+        },
+
+        hide: function () {
+            var that = this,
+                container = $(that.suggestionsContainer);
+
+            if ($.isFunction(that.options.onHide) && that.visible) {
+                that.options.onHide.call(that.element, container);
+            }
+
+            that.visible = false;
+            that.selectedIndex = -1;
+            clearTimeout(that.onChangeTimeout);
+            $(that.suggestionsContainer).hide();
+            that.signalHint(null);
+        },
+
+        suggest: function () {
+            if (!this.suggestions.length) {
+                if (this.options.showNoSuggestionNotice) {
+                    this.noSuggestions();
+                } else {
+                    this.hide();
+                }
+                return;
+            }
+
+            var that = this,
+                options = that.options,
+                groupBy = options.groupBy,
+                formatResult = options.formatResult,
+                value = that.getQuery(that.currentValue),
+                className = that.classes.suggestion,
+                classSelected = that.classes.selected,
+                container = $(that.suggestionsContainer),
+                noSuggestionsContainer = $(that.noSuggestionsContainer),
+                beforeRender = options.beforeRender,
+                html = '',
+                category,
+                formatGroup = function (suggestion, index) {
+                        var currentCategory = suggestion.data[groupBy];
+
+                        if (category === currentCategory){
+                            return '';
+                        }
+
+                        category = currentCategory;
+
+                        return options.formatGroup(suggestion, category);
+                    };
+
+            if (options.triggerSelectOnValidInput && that.isExactMatch(value)) {
+                that.select(0);
+                return;
+            }
+
+            // Build suggestions inner HTML:
+            $.each(that.suggestions, function (i, suggestion) {
+                if (groupBy){
+                    html += formatGroup(suggestion, value, i);
+                }
+
+                html += '<div class="' + className + '" data-index="' + i + '">' + formatResult(suggestion, value, i) + '</div>';
+            });
+
+            this.adjustContainerWidth();
+
+            noSuggestionsContainer.detach();
+            container.html(html);
+
+            if ($.isFunction(beforeRender)) {
+                beforeRender.call(that.element, container, that.suggestions);
+            }
+
+            that.fixPosition();
+            container.show();
+
+            // Select first value by default:
+            if (options.autoSelectFirst) {
+                that.selectedIndex = 0;
+                container.scrollTop(0);
+                container.children('.' + className).first().addClass(classSelected);
+            }
+
+            that.visible = true;
+            that.findBestHint();
+        },
+
+        noSuggestions: function() {
+             var that = this,
+                 beforeRender = that.options.beforeRender,
+                 container = $(that.suggestionsContainer),
+                 noSuggestionsContainer = $(that.noSuggestionsContainer);
+
+            this.adjustContainerWidth();
+
+            // Some explicit steps. Be careful here as it easy to get
+            // noSuggestionsContainer removed from DOM if not detached properly.
+            noSuggestionsContainer.detach();
+
+            // clean suggestions if any
+            container.empty();
+            container.append(noSuggestionsContainer);
+
+            if ($.isFunction(beforeRender)) {
+                beforeRender.call(that.element, container, that.suggestions);
+            }
+
+            that.fixPosition();
+
+            container.show();
+            that.visible = true;
+        },
+
+        adjustContainerWidth: function() {
+            var that = this,
+                options = that.options,
+                width,
+                container = $(that.suggestionsContainer);
+
+            // If width is auto, adjust width before displaying suggestions,
+            // because if instance was created before input had width, it will be zero.
+            // Also it adjusts if input width has changed.
+            if (options.width === 'auto') {
+                width = that.el.outerWidth();
+                container.css('width', width > 0 ? width : 300);
+            } else if(options.width === 'flex') {
+                // Trust the source! Unset the width property so it will be the max length
+                // the containing elements.
+                container.css('width', '');
+            }
+        },
+
+        findBestHint: function () {
+            var that = this,
+                value = that.el.val().toLowerCase(),
+                bestMatch = null;
+
+            if (!value) {
+                return;
+            }
+
+            $.each(that.suggestions, function (i, suggestion) {
+                var foundMatch = suggestion.value.toLowerCase().indexOf(value) === 0;
+                if (foundMatch) {
+                    bestMatch = suggestion;
+                }
+                return !foundMatch;
+            });
+
+            that.signalHint(bestMatch);
+        },
+
+        signalHint: function (suggestion) {
+            var hintValue = '',
+                that = this;
+            if (suggestion) {
+                hintValue = that.currentValue + suggestion.value.substr(that.currentValue.length);
+            }
+            if (that.hintValue !== hintValue) {
+                that.hintValue = hintValue;
+                that.hint = suggestion;
+                (this.options.onHint || $.noop)(hintValue);
+            }
+        },
+
+        verifySuggestionsFormat: function (suggestions) {
+            // If suggestions is string array, convert them to supported format:
+            if (suggestions.length && typeof suggestions[0] === 'string') {
+                return $.map(suggestions, function (value) {
+                    return { value: value, data: null };
+                });
+            }
+
+            return suggestions;
+        },
+
+        validateOrientation: function(orientation, fallback) {
+            orientation = $.trim(orientation || '').toLowerCase();
+
+            if($.inArray(orientation, ['auto', 'bottom', 'top']) === -1){
+                orientation = fallback;
+            }
+
+            return orientation;
+        },
+
+        processResponse: function (result, originalQuery, cacheKey) {
+            var that = this,
+                options = that.options;
+
+            result.suggestions = that.verifySuggestionsFormat(result.suggestions);
+
+            // Cache results if cache is not disabled:
+            if (!options.noCache) {
+                that.cachedResponse[cacheKey] = result;
+                if (options.preventBadQueries && !result.suggestions.length) {
+                    that.badQueries.push(originalQuery);
+                }
+            }
+
+            // Return if originalQuery is not matching current query:
+            if (originalQuery !== that.getQuery(that.currentValue)) {
+                return;
+            }
+
+            that.suggestions = result.suggestions;
+            that.suggest();
+        },
+
+        activate: function (index) {
+            var that = this,
+                activeItem,
+                selected = that.classes.selected,
+                container = $(that.suggestionsContainer),
+                children = container.find('.' + that.classes.suggestion);
+
+            container.find('.' + selected).removeClass(selected);
+
+            that.selectedIndex = index;
+
+            if (that.selectedIndex !== -1 && children.length > that.selectedIndex) {
+                activeItem = children.get(that.selectedIndex);
+                $(activeItem).addClass(selected);
+                return activeItem;
+            }
+
+            return null;
+        },
+
+        selectHint: function () {
+            var that = this,
+                i = $.inArray(that.hint, that.suggestions);
+
+            that.select(i);
+        },
+
+        select: function (i) {
+            var that = this;
+            that.hide();
+            that.onSelect(i);
+        },
+
+        moveUp: function () {
+            var that = this;
+
+            if (that.selectedIndex === -1) {
+                return;
+            }
+
+            if (that.selectedIndex === 0) {
+                $(that.suggestionsContainer).children('.' + that.classes.suggestion).first().removeClass(that.classes.selected);
+                that.selectedIndex = -1;
+                that.ignoreValueChange = false;
+                that.el.val(that.currentValue);
+                that.findBestHint();
+                return;
+            }
+
+            that.adjustScroll(that.selectedIndex - 1);
+        },
+
+        moveDown: function () {
+            var that = this;
+
+            if (that.selectedIndex === (that.suggestions.length - 1)) {
+                return;
+            }
+
+            that.adjustScroll(that.selectedIndex + 1);
+        },
+
+        adjustScroll: function (index) {
+            var that = this,
+                activeItem = that.activate(index);
+
+            if (!activeItem) {
+                return;
+            }
+
+            var offsetTop,
+                upperBound,
+                lowerBound,
+                heightDelta = $(activeItem).outerHeight();
+
+            offsetTop = activeItem.offsetTop;
+            upperBound = $(that.suggestionsContainer).scrollTop();
+            lowerBound = upperBound + that.options.maxHeight - heightDelta;
+
+            if (offsetTop < upperBound) {
+                $(that.suggestionsContainer).scrollTop(offsetTop);
+            } else if (offsetTop > lowerBound) {
+                $(that.suggestionsContainer).scrollTop(offsetTop - that.options.maxHeight + heightDelta);
+            }
+
+            if (!that.options.preserveInput) {
+                // During onBlur event, browser will trigger "change" event,
+                // because value has changed, to avoid side effect ignore,
+                // that event, so that correct suggestion can be selected
+                // when clicking on suggestion with a mouse
+                that.ignoreValueChange = true;
+                that.el.val(that.getValue(that.suggestions[index].value));
+            }
+
+            that.signalHint(null);
+        },
+
+        onSelect: function (index) {
+            var that = this,
+                onSelectCallback = that.options.onSelect,
+                suggestion = that.suggestions[index];
+
+            that.currentValue = that.getValue(suggestion.value);
+
+            if (that.currentValue !== that.el.val() && !that.options.preserveInput) {
+                that.el.val(that.currentValue);
+            }
+
+            that.signalHint(null);
+            that.suggestions = [];
+            that.selection = suggestion;
+
+            if ($.isFunction(onSelectCallback)) {
+                onSelectCallback.call(that.element, suggestion);
+            }
+        },
+
+        getValue: function (value) {
+            var that = this,
+                delimiter = that.options.delimiter,
+                currentValue,
+                parts;
+
+            if (!delimiter) {
+                return value;
+            }
+
+            currentValue = that.currentValue;
+            parts = currentValue.split(delimiter);
+
+            if (parts.length === 1) {
+                return value;
+            }
+
+            return currentValue.substr(0, currentValue.length - parts[parts.length - 1].length) + value;
+        },
+
+        dispose: function () {
+            var that = this;
+            that.el.off('.autocomplete').removeData('autocomplete');
+            $(window).off('resize.autocomplete', that.fixPositionCapture);
+            $(that.suggestionsContainer).remove();
+        }
+    };
+
+    // Create chainable jQuery plugin:
+    $.fn.devbridgeAutocomplete = function (options, args) {
+        var dataKey = 'autocomplete';
+        // If function invoked without argument return
+        // instance of the first matched element:
+        if (!arguments.length) {
+            return this.first().data(dataKey);
+        }
+
+        return this.each(function () {
+            var inputElement = $(this),
+                instance = inputElement.data(dataKey);
+
+            if (typeof options === 'string') {
+                if (instance && typeof instance[options] === 'function') {
+                    instance[options](args);
+                }
+            } else {
+                // If instance already exists, destroy it:
+                if (instance && instance.dispose) {
+                    instance.dispose();
+                }
+                instance = new Autocomplete(this, options);
+                inputElement.data(dataKey, instance);
+            }
+        });
+    };
+
+    // Don't overwrite if it already exists
+    if (!$.fn.autocomplete) {
+        $.fn.autocomplete = $.fn.devbridgeAutocomplete;
+    }
+}));

File diff suppressed because it is too large
+ 7 - 0
resources/assets/dcat/plugins/autocomplete/jquery.autocomplete.min.js


File diff suppressed because it is too large
+ 0 - 0
resources/dist/adminlte/adminlte-blue-light.css


File diff suppressed because it is too large
+ 0 - 0
resources/dist/adminlte/adminlte-blue.css


File diff suppressed because it is too large
+ 0 - 0
resources/dist/adminlte/adminlte-green.css


File diff suppressed because it is too large
+ 0 - 0
resources/dist/adminlte/adminlte.js


File diff suppressed because it is too large
+ 0 - 0
resources/dist/dcat/extra/grid-extend.js


File diff suppressed because it is too large
+ 0 - 0
resources/dist/dcat/extra/upload.js


File diff suppressed because it is too large
+ 0 - 0
resources/dist/dcat/extra/upload.js.map


File diff suppressed because it is too large
+ 0 - 0
resources/dist/dcat/js/dcat-app.js


File diff suppressed because it is too large
+ 0 - 0
resources/dist/dcat/js/dcat-app.js.map


+ 1 - 1
resources/views/form/autocomplete.blade.php

@@ -29,7 +29,7 @@
 
                     if (valueField) {
                         return $.map(data, function (dat) {
-                            return {value: Dcat.helpers.get(dat, valueField), data: dat};
+                            return {value: Dcat.helpers.get(dat, valueField) + '', data: dat};
                         });
                     }
 

+ 19 - 18
resources/views/form/hasmany.blade.php

@@ -17,12 +17,12 @@
                 {!! $form->render() !!}
 
                 @if($options['allowDelete'])
-                <div class="form-group row">
-                    <label class="{{$viewClass['label']}} control-label"></label>
-                    <div class="{{$viewClass['field']}}">
-                        <div class="remove btn btn-white btn-sm pull-right"><i class="feather icon-trash">&nbsp;</i>{{ trans('admin.remove') }}</div>
+                    <div class="form-group row">
+                        <label class="{{$viewClass['label']}} control-label"></label>
+                        <div class="{{$viewClass['field']}}">
+                            <div class="{{$columnClass}}-remove btn btn-white btn-sm pull-right"><i class="feather icon-trash">&nbsp;</i>{{ trans('admin.remove') }}</div>
+                        </div>
                     </div>
-                </div>
                 @endif
                 <hr>
             </div>
@@ -39,7 +39,7 @@
             <div class="form-group row">
                 <label class="{{$viewClass['label']}} control-label"></label>
                 <div class="{{$viewClass['field']}}">
-                    <div class="remove btn btn-white btn-sm pull-right"><i class="feather icon-trash"></i>&nbsp;{{ trans('admin.remove') }}</div>
+                    <div class="{{$columnClass}}-remove btn btn-white btn-sm pull-right"><i class="feather icon-trash"></i>&nbsp;{{ trans('admin.remove') }}</div>
                 </div>
             </div>
             <hr>
@@ -47,12 +47,12 @@
     </template>
 
     @if($options['allowCreate'])
-    <div class="form-group row">
-        <label class="{{$viewClass['label']}} control-label"></label>
-        <div class="{{$viewClass['field']}}">
-            <div class="add btn btn-primary btn-outline btn-sm"><i class="feather icon-plus"></i>&nbsp;{{ trans('admin.new') }}</div>
+        <div class="form-group row">
+            <label class="{{$viewClass['label']}} control-label"></label>
+            <div class="{{$viewClass['field']}}">
+                <div class="{{$columnClass}}-add btn btn-primary btn-outline btn-sm"><i class="feather icon-plus"></i>&nbsp;{{ trans('admin.new') }}</div>
+            </div>
         </div>
-    </div>
     @endif
 
 </div>
@@ -63,21 +63,22 @@
         forms = '.has-many-{{ $columnClass  }}-forms';
 
     function replaceNestedFormIndex(value) {
-        return String(value).replace(/{{ Dcat\Admin\Form\NestedForm::DEFAULT_KEY_NAME }}/g, nestedIndex);
+        return String(value)
+            .replace(/{{ Dcat\Admin\Form\NestedForm::DEFAULT_KEY_NAME }}/g, nestedIndex)
+            .replace(/{{ Dcat\Admin\Form\NestedForm::DEFAULT_PARENT_KEY_NAME }}/g, nestedIndex);
     }
 
-    $(container).on('click', '.add', function () {
-
+    $(container).on('click', '.{{$columnClass}}-add', function () {
         var tpl = $('template.{{ $columnClass }}-tpl');
 
         nestedIndex++;
 
-        var template = replaceNestedFormIndex(tpl.html());
-        $(forms).append(template);
+        $(forms).append(replaceNestedFormIndex(tpl.html()));
     });
 
-    $(container).on('click', '.remove', function () {
-        var $form = $(this).closest('.has-many-{{ $columnClass  }}-form');
+    $(container).on('click', '.{{$columnClass}}-remove', function () {
+        var $form = $(this).closest('.has-many-{{ $columnClass }}-form');
+
         $form.hide();
         $form.find('.{{ Dcat\Admin\Form\NestedForm::REMOVE_FLAG_CLASS }}').val(1);
         $form.find('[required]').prop('required', false);

+ 18 - 14
resources/views/form/hasmanytable.blade.php

@@ -9,8 +9,8 @@
 
         <span name="{{$column}}"></span> {{-- 用于显示错误信息 --}}
 
-        <div class="has-many-{{$columnClass}}" >
-            <table class="table table-has-many has-many-{{$columnClass}}">
+        <div class="has-many-table-{{$columnClass}}" >
+            <table class="table table-has-many has-many-table-{{$columnClass}}">
                 <thead>
                 <tr>
                     @foreach($headers as $header)
@@ -24,9 +24,9 @@
                     @endif
                 </tr>
                 </thead>
-                <tbody class="has-many-{{$columnClass}}-forms">
+                <tbody class="has-many-table-{{$columnClass}}-forms">
                 @foreach($forms as $pk => $form)
-                    <tr class="has-many-{{$columnClass}}-form fields-group">
+                    <tr class="has-many-table-{{$columnClass}}-form fields-group">
 
                         <?php $hidden = ''; ?>
 
@@ -55,7 +55,7 @@
             </table>
 
             <template class="{{$columnClass}}-tpl">
-                <tr class="has-many-{{$columnClass}}-form fields-group">
+                <tr class="has-many-table-{{$columnClass}}-form fields-group">
 
                     {!! $template !!}
 
@@ -81,28 +81,32 @@
 {{--<hr style="margin-top: 0px;">--}}
 
 <script>
+(function () {
     var nestedIndex = {!! $count !!},
-        container = '.has-many-{{ $columnClass }}';
+        container = '.has-many-table-{{ $columnClass }}';
 
     function replaceNestedFormIndex(value) {
-        return String(value).replace(/{{ Dcat\Admin\Form\NestedForm::DEFAULT_KEY_NAME }}/g, nestedIndex);
+        return String(value).replace(/{{ $parentKey ?: Dcat\Admin\Form\NestedForm::DEFAULT_KEY_NAME }}/g, nestedIndex);
     }
 
-    $(container).on('click', '.add', function () {
-        var tpl = $('template.{{ $columnClass }}-tpl');
+    $(document).off('click', container+' .add').on('click', container+' .add', function (e) {
+        var $con = $(this).closest(container);
+        var tpl = $con.find('template.{{ $columnClass }}-tpl');
 
         nestedIndex++;
 
-        var template = replaceNestedFormIndex(tpl.html());
-        $('.has-many-{{ $columnClass }}-forms').append(template);
+        $con.find('.has-many-table-{{ $columnClass }}-forms').append(replaceNestedFormIndex(tpl.html()));
+
+        e.preventDefault();
+        return false
     });
 
-    $(container).on('click', '.remove', function () {
-        var $form = $(this).closest('.has-many-{{ $columnClass }}-form');
+    $(document).off('click', container+' .remove').on('click', container+' .remove', function () {
+        var $form = $(this).closest('.has-many-table-{{ $columnClass }}-form');
 
         $form.hide();
         $form.find('[required]').prop('required', false);
         $form.find('.{{ Dcat\Admin\Form\NestedForm::REMOVE_FLAG_CLASS }}').val(1);
     });
+})();
 </script>
-

+ 1 - 1
resources/views/grid/displayer/switch.blade.php

@@ -1,4 +1,4 @@
-<input class="grid-column-switch" data-url="{{ $url }}" data-reload="{{ $refresh }}}" data-size="small" name="{{ $column }}" {{ $checked }} type="checkbox" data-color="{{ $color }}"/>
+<input class="grid-column-switch" data-url="{{ $url }}" data-reload="{{ $refresh }}" data-size="small" name="{{ $column }}" {{ $checked }} type="checkbox" data-color="{{ $color }}"/>
 
 <script require="@switchery">
     var swt = $('.grid-column-switch'),

+ 539 - 538
resources/views/helpers/scaffold.blade.php

@@ -1,538 +1,539 @@
-@php
-    $timestamps = Dcat\Admin\Widgets\Checkbox::make('timestamps')->inline();
-    $timestamps->options([1 => 'Created_at & Updated_at'])->check(1);
-
-    $soft = Dcat\Admin\Widgets\Checkbox::make('soft_deletes')->inline();
-    $soft->options([1 => (trans('admin.scaffold.soft_delete'))]);
-
-    $actionCreators = Dcat\Admin\Widgets\Checkbox::make('create[]')->inline();
-    $actionCreators->options([
-        'migration' => (trans('admin.scaffold.create_migration')),
-        'model' => (trans('admin.scaffold.create_model')),
-        'repository' => (trans('admin.scaffold.create_repository')),
-        'controller' => (trans('admin.scaffold.create_controller')),
-        'migrate' => (trans('admin.scaffold.run_migrate')),
-        'lang' => (trans('admin.scaffold.create_lang')),
-    ])->checkAll(['migrate', 'migration']);
-@endphp
-<style>
-    .select2-container .select2-selection--single {
-        height: 30px !important;
-    }
-    .choose-exist-table {
-        width: 100%;
-    }
-</style>
-<div class="card">
-    <div style="height:10px"></div>
-    <!-- /.box-header -->
-    <div class="card-body" style="padding:18px 0 0">
-
-        <form method="post" action="{{$action}}" id="scaffold" pjax-container>
-
-            <div class="form-horizontal">
-
-                <div class="form-group row">
-
-{{--                    <label for="inputTableName" class="col-sm-1 control-label text-capitalize">{{(trans('admin.scaffold.table'))}}</label>--}}
-
-                    <div for="inputTableName"  class="col-sm-1 control-label text-capitalize">
-                        <span>{{(trans('admin.scaffold.table'))}}</span>
-                    </div>
-
-                    <div class="col-sm-2 ">
-                        <div class="input-group">
-                            <input type="text" name="table_name" class="form-control" id="inputTableName" placeholder="{{(trans('admin.scaffold.table'))}}" value="{{ old('table_name') }}">
-
-                        </div>
-                    </div>
-
-                    <div class=" col-sm-2" style="margin-left: -15px;">
-                        <select class="choose-exist-table"  name="exist-table">
-                            <option value="0" selected>{{trans('admin.scaffold.choose')}}</option>
-                            @foreach($tables as $db => $tb)
-                                <optgroup label="{!! $db !!}">
-                                    @foreach($tb as $v)
-                                        <option value="{{$db}}|{{$v}}">{{$v}}</option>
-                                    @endforeach
-                                </optgroup>
-                            @endforeach
-                        </select>
-                    </div>
-
-                    <span class="help-block " id="table-name-help" style="margin-left:150px;display: none">
-                        <i class="fa fa-info"></i>&nbsp; Table name can't be empty!
-                    </span>
-
-                </div>
-                <div class="form-group row">
-                    <span for="inputModelName" class="col-sm-1 control-label text-capitalize">{{(trans('admin.scaffold.model'))}}</span>
-
-                    <div class="col-sm-4">
-                        <input type="text" name="model_name" class="form-control text-capitalize" id="inputModelName" placeholder="{{(trans('admin.scaffold.model'))}}" value="{{ old('model_name', "App\\Models\\") }}">
-                    </div>
-                </div>
-
-                <div class="form-group row">
-                    <span for="inputControllerName" class="col-sm-1 control-label text-capitalize">{{(trans('admin.scaffold.controller'))}}</span>
-
-                    <div class="col-sm-4">
-                        <input type="text" name="controller_name" class="form-control text-capitalize" id="inputControllerName" placeholder="{{(trans('admin.scaffold.controller'))}}" value="{{ old('controller_name', "App\\Admin\\Controllers\\") }}">
-                    </div>
-                </div>
-
-
-                <div class="form-group row">
-                    <span for="inputRepositoryName" class="col-sm-1 control-label text-capitalize">{{(trans('admin.scaffold.repository'))}}</span>
-
-                    <div class="col-sm-4">
-                        <input type="text" name="repository_name" class="form-control text-capitalize" id="inputRepositoryName" placeholder="{{(trans('admin.scaffold.repository'))}}" value="{{ old('repository_name', "App\\Admin\\Repositories\\") }}">
-                    </div>
-                </div>
-
-
-                <div class="form-group row">
-                    <div class="offset-sm-1 col-sm-11 mt-1 text-capitalize">
-                        {!! $actionCreators->render(); !!}
-                    </div>
-                </div>
-
-            </div>
-
-            <table class="table table-hover responsive table-header-gray " id="table-fields" style="margin-top:25px;">
-                <thead>
-                <tr>
-                    <th style="width: 200px">{{trans('admin.scaffold.field_name')}}</th>
-                    <th>{{trans('admin.scaffold.translation')}}</th>
-                    <th>{{trans('admin.scaffold.type')}}</th>
-                    <th>{{trans('admin.scaffold.nullable')}}</th>
-                    <th>{{trans('admin.scaffold.key')}}</th>
-                    <th>{{trans('admin.scaffold.default')}}</th>
-                    <th>{{trans('admin.scaffold.comment')}}</th>
-                    <th>{{trans('admin.action')}}</th>
-                </tr>
-                </thead>
-                <tbody id="table-fields-sortable">
-                @if(old('fields'))
-                    @foreach(old('fields') as $index => $field)
-                        <tr>
-                            <td>
-                                <input type="text" name="fields[{{$index}}][name]" class="form-control" placeholder="{{trans('admin.scaffold.field')}}" value="{{$field['name']}}" />
-                            </td>
-                            <td>
-                                <input type="text" name="fields[{{$index}}][translation]" class="form-control" placeholder="{{trans('admin.scaffold.translation')}}" value="{{$field['translation']}}" />
-                            </td>
-                            <td>
-                                <select style="width: 200px" name="fields[{{$index}}][type]">
-                                    @foreach($dbTypes as $type)
-                                        <option value="{{ $type }}" {{$field['type'] == $type ? 'selected' : '' }}>{{$type}}</option>
-                                    @endforeach
-                                </select>
-                            </td>
-                            <td>
-                                <div class="vs-checkbox-con vs-checkbox-primary" >
-                                    <input name="fields[{{$index}}][nullable]" type="checkbox" {{ \Illuminate\Support\Arr::get($field, 'nullable') == 'on' ? 'checked': '' }}>
-                                    <span class="vs-checkbox vs-checkbox-">
-                                      <span class="vs-checkbox--check">
-                                        <i class="vs-icon feather icon-check"></i>
-                                      </span>
-                                    </span>
-                                </div>
-                            </td>
-                            <td>
-                                <select style="width: 150px" name="fields[{{$index}}][key]">
-                                    {{--<option value="primary">Primary</option>--}}
-                                    <option value="" {{$field['key'] == '' ? 'selected' : '' }}>NULL</option>
-                                    <option value="unique" {{$field['key'] == 'unique' ? 'selected' : '' }}>Unique</option>
-                                    <option value="index" {{$field['key'] == 'index' ? 'selected' : '' }}>Index</option>
-                                </select>
-                            </td>
-                            <td><input type="text" class="form-control" placeholder="{{trans('admin.scaffold.default')}}" name="fields[{{$index}}][default]" value="{{$field['default']}}"/></td>
-                            <td><input type="text" class="form-control" placeholder="{{trans('admin.scaffold.comment')}}" name="fields[{{$index}}][comment]" value="{{$field['comment']}}" /></td>
-                            <td>
-                                <button class="btn btn-sm btn-white table-field-sort-handle" type="button" title="{{trans('admin.order')}}"><i class="fa fa-sort"></i></button>
-                                <button class="btn btn-sm btn-white table-field-remove"><i class="feather icon-trash"></i></button>
-                            </td>
-                        </tr>
-                    @endforeach
-                @else
-                    <tr>
-                        <td>
-                            <input type="text" name="fields[0][name]" class="form-control" placeholder="{{trans('admin.scaffold.field')}}" />
-                        </td>
-                        <td>
-                            <input type="text" name="fields[0][translation]" class="form-control" placeholder="{{trans('admin.scaffold.translation')}}" />
-                        </td>
-                        <td>
-                            <select style="width: 200px" name="fields[0][type]">
-                                @foreach($dbTypes as $type)
-                                    <option value="{{ $type }}">{{$type}}</option>
-                                @endforeach
-                            </select>
-                        </td>
-                        <td>
-                            <div class="vs-checkbox-con vs-checkbox-primary" >
-                                <input name="fields[0][nullable]" type="checkbox"  />
-                                <span class="vs-checkbox vs-checkbox-">
-                                  <span class="vs-checkbox--check">
-                                    <i class="vs-icon feather icon-check"></i>
-                                  </span>
-                                </span>
-                            </div>
-                        <td>
-                            <select style="width: 150px" name="fields[0][key]">
-                                {{--<option value="primary">Primary</option>--}}
-                                <option value="" selected>NULL</option>
-                                <option value="unique">Unique</option>
-                                <option value="index">Index</option>
-                            </select>
-                        </td>
-                        <td><input type="text" class="form-control" placeholder="{{trans('admin.scaffold.default')}}" name="fields[0][default]"></td>
-                        <td><input type="text" class="form-control" placeholder="{{trans('admin.scaffold.comment')}}" name="fields[0][comment]"></td>
-                        <td>
-                            <button class="btn btn-sm btn-white table-field-sort-handle" type="button" title="{{trans('admin.order')}}"><i class="fa fa-sort"></i></button>
-                            <button class="btn btn-sm btn-white table-field-remove"><i class="feather icon-trash"></i></button>
-                        </td>
-                    </tr>
-                @endif
-                </tbody>
-            </table>
-
-            <hr style="margin-top: 0;"/>
-
-            <div class='form-inline d-flex justify-content-between' style="width: 100%; padding: 0 20px 12px;">
-
-                <div class='form-group'>
-                    <button type="button" class="btn btn-sm btn-primary btn-outline text-capitalize" id="add-table-field"><i class="feather icon-plus"></i>&nbsp;&nbsp;{{(trans('admin.scaffold.add_field'))}}</button>
-                    <button type="button" class="btn btn-sm btn-primary btn-outline text-capitalize ml-1" id="sync-translation-with-comment"><i class="feather icon-repeat"></i>&nbsp;&nbsp;{{(trans('admin.scaffold.sync_translation_with_comment'))}}</button>
-                </div>
-
-                <div class="row">
-                    <div class="form-group text-capitalize" style="margin-right: 20px;">
-                        <span for="titleTranslation">{{(trans('admin.scaffold.translate_title'))}}&nbsp;&nbsp;</span>
-                        <input type="text"
-                               name="translate_title"
-                               class="form-control"
-                               id="titleTranslation"
-                               placeholder="{{(trans('admin.scaffold.translate_title'))}}"
-                               value="{{ request('translate_title') }}"
-                               style="width: 150px;">
-                    </div>
-
-                    <div class="form-group text-capitalize" style="margin-right: 20px;">
-                        <span for="inputPrimaryKey">{{(trans('admin.scaffold.pk'))}}&nbsp;&nbsp;</span>
-                        <input
-                                type="text"
-                                name="primary_key"
-                                class="form-control"
-                                id="inputPrimaryKey"
-                                placeholder="{{(trans('admin.scaffold.pk'))}}"
-                                value="{{ request('primary_key') ?: 'id' }}"
-                                style="width: 100px;">
-                    </div>
-
-                    <div class='form-group text-capitalize'>
-                        {!! $timestamps->render() !!}
-                        {!! $soft->render() !!}
-                    </div>
-
-                </div>
-            </div>
-
-            <!-- /.box-body -->
-            <div class="box-footer d-flex justify-content-between">
-                <div></div>
-                <button type="submit" class="btn btn-primary text-capitalize"><i class="feather icon-save"></i> {{(trans('admin.submit'))}}</button>
-            </div>
-
-        {{ csrf_field() }}
-
-        </form>
-
-    </div>
-
-</div>
-
-<template id="table-field-tpl">
-    <tr>
-        <td>
-            <input type="text" value="{name}" name="fields[__index__][name]" class="form-control" placeholder="{{trans('admin.scaffold.field')}}" />
-        </td>
-        <td>
-            <input type="text" value="{translation}" name="fields[__index__][translation]" class="form-control" placeholder="{{trans('admin.scaffold.translation')}}" />
-        </td>
-        <td>
-            <select style="width: 200px" name="fields[__index__][type]">
-                @foreach($dbTypes as $type)
-                    <option value="{{ $type }}">{{$type}}</option>
-                @endforeach
-            </select>
-        </td>
-        <td>
-            <div class="vs-checkbox-con vs-checkbox-primary" >
-                <input {nullable} name="fields[__index__][nullable]" type="checkbox"  />
-                <span class="vs-checkbox vs-checkbox-">
-                  <span class="vs-checkbox--check">
-                    <i class="vs-icon feather icon-check"></i>
-                  </span>
-                </span>
-            </div>
-        <td>
-            <select style="width: 150px" name="fields[__index__][key]">
-                <option value="" selected>NULL</option>
-                <option value="unique">Unique</option>
-                <option value="index">Index</option>
-            </select>
-        </td>
-        <td><input value="{default}" type="text" class="form-control" placeholder="{{trans('admin.scaffold.default')}}" name="fields[__index__][default]"></td>
-        <td><input value="{comment}" type="text" class="form-control" placeholder="{{trans('admin.scaffold.comment')}}" name="fields[__index__][comment]"></td>
-        <td>
-            <button class="btn btn-sm btn-white table-field-sort-handle" type="button" title="{{trans('admin.order')}}"><i class="fa fa-sort"></i></button>
-            <button class="btn btn-sm btn-white table-field-remove"><i class="feather icon-trash"></i></button>
-        </td>
-    </tr>
-</template>
-
-<script>
-    Dcat.ready(function () {
-        var $model = $('#inputModelName'),
-            $controller = $('#inputControllerName'),
-            $repository = $('#inputRepositoryName'),
-            $table = $('#inputTableName'),
-            $fieldsBody = $('#table-fields tbody'),
-            tpl = $('#table-field-tpl').html(),
-            modelNamespace = 'App\\Models\\',
-            repositoryNamespace = 'App\\Admin\\Repositories\\',
-            controllerNamespace = 'App\\Admin\\Controllers\\',
-            dataTypeMap = {!! json_encode($dataTypeMap) !!},
-            helpers = Dcat.helpers;
-
-        var withSingularName = helpers.debounce(function (table) {
-            $.ajax('{{ url(request()->path()) }}?singular=' + table, {
-                success: function (data) {
-                    writeController(data.value);
-                    writeModel(data.value);
-                    witeRepository(data.value);
-                }
-            });
-        }, 500);
-
-        $('select').select2();
-
-        var sortable = Sortable.create(document.getElementById("table-fields-sortable"),{
-            handle:'.table-field-sort-handle',
-            onEnd: function () {
-                getTR().each(function(index){
-                    $(this).find("[name^='fields']").each(function(){
-                        var newName = $(this).attr('name').replace(/fields\[(\d)\]/, `fields[${index}]`);
-                        $(this).attr('name', newName);
-                    })
-                });
-            }
-        });
-
-        $('#add-table-field').click(function (event) {
-            addField();
-        });
-
-        $('#sync-translation-with-comment').click(function (event) {
-            var element = $('#table-fields-sortable tr');
-            if (element.length > 0) {
-                element.each(function (i, v) {
-                    var translation = $(v).find('input[name="fields[' + i + '][translation]"]');
-                    var comment = $(v).find('input[name="fields[' + i + '][comment]"]');
-                    if (translation.val() !== "" && comment.val() === "") {
-                        comment.val(translation.val());
-                    }
-                    if (translation.val() === "" && comment.val() !== "") {
-                        translation.val(comment.val());
-                    }
-                });
-            }
-        });
-
-        $('#table-fields').on('click', '.table-field-remove', function(event) {
-            $(event.target).closest('tr').remove();
-        });
-
-        $('#scaffold').on('submit', function (event) {
-
-            //event.preventDefault();
-
-            if ($table.val() == '') {
-                $table.closest('.form-group').addClass('has-error');
-                $('#table-name-help').show();
-
-                return false;
-            }
-
-            return true;
-        });
-
-        $('.choose-exist-table').on('change', function () {
-            var val = $(this).val(), tb, db;
-            if (val == '0') {
-                $table.val('');
-                getTR().remove();
-                return;
-            }
-            val = val.split('|');
-            db = val[0];
-            tb = val[1];
-
-            Dcat.loading();
-            $table.val(tb);
-
-            withSingularName(tb);
-
-            $.post({
-                url: '{{ admin_url('helpers/scaffold/table') }}',
-                data: {db: db, tb: tb},
-                success: function (res) {
-                    Dcat.loading(false);
-
-                    if (!res.list) return;
-                    var i, list = res.list, $id = $('#inputPrimaryKey'), updated, created, soft;
-
-                    getTR().remove();
-                    for (i in list) {
-                        if (list[i].id) {
-                            $id.val(i);
-                            continue;
-                        }
-                        if (i == 'updated_at') {
-                            updated = list[i];
-                            continue;
-                        }
-                        if (i == 'created_at') {
-                            created = list[i];
-                            continue;
-                        }
-                        if (i == 'deleted_at') {
-                            soft = list[i];
-                            continue;
-                        }
-
-                        var c = helpers.replace(list[i].comment, '"', '');
-                        addField({
-                            name: i,
-                            lang: c,
-                            type: list[i].type,
-                            default: helpers.replace(list[i].default, '"', ''),
-                            comment: c,
-                            nullable: list[i].nullable != 'NO',
-                        });
-                    }
-
-                    addTimestamps(updated, created);
-                    addSoftdelete(soft);
-                }
-            });
-
-        });
-
-        $table.on('keyup', function (e) {
-            withSingularName($(this).val());
-        });
-
-        function addTimestamps(updated, created) {
-            if (updated && created) {
-                return;
-            }
-            $('[name="timestamps"]').prop('checked', false);
-
-            var c;
-            if (updated) {
-                c = helpers.replace(updated.comment, '"', '');
-                addField({
-                    name: 'updated_at',
-                    lang: c,
-                    type: updated.type,
-                    default: helpers.replace(updated.default, '"', ''),
-                    comment: c,
-                    nullable: updated.nullable != 'NO',
-                });
-            }
-            if (created) {
-                c = helpers.replace(created.comment, '"', '');
-                addField({
-                    name: 'created_at',
-                    lang: c,
-                    type: created.type,
-                    default: helpers.replace(created.default, '"', ''),
-                    comment: c,
-                    nullable: created.nullable != 'NO',
-                });
-            }
-        }
-
-        function addSoftdelete(soft) {
-            if (soft) {
-                $('[name="soft_deletes"]').prop('checked', true);
-            }
-        }
-
-        function addField(val) {
-            val = val || {};
-
-            var idx = getTR().length,
-                $field = $(
-                    tpl
-                        .replace(/__index__/g, idx)
-                        .replace(/{name}/g, val.name || '')
-                        .replace(/{translation}/g, val.lang || '')
-                        .replace(/{default}/g, val.default || '')
-                        .replace(/{comment}/g, val.comment || '')
-                        .replace(/{nullable}/g, val.nullable ? 'checked' : '')
-                ),
-                i;
-
-            $fieldsBody.append($field);
-            $('select').select2();
-
-            // 选中字段类型
-            for (i in dataTypeMap) {
-                if (val.type == i) {
-                    $field.find('[name="fields['+ idx +'][type]"]')
-                        .val(dataTypeMap[i])
-                        .trigger("change");
-                }
-            }
-
-        }
-
-        function writeController(val) {
-            val = ucfirst(toHump(toLine(val)));
-            $controller.val(val ? (controllerNamespace + val + 'Controller') : controllerNamespace);
-        }
-        function writeModel(val) {
-            $model.val(modelNamespace + ucfirst(ucfirst(toHump(toLine(val)))));
-        }
-        function witeRepository(val) {
-            $repository.val(repositoryNamespace + ucfirst(ucfirst(toHump(toLine(val)))))
-        }
-
-        function getTR() {
-            return $('#table-fields tbody tr');
-        }
-
-        // 下划线转换驼峰
-        function toHump(name) {
-            return name.replace(/\_(\w)/g, function (all, letter) {
-                return letter.toUpperCase();
-            });
-        }
-
-        // 驼峰转换下划线
-        function toLine(name) {
-            return name.replace(/([A-Z])/g,"_$1").toLowerCase();
-        }
-
-        function ucfirst(str) {
-            var reg = /\b(\w)|\s(\w)/g;
-            return str.replace(reg,function(m){
-                return m.toUpperCase()
-            });
-        }
-    });
-</script>
+@php
+    $timestamps = Dcat\Admin\Widgets\Checkbox::make('timestamps')->inline();
+    $timestamps->options([1 => 'Created_at & Updated_at'])->check(1);
+
+    $soft = Dcat\Admin\Widgets\Checkbox::make('soft_deletes')->inline();
+    $soft->options([1 => (trans('admin.scaffold.soft_delete'))]);
+
+    $actionCreators = Dcat\Admin\Widgets\Checkbox::make('create[]')->inline();
+    $actionCreators->options([
+        'migration' => (trans('admin.scaffold.create_migration')),
+        'model' => (trans('admin.scaffold.create_model')),
+        'repository' => (trans('admin.scaffold.create_repository')),
+        'controller' => (trans('admin.scaffold.create_controller')),
+        'migrate' => (trans('admin.scaffold.run_migrate')),
+        'lang' => (trans('admin.scaffold.create_lang')),
+    ])->checkAll(['migrate', 'migration']);
+@endphp
+<style>
+    .select2-container .select2-selection--single {
+        height: 30px !important;
+    }
+    .choose-exist-table {
+        width: 100%;
+    }
+</style>
+<div class="card">
+    <div style="height:10px"></div>
+    <!-- /.box-header -->
+    <div class="card-body" style="padding:18px 0 0">
+
+        <form method="post" action="{{$action}}" id="scaffold" pjax-container>
+
+            <div class="form-horizontal">
+
+                <div class="form-group row">
+
+{{--                    <label for="inputTableName" class="col-sm-1 control-label text-capitalize">{{(trans('admin.scaffold.table'))}}</label>--}}
+
+                    <div for="inputTableName"  class="col-sm-1 control-label text-capitalize">
+                        <span>{{(trans('admin.scaffold.table'))}}</span>
+                    </div>
+
+                    <div class="col-sm-2 ">
+                        <div class="input-group">
+                            <input type="text" name="table_name" class="form-control" id="inputTableName" placeholder="{{(trans('admin.scaffold.table'))}}" value="{{ old('table_name') }}">
+
+                        </div>
+                    </div>
+
+                    <div class=" col-sm-2" style="margin-left: -15px;">
+                        <select class="choose-exist-table"  name="exist-table">
+                            <option value="0" selected>{{trans('admin.scaffold.choose')}}</option>
+                            @foreach($tables as $db => $tb)
+                                <optgroup label="{!! $db !!}">
+                                    @foreach($tb as $v)
+                                        <option value="{{$db}}|{{$v}}">{{$v}}</option>
+                                    @endforeach
+                                </optgroup>
+                            @endforeach
+                        </select>
+                    </div>
+
+                    <span class="help-block " id="table-name-help" style="margin-left:150px;display: none">
+                        <i class="fa fa-info"></i>&nbsp; Table name can't be empty!
+                    </span>
+
+                </div>
+                <div class="form-group row">
+                    <span for="inputModelName" class="col-sm-1 control-label text-capitalize">{{(trans('admin.scaffold.model'))}}</span>
+
+                    <div class="col-sm-4">
+                        <input type="text" name="model_name" class="form-control text-capitalize" id="inputModelName" placeholder="{{(trans('admin.scaffold.model'))}}" value="{{ old('model_name', "App\\Models\\") }}">
+                    </div>
+                </div>
+
+                <div class="form-group row">
+                    <span for="inputControllerName" class="col-sm-1 control-label text-capitalize">{{(trans('admin.scaffold.controller'))}}</span>
+
+                    <div class="col-sm-4">
+                        <input type="text" name="controller_name" class="form-control text-capitalize" id="inputControllerName" placeholder="{{(trans('admin.scaffold.controller'))}}" value="{{ old('controller_name', $namespaceBase."\\Controllers\\") }}">
+                    </div>
+                </div>
+
+
+                <div class="form-group row">
+                    <span for="inputRepositoryName" class="col-sm-1 control-label text-capitalize">{{(trans('admin.scaffold.repository'))}}</span>
+
+                    <div class="col-sm-4">
+                        <input type="text" name="repository_name" class="form-control text-capitalize" id="inputRepositoryName" placeholder="{{(trans('admin.scaffold.repository'))}}" value="{{ old('repository_name', $namespaceBase."\\Repositories\\") }}">
+                    </div>
+                </div>
+
+
+                <div class="form-group row">
+                    <div class="offset-sm-1 col-sm-11 mt-1 text-capitalize">
+                        {!! $actionCreators->render(); !!}
+                    </div>
+                </div>
+
+            </div>
+
+            <table class="table table-hover responsive table-header-gray " id="table-fields" style="margin-top:25px;">
+                <thead>
+                <tr>
+                    <th style="width: 200px">{{trans('admin.scaffold.field_name')}}</th>
+                    <th>{{trans('admin.scaffold.translation')}}</th>
+                    <th>{{trans('admin.scaffold.type')}}</th>
+                    <th>{{trans('admin.scaffold.nullable')}}</th>
+                    <th>{{trans('admin.scaffold.key')}}</th>
+                    <th>{{trans('admin.scaffold.default')}}</th>
+                    <th>{{trans('admin.scaffold.comment')}}</th>
+                    <th>{{trans('admin.action')}}</th>
+                </tr>
+                </thead>
+                <tbody id="table-fields-sortable">
+                @if(old('fields'))
+                    @foreach(old('fields') as $index => $field)
+                        <tr>
+                            <td>
+                                <input type="text" name="fields[{{$index}}][name]" class="form-control" placeholder="{{trans('admin.scaffold.field')}}" value="{{$field['name']}}" />
+                            </td>
+                            <td>
+                                <input type="text" name="fields[{{$index}}][translation]" class="form-control" placeholder="{{trans('admin.scaffold.translation')}}" value="{{$field['translation']}}" />
+                            </td>
+                            <td>
+                                <select style="width: 200px" name="fields[{{$index}}][type]">
+                                    @foreach($dbTypes as $type)
+                                        <option value="{{ $type }}" {{$field['type'] == $type ? 'selected' : '' }}>{{$type}}</option>
+                                    @endforeach
+                                </select>
+                            </td>
+                            <td>
+                                <div class="vs-checkbox-con vs-checkbox-primary" >
+                                    <input name="fields[{{$index}}][nullable]" type="checkbox" {{ \Illuminate\Support\Arr::get($field, 'nullable') == 'on' ? 'checked': '' }}>
+                                    <span class="vs-checkbox vs-checkbox-">
+                                      <span class="vs-checkbox--check">
+                                        <i class="vs-icon feather icon-check"></i>
+                                      </span>
+                                    </span>
+                                </div>
+                            </td>
+                            <td>
+                                <select style="width: 150px" name="fields[{{$index}}][key]">
+                                    {{--<option value="primary">Primary</option>--}}
+                                    <option value="" {{$field['key'] == '' ? 'selected' : '' }}>NULL</option>
+                                    <option value="unique" {{$field['key'] == 'unique' ? 'selected' : '' }}>Unique</option>
+                                    <option value="index" {{$field['key'] == 'index' ? 'selected' : '' }}>Index</option>
+                                </select>
+                            </td>
+                            <td><input type="text" class="form-control" placeholder="{{trans('admin.scaffold.default')}}" name="fields[{{$index}}][default]" value="{{$field['default']}}"/></td>
+                            <td><input type="text" class="form-control" placeholder="{{trans('admin.scaffold.comment')}}" name="fields[{{$index}}][comment]" value="{{$field['comment']}}" /></td>
+                            <td>
+                                <button class="btn btn-sm btn-white table-field-sort-handle" type="button" title="{{trans('admin.order')}}"><i class="fa fa-sort"></i></button>
+                                <button class="btn btn-sm btn-white table-field-remove"><i class="feather icon-trash"></i></button>
+                            </td>
+                        </tr>
+                    @endforeach
+                @else
+                    <tr>
+                        <td>
+                            <input type="text" name="fields[0][name]" class="form-control" placeholder="{{trans('admin.scaffold.field')}}" />
+                        </td>
+                        <td>
+                            <input type="text" name="fields[0][translation]" class="form-control" placeholder="{{trans('admin.scaffold.translation')}}" />
+                        </td>
+                        <td>
+                            <select style="width: 200px" name="fields[0][type]">
+                                @foreach($dbTypes as $type)
+                                    <option value="{{ $type }}">{{$type}}</option>
+                                @endforeach
+                            </select>
+                        </td>
+                        <td>
+                            <div class="vs-checkbox-con vs-checkbox-primary" >
+                                <input name="fields[0][nullable]" type="checkbox"  />
+                                <span class="vs-checkbox vs-checkbox-">
+                                  <span class="vs-checkbox--check">
+                                    <i class="vs-icon feather icon-check"></i>
+                                  </span>
+                                </span>
+                            </div>
+                        <td>
+                            <select style="width: 150px" name="fields[0][key]">
+                                {{--<option value="primary">Primary</option>--}}
+                                <option value="" selected>NULL</option>
+                                <option value="unique">Unique</option>
+                                <option value="index">Index</option>
+                            </select>
+                        </td>
+                        <td><input type="text" class="form-control" placeholder="{{trans('admin.scaffold.default')}}" name="fields[0][default]"></td>
+                        <td><input type="text" class="form-control" placeholder="{{trans('admin.scaffold.comment')}}" name="fields[0][comment]"></td>
+                        <td>
+                            <button class="btn btn-sm btn-white table-field-sort-handle" type="button" title="{{trans('admin.order')}}"><i class="fa fa-sort"></i></button>
+                            <button class="btn btn-sm btn-white table-field-remove"><i class="feather icon-trash"></i></button>
+                        </td>
+                    </tr>
+                @endif
+                </tbody>
+            </table>
+
+            <hr style="margin-top: 0;"/>
+
+            <div class='form-inline d-flex justify-content-between' style="width: 100%; padding: 0 20px 12px;">
+
+                <div class='form-group'>
+                    <button type="button" class="btn btn-sm btn-primary btn-outline text-capitalize" id="add-table-field"><i class="feather icon-plus"></i>&nbsp;&nbsp;{{(trans('admin.scaffold.add_field'))}}</button>
+                    <button type="button" class="btn btn-sm btn-primary btn-outline text-capitalize ml-1" id="sync-translation-with-comment"><i class="feather icon-repeat"></i>&nbsp;&nbsp;{{(trans('admin.scaffold.sync_translation_with_comment'))}}</button>
+                </div>
+
+                <div class="row">
+                    <div class="form-group text-capitalize" style="margin-right: 20px;">
+                        <span for="titleTranslation">{{(trans('admin.scaffold.translate_title'))}}&nbsp;&nbsp;</span>
+                        <input type="text"
+                               name="translate_title"
+                               class="form-control"
+                               id="titleTranslation"
+                               placeholder="{{(trans('admin.scaffold.translate_title'))}}"
+                               value="{{ request('translate_title') }}"
+                               style="width: 150px;">
+                    </div>
+
+                    <div class="form-group text-capitalize" style="margin-right: 20px;">
+                        <span for="inputPrimaryKey">{{(trans('admin.scaffold.pk'))}}&nbsp;&nbsp;</span>
+                        <input
+                                type="text"
+                                name="primary_key"
+                                class="form-control"
+                                id="inputPrimaryKey"
+                                placeholder="{{(trans('admin.scaffold.pk'))}}"
+                                value="{{ request('primary_key') ?: 'id' }}"
+                                style="width: 100px;">
+                    </div>
+
+                    <div class='form-group text-capitalize'>
+                        {!! $timestamps->render() !!}
+                        {!! $soft->render() !!}
+                    </div>
+
+                </div>
+            </div>
+
+            <!-- /.box-body -->
+            <div class="box-footer d-flex justify-content-between">
+                <div></div>
+                <button type="submit" class="btn btn-primary text-capitalize"><i class="feather icon-save"></i> {{(trans('admin.submit'))}}</button>
+            </div>
+
+        {{ csrf_field() }}
+
+        </form>
+
+    </div>
+
+</div>
+
+<template id="table-field-tpl">
+    <tr>
+        <td>
+            <input type="text" value="{name}" name="fields[__index__][name]" class="form-control" placeholder="{{trans('admin.scaffold.field')}}" />
+        </td>
+        <td>
+            <input type="text" value="{translation}" name="fields[__index__][translation]" class="form-control" placeholder="{{trans('admin.scaffold.translation')}}" />
+        </td>
+        <td>
+            <select style="width: 200px" name="fields[__index__][type]">
+                @foreach($dbTypes as $type)
+                    <option value="{{ $type }}">{{$type}}</option>
+                @endforeach
+            </select>
+        </td>
+        <td>
+            <div class="vs-checkbox-con vs-checkbox-primary" >
+                <input {nullable} name="fields[__index__][nullable]" type="checkbox"  />
+                <span class="vs-checkbox vs-checkbox-">
+                  <span class="vs-checkbox--check">
+                    <i class="vs-icon feather icon-check"></i>
+                  </span>
+                </span>
+            </div>
+        <td>
+            <select style="width: 150px" name="fields[__index__][key]">
+                <option value="" selected>NULL</option>
+                <option value="unique">Unique</option>
+                <option value="index">Index</option>
+            </select>
+        </td>
+        <td><input value="{default}" type="text" class="form-control" placeholder="{{trans('admin.scaffold.default')}}" name="fields[__index__][default]"></td>
+        <td><input value="{comment}" type="text" class="form-control" placeholder="{{trans('admin.scaffold.comment')}}" name="fields[__index__][comment]"></td>
+        <td>
+            <button class="btn btn-sm btn-white table-field-sort-handle" type="button" title="{{trans('admin.order')}}"><i class="fa fa-sort"></i></button>
+            <button class="btn btn-sm btn-white table-field-remove"><i class="feather icon-trash"></i></button>
+        </td>
+    </tr>
+</template>
+
+<script>
+    Dcat.ready(function () {
+        var $model = $('#inputModelName'),
+            $controller = $('#inputControllerName'),
+            $repository = $('#inputRepositoryName'),
+            $table = $('#inputTableName'),
+            $fieldsBody = $('#table-fields tbody'),
+            tpl = $('#table-field-tpl').html(),
+            modelNamespace = 'App\\Models\\',
+            namespaceBase = '{{ str_replace( '\\', '\\\\', $namespaceBase ) }}',
+            repositoryNamespace = namespaceBase + '\\Repositories\\',
+            controllerNamespace = namespaceBase + '\\Controllers\\',
+            dataTypeMap = {!! json_encode($dataTypeMap) !!},
+            helpers = Dcat.helpers;
+
+        var withSingularName = helpers.debounce(function (table) {
+            $.ajax('{{ url(request()->path()) }}?singular=' + table, {
+                success: function (data) {
+                    writeController(data.value);
+                    writeModel(data.value);
+                    witeRepository(data.value);
+                }
+            });
+        }, 500);
+
+        $('select').select2();
+
+        var sortable = Sortable.create(document.getElementById("table-fields-sortable"),{
+            handle:'.table-field-sort-handle',
+            onEnd: function () {
+                getTR().each(function(index){
+                    $(this).find("[name^='fields']").each(function(){
+                        var newName = $(this).attr('name').replace(/fields\[(\d)\]/, `fields[${index}]`);
+                        $(this).attr('name', newName);
+                    })
+                });
+            }
+        });
+
+        $('#add-table-field').click(function (event) {
+            addField();
+        });
+
+        $('#sync-translation-with-comment').click(function (event) {
+            var element = $('#table-fields-sortable tr');
+            if (element.length > 0) {
+                element.each(function (i, v) {
+                    var translation = $(v).find('input[name="fields[' + i + '][translation]"]');
+                    var comment = $(v).find('input[name="fields[' + i + '][comment]"]');
+                    if (translation.val() !== "" && comment.val() === "") {
+                        comment.val(translation.val());
+                    }
+                    if (translation.val() === "" && comment.val() !== "") {
+                        translation.val(comment.val());
+                    }
+                });
+            }
+        });
+
+        $('#table-fields').on('click', '.table-field-remove', function(event) {
+            $(event.target).closest('tr').remove();
+        });
+
+        $('#scaffold').on('submit', function (event) {
+
+            //event.preventDefault();
+
+            if ($table.val() == '') {
+                $table.closest('.form-group').addClass('has-error');
+                $('#table-name-help').show();
+
+                return false;
+            }
+
+            return true;
+        });
+
+        $('.choose-exist-table').on('change', function () {
+            var val = $(this).val(), tb, db;
+            if (val == '0') {
+                $table.val('');
+                getTR().remove();
+                return;
+            }
+            val = val.split('|');
+            db = val[0];
+            tb = val[1];
+
+            Dcat.loading();
+            $table.val(tb);
+
+            withSingularName(tb);
+
+            $.post({
+                url: '{{ admin_url('helpers/scaffold/table') }}',
+                data: {db: db, tb: tb},
+                success: function (res) {
+                    Dcat.loading(false);
+
+                    if (!res.list) return;
+                    var i, list = res.list, $id = $('#inputPrimaryKey'), updated, created, soft;
+
+                    getTR().remove();
+                    for (i in list) {
+                        if (list[i].id) {
+                            $id.val(i);
+                            continue;
+                        }
+                        if (i == 'updated_at') {
+                            updated = list[i];
+                            continue;
+                        }
+                        if (i == 'created_at') {
+                            created = list[i];
+                            continue;
+                        }
+                        if (i == 'deleted_at') {
+                            soft = list[i];
+                            continue;
+                        }
+
+                        var c = helpers.replace(list[i].comment, '"', '');
+                        addField({
+                            name: i,
+                            lang: c,
+                            type: list[i].type,
+                            default: helpers.replace(list[i].default, '"', ''),
+                            comment: c,
+                            nullable: list[i].nullable != 'NO',
+                        });
+                    }
+
+                    addTimestamps(updated, created);
+                    addSoftdelete(soft);
+                }
+            });
+
+        });
+
+        $table.on('keyup', function (e) {
+            withSingularName($(this).val());
+        });
+
+        function addTimestamps(updated, created) {
+            if (updated && created) {
+                return;
+            }
+            $('[name="timestamps"]').prop('checked', false);
+
+            var c;
+            if (updated) {
+                c = helpers.replace(updated.comment, '"', '');
+                addField({
+                    name: 'updated_at',
+                    lang: c,
+                    type: updated.type,
+                    default: helpers.replace(updated.default, '"', ''),
+                    comment: c,
+                    nullable: updated.nullable != 'NO',
+                });
+            }
+            if (created) {
+                c = helpers.replace(created.comment, '"', '');
+                addField({
+                    name: 'created_at',
+                    lang: c,
+                    type: created.type,
+                    default: helpers.replace(created.default, '"', ''),
+                    comment: c,
+                    nullable: created.nullable != 'NO',
+                });
+            }
+        }
+
+        function addSoftdelete(soft) {
+            if (soft) {
+                $('[name="soft_deletes"]').prop('checked', true);
+            }
+        }
+
+        function addField(val) {
+            val = val || {};
+
+            var idx = getTR().length,
+                $field = $(
+                    tpl
+                        .replace(/__index__/g, idx)
+                        .replace(/{name}/g, val.name || '')
+                        .replace(/{translation}/g, val.lang || '')
+                        .replace(/{default}/g, val.default || '')
+                        .replace(/{comment}/g, val.comment || '')
+                        .replace(/{nullable}/g, val.nullable ? 'checked' : '')
+                ),
+                i;
+
+            $fieldsBody.append($field);
+            $('select').select2();
+
+            // 选中字段类型
+            for (i in dataTypeMap) {
+                if (val.type == i) {
+                    $field.find('[name="fields['+ idx +'][type]"]')
+                        .val(dataTypeMap[i])
+                        .trigger("change");
+                }
+            }
+
+        }
+
+        function writeController(val) {
+            val = ucfirst(toHump(toLine(val)));
+            $controller.val(val ? (controllerNamespace + val + 'Controller') : controllerNamespace);
+        }
+        function writeModel(val) {
+            $model.val(modelNamespace + ucfirst(ucfirst(toHump(toLine(val)))));
+        }
+        function witeRepository(val) {
+            $repository.val(repositoryNamespace + ucfirst(ucfirst(toHump(toLine(val)))))
+        }
+
+        function getTR() {
+            return $('#table-fields tbody tr');
+        }
+
+        // 下划线转换驼峰
+        function toHump(name) {
+            return name.replace(/\_(\w)/g, function (all, letter) {
+                return letter.toUpperCase();
+            });
+        }
+
+        // 驼峰转换下划线
+        function toLine(name) {
+            return name.replace(/([A-Z])/g,"_$1").toLowerCase();
+        }
+
+        function ucfirst(str) {
+            var reg = /\b(\w)|\s(\w)/g;
+            return str.replace(reg,function(m){
+                return m.toUpperCase()
+            });
+        }
+    });
+</script>

+ 1 - 1
src/Admin.php

@@ -31,7 +31,7 @@ class Admin
     use HasAssets;
     use HasHtml;
 
-    const VERSION = '2.1.5-beta';
+    const VERSION = '2.2.0-beta';
 
     const SECTION = [
         // 往 <head> 标签内输入内容

+ 1 - 0
src/Application.php

@@ -81,6 +81,7 @@ class Application
 
     public function routes($pathOrCallback)
     {
+        $this->switch(static::DEFAULT);
         $this->loadRoutesFrom($pathOrCallback, static::DEFAULT);
 
         if ($apps = $this->getApps()) {

+ 24 - 9
src/Console/PublishCommand.php

@@ -8,6 +8,7 @@ use Illuminate\Support\ServiceProvider;
 use Illuminate\Support\Str;
 use League\Flysystem\Adapter\Local as LocalAdapter;
 use League\Flysystem\Filesystem as Flysystem;
+use League\Flysystem\Local\LocalFilesystemAdapter;
 use League\Flysystem\MountManager;
 
 class PublishCommand extends Command
@@ -145,23 +146,37 @@ class PublishCommand extends Command
 
     protected function publishDirectory($from, $to)
     {
+        $localClass = class_exists(LocalAdapter::class) ? LocalAdapter::class : LocalFilesystemAdapter::class;
+
         $this->moveManagedFiles(new MountManager([
-            'from' => new Flysystem(new LocalAdapter($from)),
-            'to' => new Flysystem(new LocalAdapter($to)),
+            'from' => new Flysystem(new $localClass($from)),
+            'to' => new Flysystem(new $localClass($to)),
         ]));
 
         $this->status($from, $to, 'Directory');
     }
 
-    protected function moveManagedFiles($manager)
+    protected function moveManagedFiles(MountManager $manager)
     {
+        if (method_exists($manager, 'put')) {
+            foreach ($manager->listContents('from://', true) as $file) {
+                if (
+                    $file['type'] === 'file'
+                    && (! $manager->has('to://'.$file['path']) || $this->option('force'))
+                    && ! $this->isExceptPath($manager, $file['path'])
+                ) {
+                    $manager->put('to://'.$file['path'], $manager->read('from://'.$file['path']));
+                }
+            }
+
+            return;
+        }
+
         foreach ($manager->listContents('from://', true) as $file) {
-            if (
-                $file['type'] === 'file'
-                && (! $manager->has('to://'.$file['path']) || $this->option('force'))
-                && ! $this->isExceptPath($manager, $file['path'])
-            ) {
-                $manager->put('to://'.$file['path'], $manager->read('from://'.$file['path']));
+            $path = Str::after($file['path'], 'from://');
+
+            if ($file['type'] === 'file' && (! $manager->fileExists('to://'.$path) || $this->option('force'))) {
+                $manager->write('to://'.$path, $manager->read($file['path']));
             }
         }
     }

+ 1 - 1
src/Console/stubs/config.stub

@@ -341,7 +341,7 @@ return [
     |--------------------------------------------------------------------------
     |
     */
-    'exception_handler' => Dcat\Admin\Http\Exception\Handler::class,
+    'exception_handler' => Dcat\Admin\Exception\Handler::class,
 
     /*
     |--------------------------------------------------------------------------

+ 1 - 2
src/Extend/Manager.php

@@ -5,7 +5,6 @@ namespace Dcat\Admin\Extend;
 use Dcat\Admin\Admin;
 use Dcat\Admin\Exception\AdminException;
 use Dcat\Admin\Exception\RuntimeException;
-use Dcat\Admin\Models\Extension;
 use Dcat\Admin\Models\Extension as ExtensionModel;
 use Dcat\Admin\Support\Composer;
 use Dcat\Admin\Support\Helper;
@@ -97,7 +96,7 @@ class Manager
     {
         $name = $this->getName($name);
 
-        $extension = Extension::where('name', $name)->first();
+        $extension = ExtensionModel::where('name', $name)->first();
 
         if (! $extension) {
             throw new RuntimeException(sprintf('Please install the extension(%s) first!', $name));

+ 1 - 1
src/Form.php

@@ -998,7 +998,7 @@ class Form implements Renderable
             $columns = $field->column();
 
             // If column not in input array data, then continue.
-            if (! Arr::has($updates, $columns)) {
+            if (! Arr::has($updates, $columns) || Arr::has($prepared, $columns)) {
                 continue;
             }
 

+ 4 - 2
src/Form/Concerns/HasTabs.php

@@ -17,11 +17,13 @@ trait HasTabs
      *
      * @param  string  $title
      * @param  Closure  $content
+     * @param  bool  $active
+     * @param  string|null  $id
      * @return $this
      */
-    public function tab($title, Closure $content, $active = false)
+    public function tab($title, Closure $content, $active = false, ?string $id = null)
     {
-        $this->getTab()->append($title, $content, $active);
+        $this->getTab()->append($title, $content, $active, $id);
 
         return $this;
     }

+ 3 - 3
src/Form/Field.php

@@ -537,7 +537,7 @@ class Field implements Renderable
     /**
      * Set or get value of the field.
      *
-     * @param  null  $value
+     * @param  mixed|null  $value
      * @return mixed|$this
      */
     public function value($value = null)
@@ -640,7 +640,7 @@ class Field implements Renderable
     /**
      * Get or set label of the field.
      *
-     * @param  null  $label
+     * @param  mixed|null  $label
      * @return $this|string
      */
     public function label($label = null)
@@ -1065,7 +1065,7 @@ class Field implements Renderable
     /**
      * @param  array|string  $labelClass
      * @param  bool  $append
-     * @return $this|string
+     * @return $this
      */
     public function setLabelClass($labelClass, bool $append = true)
     {

+ 3 - 1
src/Form/Field/ArrayField.php

@@ -57,7 +57,9 @@ class ArrayField extends HasMany
 
     public function buildNestedForm($key = null)
     {
-        $form = new NestedForm($this->column);
+        $form = new NestedForm($this->getNestedFormColumnName());
+
+        $this->setNestedFormDefaultKey($form);
 
         $form->setForm($this->form)
             ->setKey($key);

+ 4 - 4
src/Form/Field/Autocomplete.php

@@ -119,15 +119,15 @@ class Autocomplete extends Text
     {
         $this->formatGroupOptions();
 
+        $this->configs([
+            'groupBy' => $this->groupBy,
+        ]);
+
         $this->addVariables([
             'options' => json_encode($this->options, \JSON_UNESCAPED_UNICODE),
             'configs' => JavaScript::format($this->configs),
         ]);
 
-        $this->configs([
-            'groupBy' => $this->groupBy,
-        ]);
-
         return parent::render();
     }
 

+ 50 - 2
src/Form/Field/HasMany.php

@@ -22,7 +22,17 @@ class HasMany extends Field
      *
      * @var string
      */
-    protected $relationName = '';
+    protected $relationName;
+
+    /**
+     * @var string
+     */
+    protected $parentRelationName;
+
+    /**
+     * @var string|int
+     */
+    protected $parentKey;
 
     /**
      * Relation key name.
@@ -350,6 +360,41 @@ class HasMany extends Field
         );
     }
 
+    public function setParentRelationName($name, $parentKey)
+    {
+        $this->parentRelationName = $name;
+        $this->parentKey = $parentKey;
+
+        return $this;
+    }
+
+    public function getNestedFormColumnName()
+    {
+        if ($this->parentRelationName) {
+            $key = $this->parentKey ?? (NestedForm::DEFAULT_KEY_PREFIX.NestedForm::DEFAULT_PARENT_KEY_NAME);
+
+            return $this->parentRelationName.'.'.$key.'.'.$this->column;
+        }
+
+        return $this->column;
+    }
+
+    protected function getNestedFormDefaultKeyName()
+    {
+        if ($this->parentRelationName) {
+            // hasmany嵌套table时,需要重新设置行的默认key
+            return $this->parentRelationName.'_NKEY_';
+        }
+    }
+
+    protected function setNestedFormDefaultKey(Form\NestedForm $form)
+    {
+        if ($this->parentRelationName) {
+            // hasmany嵌套table时需要特殊处理
+            $form->setDefaultKey(Form\NestedForm::DEFAULT_KEY_PREFIX.$this->getNestedFormDefaultKeyName());
+        }
+    }
+
     /**
      * Build a Nested form.
      *
@@ -358,7 +403,9 @@ class HasMany extends Field
      */
     public function buildNestedForm($key = null)
     {
-        $form = new Form\NestedForm($this->column, $key);
+        $form = new Form\NestedForm($this->getNestedFormColumnName(), $key);
+
+        $this->setNestedFormDefaultKey($form);
 
         $form->setForm($this->form);
 
@@ -576,6 +623,7 @@ class HasMany extends Field
             'options'      => $this->options,
             'count'        => count($this->value()),
             'columnClass'  => $this->columnClass,
+            'parentKey'    => $this->getNestedFormDefaultKeyName(),
         ]);
 
         return parent::render();

+ 0 - 12
src/Form/Field/Image.php

@@ -122,16 +122,4 @@ class Image extends File
 
         $this->uploadAndDeleteOriginalThumbnail($file);
     }
-
-    /**
-     * Destroy original files.
-     *
-     * @return void.
-     */
-    public function destroy()
-    {
-        parent::destroy();
-
-        $this->destroyThumbnail();
-    }
 }

+ 4 - 2
src/Form/Field/ImageField.php

@@ -132,7 +132,7 @@ trait ImageField
 
         if (is_array($file)) {
             foreach ($file as $f) {
-                $this->destroyThumbnail($f);
+                $this->destroyThumbnail($f, $force);
             }
 
             return;
@@ -188,7 +188,9 @@ trait ImageField
             }
         }
 
-        $this->destroyThumbnail();
+        if (! is_array($this->original)) {
+            $this->destroyThumbnail();
+        }
 
         return $this;
     }

+ 4 - 0
src/Form/Field/UploadField.php

@@ -394,6 +394,10 @@ trait UploadField
             return;
         }
 
+        if (method_exists($this, 'destroyThumbnail')) {
+            $this->destroyThumbnail($paths);
+        }
+
         $storage = $this->getStorage();
 
         foreach ((array) $paths as $path) {

+ 29 - 5
src/Form/NestedForm.php

@@ -10,7 +10,9 @@ use Illuminate\Support\Collection;
 
 class NestedForm extends WidgetForm
 {
-    const DEFAULT_KEY_NAME = '__LA_KEY__';
+    const DEFAULT_KEY_PREFIX = 'new_';
+    const DEFAULT_PARENT_KEY_NAME = '__PARENT_NESTED__';
+    const DEFAULT_KEY_NAME = '__NESTED__';
 
     const REMOVE_FLAG_NAME = '_remove_';
 
@@ -24,10 +26,15 @@ class NestedForm extends WidgetForm
     /**
      * NestedForm key.
      *
-     * @var
+     * @var string
      */
     protected $key;
 
+    /**
+     * @var string
+     */
+    protected $defaultKey;
+
     /**
      * Fields in form.
      *
@@ -289,6 +296,11 @@ class NestedForm extends WidgetForm
             $this->form->builder()->pushField((clone $field)->display(false));
         }
 
+        if ($field instanceof Form\Field\HasMany) {
+            // HasMany以及array嵌套table,需要保存上级字段名
+            $field->setParentRelationName($this->relationName, $this->key);
+        }
+
         $this->callResolvingFieldCallbacks($field);
 
         $field->setRelation([
@@ -341,6 +353,18 @@ class NestedForm extends WidgetForm
         return $this;
     }
 
+    public function getDefaultKey()
+    {
+        return $this->defaultKey ?: (static::DEFAULT_KEY_PREFIX.static::DEFAULT_KEY_NAME);
+    }
+
+    public function setDefaultKey($key)
+    {
+        $this->defaultKey = $key;
+
+        return $this;
+    }
+
     /**
      * Set `errorKey` `elementName` `elementClass` for fields inside hasmany fields.
      *
@@ -353,17 +377,17 @@ class NestedForm extends WidgetForm
 
         $elementName = $elementClass = $errorKey = [];
 
-        $key = $this->key ?: 'new_'.static::DEFAULT_KEY_NAME;
+        $key = $this->key ?? $this->getDefaultKey();
 
         if (is_array($column)) {
             foreach ($column as $k => $name) {
                 $errorKey[$k] = sprintf('%s.%s.%s', $this->relationName, $key, $name);
-                $elementName[$k] = sprintf('%s[%s][%s]', $this->formatName(), $key, $name);
+                $elementName[$k] = Helper::formatElementName($this->formatName().'.'.$key.'.'.$name);
                 $elementClass[$k] = [$this->formatClass(), $this->formatClass($name), $this->formatClass($name, false)];
             }
         } else {
             $errorKey = sprintf('%s.%s.%s', $this->relationName, $key, $column);
-            $elementName = sprintf('%s[%s][%s]', $this->formatName(), $key, $column);
+            $elementName = Helper::formatElementName($this->formatName().'.'.$key.'.'.$column);
             $elementClass = [$this->formatClass(), $this->formatClass($column), $this->formatClass($column, false)];
         }
 

+ 4 - 3
src/Grid/Exporters/AbstractExporter.php

@@ -4,6 +4,7 @@ namespace Dcat\Admin\Grid\Exporters;
 
 use Dcat\Admin\Grid;
 use Illuminate\Support\Arr;
+use Illuminate\Support\Collection;
 use Illuminate\Support\Str;
 
 /**
@@ -206,7 +207,7 @@ abstract class AbstractExporter implements ExporterInterface
             $model->forPage($page, $perPage);
         }
 
-        $array = $this->grid->processFilter()->toArray();
+        $array = $this->grid->processFilter();
 
         $model->reset();
 
@@ -219,7 +220,7 @@ abstract class AbstractExporter implements ExporterInterface
      * @param  array  $data
      * @return array
      */
-    protected function normalize(array $data)
+    protected function normalize(Collection $data)
     {
         foreach ($data as &$row) {
             $row = Arr::dot($row);
@@ -256,7 +257,7 @@ abstract class AbstractExporter implements ExporterInterface
      * @param  array  $data
      * @return array
      */
-    protected function callBuilder(array &$data)
+    protected function callBuilder(Collection &$data)
     {
         if ($data && $this->builder) {
             return ($this->builder)($data);

+ 3 - 0
src/Grid/Filter.php

@@ -13,6 +13,7 @@ use Dcat\Admin\Grid\Filter\Date;
 use Dcat\Admin\Grid\Filter\Day;
 use Dcat\Admin\Grid\Filter\EndWith;
 use Dcat\Admin\Grid\Filter\Equal;
+use Dcat\Admin\Grid\Filter\FindInSet;
 use Dcat\Admin\Grid\Filter\Group;
 use Dcat\Admin\Grid\Filter\Gt;
 use Dcat\Admin\Grid\Filter\Hidden;
@@ -66,6 +67,7 @@ use Illuminate\Support\Traits\Macroable;
  * @method Hidden hidden($name, $value)
  * @method Group group($column, $builder = null, $label = '')
  * @method Newline newline()
+ * @method FindInSet findInSet($column, $label = '')
  */
 class Filter implements Renderable
 {
@@ -107,6 +109,7 @@ class Filter implements Renderable
         'year'         => Year::class,
         'hidden'       => Hidden::class,
         'newline'      => Newline::class,
+        'findInSet'    => FindInSet::class,
     ];
 
     /**

+ 1 - 1
src/Grid/Filter/AbstractFilter.php

@@ -318,7 +318,7 @@ abstract class AbstractFilter
     }
 
     /**
-     * @param  array  $options
+     * @param  array|\Illuminate\Contracts\Support\Arrayable|\Closure  $options
      * @return MultipleSelect
      */
     public function multipleSelect($options = [])

+ 38 - 0
src/Grid/Filter/FindInSet.php

@@ -0,0 +1,38 @@
+<?php
+
+namespace Dcat\Admin\Grid\Filter;
+
+use Illuminate\Support\Arr;
+
+class FindInSet extends AbstractFilter
+{
+    /**
+     * Input value from presenter.
+     *
+     * @var mixed
+     */
+    public $input;
+
+    /**
+     * Get condition of this filter.
+     *
+     * @param  array  $inputs
+     * @return array|mixed|void
+     */
+    public function condition($inputs)
+    {
+        $value = Arr::get($inputs, $this->column);
+
+        if ($value === null) {
+            return;
+        }
+
+        $this->input = $this->value = $value;
+
+        $query = function ($query) {
+            $query->whereRaw("FIND_IN_SET(?, $this->column)", $this->value);
+        };
+
+        return $this->buildCondition($query->bindTo($this));
+    }
+}

+ 6 - 1
src/Grid/Model.php

@@ -535,7 +535,12 @@ class Model
             return;
         }
 
-        return $this->request->get($this->getPerPageName()) ?: $this->perPage;
+        $perPage = $this->request->get($this->getPerPageName()) ?: $this->perPage;
+        if ($perPage) {
+            return (int) $perPage;
+        }
+
+        return null;
     }
 
     /**

+ 1 - 1
src/Http/Controllers/PermissionController.php

@@ -151,7 +151,7 @@ class PermissionController extends AdminController
         $container = collect();
 
         $routes = collect(app('router')->getRoutes())->map(function ($route) use ($prefix, $container) {
-            if (! Str::startsWith($uri = $route->uri(), $prefix) && $prefix) {
+            if (! Str::startsWith($uri = $route->uri(), $prefix) && $prefix && $prefix !== '/') {
                 return;
             }
 

+ 4 - 1
src/Http/Controllers/ScaffoldController.php

@@ -78,6 +78,9 @@ class ScaffoldController extends Controller
         $dbTypes = static::$dbTypes;
         $dataTypeMap = static::$dataTypeMap;
         $action = URL::current();
+        $namespaceBase = 'App\\'.implode('\\', array_map(function ($name) {
+            return Str::studly($name);
+        }, explode(DIRECTORY_SEPARATOR, ltrim(config('admin.directory'), app_path().DIRECTORY_SEPARATOR))));
         $tables = collect($this->getDatabaseColumns())->map(function ($v) {
             return array_keys($v);
         })->toArray();
@@ -87,7 +90,7 @@ class ScaffoldController extends Controller
             ->description(' ')
             ->body(view(
                 'admin::helpers.scaffold',
-                compact('dbTypes', 'action', 'tables', 'dataTypeMap')
+                compact('dbTypes', 'action', 'tables', 'dataTypeMap', 'namespaceBase')
             ));
     }
 

+ 1 - 11
src/Http/Middleware/Authenticate.php

@@ -26,17 +26,7 @@ class Authenticate
             return $next($request);
         }
 
-        $loginPage = admin_base_path('auth/login');
-
-        if ($request->ajax() && ! $request->pjax()) {
-            return response()->json(['message' => 'Unauthorized.', 'login' => $loginPage], 401);
-        }
-
-        if ($request->pjax()) {
-            return response("<script>location.href = '$loginPage';</script>");
-        }
-
-        return redirect()->guest($loginPage);
+        return admin_redirect('auth/login', 401);
     }
 
     /**

+ 2 - 2
src/Http/Middleware/Session.php

@@ -15,11 +15,11 @@ class Session
         $path_prefix = '';
         $path_arr = parse_url(config('app.url'));
 
-        if (array_key_exists('path', $path_arr) && !empty($path_arr['path'])) {
+        if (array_key_exists('path', $path_arr) && ! empty($path_arr['path'])) {
             $path_prefix = rtrim($path_arr['path'], '/');
         }
 
-        $path = $path_prefix . '/' . trim(config('admin.route.prefix'), '/');
+        $path = $path_prefix.'/'.trim(config('admin.route.prefix'), '/');
 
         config(['session.path' => $path]);
 

+ 3 - 3
src/Scaffold/MigrationCreator.php

@@ -42,10 +42,10 @@ class MigrationCreator extends BaseMigrationCreator
 
         $stub = $this->files->get(__DIR__.'/stubs/create.stub');
 
-        $this->files->put($path, $this->populateStub($name, $stub, $table));
+        $this->files->put($path, $this->populateAdminStub($name, $stub, $table));
         $this->files->chmod($path, 0777);
 
-        $this->firePostCreateHooks($table);
+        $this->firePostCreateHooks($table, $path);
 
         return $path;
     }
@@ -58,7 +58,7 @@ class MigrationCreator extends BaseMigrationCreator
      * @param  string  $table
      * @return mixed
      */
-    protected function populateStub($name, $stub, $table)
+    protected function populateAdminStub($name, $stub, $table)
     {
         return str_replace(
             ['DummyClass', 'DummyTable', 'DummyStructure'],

+ 17 - 0
src/Show/Field.php

@@ -755,4 +755,21 @@ HTML;
     {
         return static::$extendedFields;
     }
+
+    /**
+     * set file size.
+     *
+     * @param  int  $dec
+     * @return Field
+     */
+    public function filesize($dec = 0)
+    {
+        return $this->unescape()->as(function ($value) use ($dec) {
+            if (empty($value)) {
+                return $this;
+            }
+
+            return format_byte($value, $dec);
+        });
+    }
 }

+ 30 - 0
src/Support/Helper.php

@@ -12,6 +12,7 @@ use Illuminate\Http\Request;
 use Illuminate\Support\Arr;
 use Illuminate\Support\Collection;
 use Illuminate\Support\Facades\File;
+use Illuminate\Support\Facades\URL;
 use Illuminate\Support\Str;
 use Symfony\Component\Process\Process;
 
@@ -969,4 +970,33 @@ class Helper
 
         return array_key_exists($key, $arrayOrObject);
     }
+
+    /**
+     * 跳转.
+     *
+     * @param  string  $to
+     * @param  int  $statusCode
+     * @param  Request  $request
+     * @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\JsonResponse|\Illuminate\Http\RedirectResponse|\Illuminate\Http\Response|\Illuminate\Routing\Redirector
+     */
+    public static function redirect($to, int $statusCode = 302, $request = null)
+    {
+        $request = $request ?: request();
+
+        if (! URL::isValidUrl($to)) {
+            $to = admin_base_path($to);
+        }
+
+        if ($request->ajax() && ! $request->pjax()) {
+            return response()->json(['redirect' => $to], $statusCode);
+        }
+
+        if ($request->pjax()) {
+            return response("<script>location.href = '{$to}';</script>");
+        }
+
+        $redirectCodes = [201, 301, 302, 303, 307, 308];
+
+        return redirect($to, in_array($statusCode, $redirectCodes, true) ? $statusCode : 302);
+    }
 }

+ 38 - 0
src/Support/helpers.php

@@ -4,6 +4,7 @@ use Dcat\Admin\Admin;
 use Dcat\Admin\Support\Helper;
 use Illuminate\Contracts\Support\Htmlable;
 use Illuminate\Contracts\Support\Renderable;
+use Illuminate\Http\Request;
 use Illuminate\Support\MessageBag;
 use Symfony\Component\HttpFoundation\Response;
 
@@ -555,3 +556,40 @@ if (! function_exists('admin_exit')) {
         Admin::exit($response);
     }
 }
+
+if (! function_exists('admin_redirect')) {
+    /**
+     * 跳转.
+     *
+     * @param  string  $to
+     * @param  int  $statusCode
+     * @param  Request  $request
+     * @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\JsonResponse|\Illuminate\Http\RedirectResponse|\Illuminate\Http\Response|\Illuminate\Routing\Redirector
+     */
+    function admin_redirect($to, int $statusCode = 302, Request $request = null)
+    {
+        return Helper::redirect($to, $statusCode, $request);
+    }
+}
+
+if (! function_exists('format_byte')) {
+    /**
+     * 文件单位换算.
+     *
+     * @param $input
+     * @param  int  $dec
+     * @return string
+     */
+    function format_byte($input, $dec = 0)
+    {
+        $prefix_arr = ['B', 'KB', 'MB', 'GB', 'TB'];
+        $value = round($input, $dec);
+        $i = 0;
+        while ($value > 1024) {
+            $value /= 1024;
+            $i++;
+        }
+
+        return round($value, $dec).$prefix_arr[$i];
+    }
+}

+ 18 - 6
src/Traits/ModelTree.php

@@ -15,6 +15,7 @@ use Spatie\EloquentSortable\SortableTrait;
  * @property string $parentColumn
  * @property string $titleColumn
  * @property string $orderColumn
+ * @property string $depthColumn
  * @property string $defaultParentId
  * @property array $sortable
  */
@@ -37,7 +38,7 @@ trait ModelTree
      */
     public function getParentColumn()
     {
-        return empty($this->parentColumn) ? 'parent_id' : $this->parentColumn;
+        return property_exists($this, 'parentColumn') ? $this->parentColumn : 'parent_id';
     }
 
     /**
@@ -47,7 +48,7 @@ trait ModelTree
      */
     public function getTitleColumn()
     {
-        return empty($this->titleColumn) ? 'title' : $this->titleColumn;
+        return property_exists($this, 'titleColumn') ? $this->titleColumn : 'title';
     }
 
     /**
@@ -57,7 +58,17 @@ trait ModelTree
      */
     public function getOrderColumn()
     {
-        return empty($this->orderColumn) ? 'order' : $this->orderColumn;
+        return property_exists($this, 'orderColumn') ? $this->orderColumn : 'order';
+    }
+
+    /**
+     * Get depth column name.
+     *
+     * @return string
+     */
+    public function getDepthColumn()
+    {
+        return property_exists($this, 'depthColumn') ? $this->depthColumn : '';
     }
 
     /**
@@ -65,7 +76,7 @@ trait ModelTree
      */
     public function getDefaultParentId()
     {
-        return isset($this->defaultParentId) ? $this->defaultParentId : '0';
+        return property_exists($this, 'defaultParentId') ? $this->defaultParentId : '0';
     }
 
     /**
@@ -148,7 +159,7 @@ trait ModelTree
      * @param  array  $tree
      * @param  int  $parentId
      */
-    public static function saveOrder($tree = [], $parentId = 0)
+    public static function saveOrder($tree = [], $parentId = 0, $depth = 1)
     {
         if (empty(static::$branchOrder)) {
             static::setBranchOrder($tree);
@@ -159,10 +170,11 @@ trait ModelTree
 
             $node->{$node->getParentColumn()} = $parentId;
             $node->{$node->getOrderColumn()} = static::$branchOrder[$branch['id']];
+            $node->getDepthColumn() && $node->{$node->getDepthColumn()} = $depth;
             $node->save();
 
             if (isset($branch['children'])) {
-                static::saveOrder($branch['children'], $branch['id']);
+                static::saveOrder($branch['children'], $branch['id'], $depth + 1);
             }
         }
     }

+ 4 - 4
src/Widgets/DialogForm.php

@@ -28,7 +28,7 @@ class DialogForm
         'query'          => null,
         'lang'           => null,
         'forceRefresh'   => false,
-        'reset'          => true,
+        'resetButton'    => true,
     ];
 
     /**
@@ -108,7 +108,7 @@ class DialogForm
      */
     public function resetButton(bool $value = true)
     {
-        $this->options['reset'] = $value;
+        $this->options['resetButton'] = $value;
 
         return $this;
     }
@@ -226,7 +226,7 @@ class DialogForm
             <<<JS
 (function () {
     var opts = {$opts};
-    
+
     opts.success = function (success, response) {
         {$this->handlers['success']}
     };
@@ -236,7 +236,7 @@ class DialogForm
     opts.saved = function (success, response) {
         {$this->handlers['saved']}
     };
-    
+
     Dcat.DialogForm(opts);
 })();
 JS

+ 2 - 2
tests/Browser/Components/Form/Field/HasMany.php

@@ -48,8 +48,8 @@ class HasMany extends Component
     {
         return [
             '@container' => '.has-many-'.$this->relation,
-            '@add' => '.add',
-            '@remove' => '.remove',
+            '@add' => ".{$this->relation}-add",
+            '@remove' => ".{$this->relation}-remove",
             '@forms' => ".has-many-{$this->relation}-forms",
             '@group' => ".has-many-{$this->relation}-forms .has-many-{$this->relation}-form",
         ];

+ 1 - 1
tests/bin/install-admin.sh

@@ -4,6 +4,6 @@ cd ./laravel-tests
 php artisan admin:publish --force
 php artisan admin:install
 php artisan migrate:rollback
-php artisan dusk:chrome-driver 93
+php artisan dusk:chrome-driver 98
 cp -f ./tests/routes.php ./app/Admin/
 cp -rf ./tests/resources/config ./config/

Some files were not shown because too many files changed in this diff