diff --git a/README.md b/README.md index 593243f..2b4c3bf 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,55 @@ -# AimAdmin - Laravel Admin Panel for Streamlined Development -Welcome to AimAdmin, the ultimate Laravel admin panel plugin engineered to elevate developers' productivity and streamline development workflows. AimAdmin empowers Laravel developers to concentrate exclusively on crafting domain-specific features and functionalities by abstracting away the complexities of building admin UI/UX and common CRUD functionality. With seamless integration and an intuitive interface, AimAdmin accelerates project development without compromising quality or flexibility. -# Key Benefits for Laravel Developers -- **Effortless Integration:** Seamlessly integrate AimAdmin into your Laravel application with minimal setup and configuration overhead. -- **Time-Saving:** Bid farewell to repetitive tasks. Focus your efforts on building core application features while AimAdmin handles administrative tasks efficiently. -- **Customization Made Easy:** Tailor AimAdmin to fit your project requirements effortlessly. Enjoy flexibility for customization without sacrificing simplicity. -- **Robust Functionality:** Explore a comprehensive suite of features, including user management, role-based access control, content management, and more, readily available out of the box. -- **Enhanced Productivity:** Streamline your development process and expedite project delivery with AimAdmin's ready-to-use admin panel solution. -# Get Started -1. Clone the AimAdmin repository. -2. Install dependencies via Composer. -3. Configure your environment variables. -4. Run migrations and seeders. -5. Dive into crafting your application's unique features while AimAdmin handles administrative tasks seamlessly! - -# Contributions -Join our vibrant community of developers and contribute to making Laravel development even more efficient and accessible. Whether it's through bug fixes, feature enhancements, or documentation improvements, your contributions to AimAdmin are valued and appreciated. +![Aim-Admin](https://github.com/user-attachments/assets/07112a33-3179-4b49-a625-8fe5b47ad900) + +![php-laravel](https://github.com/user-attachments/assets/a146f866-bbfc-4b65-939b-d36fe572227c) +![developed-by-codecoz](https://github.com/user-attachments/assets/041fa195-e167-4b5f-97a7-0d7abf54e26f) + +[![Latest Version](https://img.shields.io/packagist/v/codecoz/aim-admin?color=blue&label=release&style=for-the-badge)](https://packagist.org/packages/codecoz/aim-admin) +[![Stars](https://img.shields.io/github/stars/codecoz/aim-admin?color=rgb%2806%20189%20248%29&label=stars&style=for-the-badge)](https://packagist.org/packages/codecoz/aim-admin) +[![Total Downloads](https://img.shields.io/packagist/dt/codecoz/aim-admin.svg?color=rgb%28249%20115%2022%29&style=for-the-badge)](https://packagist.org/packages/codecoz/aim-admin) +[![Forks](https://img.shields.io/github/forks/codecoz/aim-admin?color=rgb%28134%20115%2022%29&style=for-the-badge)](https://packagist.org/packages/codecoz/aim-admin) +[![Issues](https://img.shields.io/github/issues/codecoz/aim-admin?color=rgb%28134%20239%20128%29&style=for-the-badge)](https://packagist.org/packages/codecoz/aim-admin) + + +# AIM Admin ๐ŸŽ‰ +
+A comprehensive Laravel package designed to simplify admin tasks and develop enterprise web applications. With a robust, flexible, and feature-rich admin dashboard, it promotes best coding practices, following SOLID principles for enhanced code readability, maintainability, and extensibility. + +
+ +## +**Boost Laravel Development with Aim Admin** + +Aim Admin is a powerful Laravel package designed to simplify admin dashboard development. It empowers you to rapidly build and manage data-driven interfaces with its built-in CRUD generator, eliminating the need for extensive boilerplate code. + +**Key Features:** + +* **Effortless CRUD Generation:** Generate complete CRUD (Create, Read, Update, Delete) operations for your database models in a single command, saving you time and effort. +## +* **Elegant Admin Interface:** Leverage the popular AdminLTE template for a visually appealing and user-friendly admin dashboard experience. +## +* **Best Coding Practices:** Aim Admin promotes Laravel's best practices and coding standards, ensuring your applications are maintainable and scalable. +## +* **Flexible Customization:** Easily tailor the generated code and interface to perfectly match your specific project requirements. +## +* **Highly Extensible:** Benefit from Laravel's modular architecture to extend Aim Admin's functionality and integrate with other third-party libraries. + +## +**Installation:** + +```bash +composer require codecoz/aim-admin +``` + +**Documentation:** + +For detailed usage instructions and examples, please refer to the comprehensive documentation: https://codecoz.com/aim-admin + +## + +**Contributing:** + +We welcome contributions from the community! Please feel free to submit bug reports, feature requests, or pull requests. + +**License:** + +Aim Admin is licensed under the MIT License. diff --git a/changelog.md b/changelog.md new file mode 100644 index 0000000..0a2ac99 --- /dev/null +++ b/changelog.md @@ -0,0 +1,8 @@ +# Changelog + +All notable changes to `Admin` will be documented in this file. + +## Version 1.0 + +### Added +- Everything diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..0be77c7 --- /dev/null +++ b/composer.json @@ -0,0 +1,48 @@ +{ + "name": "codecoz/aim-admin", + "description": "Welcome to AimAdmin, the ultimate Laravel admin panel plugin engineered to elevate developers' productivity and streamline development workflows.", + "license": "MIT", + "minimum-stability": "stable", + "authors": [ + { + "name": "CodeCoz", + "email": "contact@codecoz.com", + "homepage": "https://codecoz.com" + } + ], + "homepage": "https://codecoz.com", + "keywords": [ + "Laravel", + "Aim", + "Admin" + ], + "require": { + "php": "^8.1", + "illuminate/support": "~10|~11" + }, + "require-dev": { + "orchestra/testbench": "~9", + "gajus/dindent": "^2.0", + "phpunit/phpunit": "^10.0" + }, + "autoload": { + "psr-4": { + "CodeCoz\\AimAdmin\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "CodeCoz\\AimAdmin\\Tests\\": "tests/" + } + }, + "extra": { + "laravel": { + "providers": [ + "CodeCoz\\AimAdmin\\AdminServiceProvider" + ], + "aliases": { + "Admin": "CodeCoz\\AimAdmin\\Facades\\Admin" + } + } + } +} diff --git a/config/aim-admin.php b/config/aim-admin.php new file mode 100644 index 0000000..219700e --- /dev/null +++ b/config/aim-admin.php @@ -0,0 +1,106 @@ + [ + CodeCoz\AimAdmin\MenuBuilder\Filters\HrefFilter::class, + CodeCoz\AimAdmin\MenuBuilder\Filters\ActiveFilter::class, + CodeCoz\AimAdmin\MenuBuilder\Filters\ClassesFilter::class, + ], + + 'menu' => [ + // Navbar items: + [ + 'text' => 'Dashboard', + 'icon' => 'ti ti-home', + 'url' => 'dashboard' + ], + + [ + 'text' => 'Support 1', + 'url' => '#', + 'icon' => 'ti ti-help', + 'active' => ['support1'], + 'submenu' => [ + [ + 'text' => 'Ticket', + 'url' => 'support1', + 'icon' => 'ti ti-article', + ] + ], + ], + + [ + 'text' => 'Support 2', + 'url' => '#', + 'icon' => 'ti ti-help', + 'active' => ['support2'], + 'submenu' => [ + [ + 'text' => 'Ticket', + 'url' => 'support2', + 'icon' => 'ti ti-article', + ] + ], + ], + + [ + 'text' => 'Support 3', + 'url' => '#', + 'icon' => 'ti ti-help', + 'active' => ['support3'], + 'submenu' => [ + [ + 'text' => 'Ticket', + 'url' => 'support3', + 'icon' => 'ti ti-article', + ] + ], + ], + + ], + + /* -------------------------------------------------------------------------------------------- + * File Upload Configuration + * -------------------------------------------------------------------------------------------- + */ + 'upload_file_type' => ['png', 'jpg', 'bmp', 'pdf', 'doc', 'xls', 'ppt'], + 'upload_file_size' => 5 * 1024, + + /* -------------------------------------------------------------------------------------------- + * Auth Configuration + * -------------------------------------------------------------------------------------------- + */ + 'auth' => [ + 'controller' => \CodeCoz\AimAdmin\Http\Controllers\Auth\LoginController::class, + 'user_model' => \App\Models\User::class, + 'url' => 'login', + 'logout_url' => 'logout', + 'profile_url' => 'profile', + 'middleware' => ['guest', 'web'], + ], + 'registration' => [ + 'controller' => \CodeCoz\AimAdmin\Http\Controllers\Auth\RegistrationController::class, + 'fields' => [ + 'number' => 'id', + 'text' => 'full_name' + ] + ], + 'back_to_top' => true, + 'layout_class' => [ + 'body' => 'text-sm', + 'brand' => '', + 'sidebar' => '', + 'navbar' => '', + 'footer' => '', + ], + 'footer_text' => 'Anything you want', + //Toast Time + 'flash-timer' => 2000, + 'show_toast_error' => true, + 'show_inline_alert_box' => true, + 'inline_validation_error' => true +]; diff --git a/contributing.md b/contributing.md new file mode 100644 index 0000000..ee76afc --- /dev/null +++ b/contributing.md @@ -0,0 +1,27 @@ +# Contributing + +Contributions are welcome and will be fully credited. + +Contributions are accepted via Pull Requests on [Github](https://github.com/codecoz/aim-admin). + +# Things you could do +If you want to contribute but do not know where to start, this list provides some starting points. +- Add license text +- Remove rewriteRules.php +- Set up TravisCI, StyleCI, ScrutinizerCI +- Write a comprehensive ReadMe + +## Pull Requests + +- **Add tests!** - Your patch won't be accepted if it doesn't have tests. + +- **Document any change in behaviour** - Make sure the `readme.md` and any other relevant documentation are kept up-to-date. + +- **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. + +- **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. + +- **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](http://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. + + +**Happy coding**! diff --git a/database/migrations/0001_01_01_000000_create_users_table.php b/database/migrations/0001_01_01_000000_create_users_table.php new file mode 100644 index 0000000..05fb5d9 --- /dev/null +++ b/database/migrations/0001_01_01_000000_create_users_table.php @@ -0,0 +1,49 @@ +id(); + $table->string('name'); + $table->string('email')->unique(); + $table->timestamp('email_verified_at')->nullable(); + $table->string('password'); + $table->rememberToken(); + $table->timestamps(); + }); + + Schema::create('password_reset_tokens', function (Blueprint $table) { + $table->string('email')->primary(); + $table->string('token'); + $table->timestamp('created_at')->nullable(); + }); + + Schema::create('sessions', function (Blueprint $table) { + $table->string('id')->primary(); + $table->foreignId('user_id')->nullable()->index(); + $table->string('ip_address', 45)->nullable(); + $table->text('user_agent')->nullable(); + $table->longText('payload'); + $table->integer('last_activity')->index(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('users'); + Schema::dropIfExists('password_reset_tokens'); + Schema::dropIfExists('sessions'); + } +}; diff --git a/docs/contents/form.md b/docs/contents/form.md deleted file mode 100644 index e69de29..0000000 diff --git a/docs/index.md b/docs/index.md deleted file mode 100644 index 527165c..0000000 --- a/docs/index.md +++ /dev/null @@ -1,9 +0,0 @@ -# AimAdmin -Welcome to AimAdmin, the ultimate Laravel admin panel plugin engineered to elevate developers' productivity and streamline development workflows. AimAdmin empowers Laravel developers to concentrate exclusively on crafting domain-specific features and functionalities by abstracting away the complexities of building admin UI/UX and common CRUD functionality. With seamless integration and an intuitive interface, AimAdmin accelerates project development without compromising quality or flexibility. - -# Table of Contents - -- [Design](contents/design.md) -- [Crudboard](contents/crudboard.md) -- [Form](contents/form.md) -- [Filter](contents/filter.md) diff --git a/license.md b/license.md new file mode 100644 index 0000000..cd1ce2e --- /dev/null +++ b/license.md @@ -0,0 +1,5 @@ +# The license + +Copyright (c) Md Abdullah Ibne Masud + +...Add your license text here... diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..99702e4 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,14 @@ + + + + + ./tests/Components + ./tests/Service + + + + + src/ + + + diff --git a/resources/css/modules/backtotop.css b/resources/css/modules/backtotop.css new file mode 100644 index 0000000..5a2ab16 --- /dev/null +++ b/resources/css/modules/backtotop.css @@ -0,0 +1,15 @@ +.back-to-top { + transition: opacity 0.5s ease-out, visibility 0.5s; + opacity: 0; + visibility: hidden; +} + +.back-to-top.show { + opacity: 1; + visibility: visible; +} + +.back-to-top:hover { + transform: scale(1.1); + transition: transform 0.3s ease-in-out; +} diff --git a/docs/contents/crudboard.md b/resources/fonts/.gitkeep similarity index 100% rename from docs/contents/crudboard.md rename to resources/fonts/.gitkeep diff --git a/resources/fonts/manrope/manrope-variablefont_wght.ttf b/resources/fonts/manrope/manrope-variablefont_wght.ttf new file mode 100644 index 0000000..f39ca39 Binary files /dev/null and b/resources/fonts/manrope/manrope-variablefont_wght.ttf differ diff --git a/resources/fonts/manrope/manrope-variablefont_wght.woff b/resources/fonts/manrope/manrope-variablefont_wght.woff new file mode 100644 index 0000000..13e37e6 Binary files /dev/null and b/resources/fonts/manrope/manrope-variablefont_wght.woff differ diff --git a/resources/fonts/manrope/manrope-variablefont_wght.woff2 b/resources/fonts/manrope/manrope-variablefont_wght.woff2 new file mode 100644 index 0000000..e195f39 Binary files /dev/null and b/resources/fonts/manrope/manrope-variablefont_wght.woff2 differ diff --git a/docs/contents/design.md b/resources/fonts/piru.ttf similarity index 100% rename from docs/contents/design.md rename to resources/fonts/piru.ttf diff --git a/resources/img/loader.gif b/resources/img/loader.gif new file mode 100644 index 0000000..945b457 Binary files /dev/null and b/resources/img/loader.gif differ diff --git a/resources/img/logo.png b/resources/img/logo.png new file mode 100644 index 0000000..fbb3c20 Binary files /dev/null and b/resources/img/logo.png differ diff --git a/resources/js/adminlte.js b/resources/js/adminlte.js new file mode 100644 index 0000000..b986bc0 --- /dev/null +++ b/resources/js/adminlte.js @@ -0,0 +1,33 @@ +import 'admin-lte/dist/js/adminlte' +import 'admin-lte/plugins/bootstrap/js/bootstrap.bundle' + +// fontawesome +import "./modules/fontawesome"; + +// ChartJs +import "./modules/chartjs"; + +// Select2 +import "./modules/sweetalert2"; + +//Date-rangePicker +import "./modules/daterangepicker"; + +//Date Picker +import "./modules/flatpickr"; + +// Select 2 +import "./modules/select2"; + +//Button Loader +import "./modules/buttonloader"; + +//Modal Loader +import "./modules/modal"; + +//Ajax request +import "./modules/ajaxrequest"; + +//Back to Top +import "./modules/backtotop"; + diff --git a/resources/js/modules/ajaxrequest.js b/resources/js/modules/ajaxrequest.js new file mode 100644 index 0000000..49915bc --- /dev/null +++ b/resources/js/modules/ajaxrequest.js @@ -0,0 +1,166 @@ +$('.btn-submit-action').on('click', function (e) { + $("#myForm").submit(); +}); + +window.fadeOutAndClear = function (elementId, timeout = 2000) { + setTimeout(() => { + $(`#${elementId}`).fadeOut('slow', function () { + $(this).html(''); + $(this).show(); // Ensure itโ€™s not permanently hidden if you want to reuse it. + }); + }, timeout); +}; + +window.ajaxRequest = function (url, data = {}, successCallback, errorCallback, completeCallback, method = 'POST') { + $('.ajax-submit-button').buttonLoader('start'); + let settings = { + url: url, + type: method, + data: data, + headers: { + 'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content') + }, + success: function (response) { + if (successCallback && typeof successCallback === 'function') { + successCallback(response); + } + console.log(response); + if (response.data && response.alert) { + $("#alert").html(response.alert); + if (response.fade_out) { + window.fadeOutAndClear('alert', response.fade_out_delay); + } + } + + if (response.data && response.redirect) { + // Redirect after a delay (customize the delay as needed) + let delay = response.redirect_delay ?? 1500; // Default to 2000ms if not provided + setTimeout(function () { + // Use window.location.href for redirection + window.location.href = response.redirect; + }, delay); + } + }, + error: function (error) { + let response; + if (error && error.responseJSON) { + response = error.responseJSON; + if (response.alert) { + $("#alert").html(response.alert); + } + } else { + console.error('An error occurred:', error); + } + if (response.fade_out) { + window.fadeOutAndClear('alert', response.fade_out_delay); + } + // Call the errorCallback after handling the error + if (errorCallback && typeof errorCallback === 'function') { + errorCallback(error); + } + }, + complete: function (data) { + $('.ajax-submit-button').buttonLoader('stop'); + let response; + response = data.responseJSON; + if (response && response.scroll_to_top) { + $('html, body').scrollTop(0); + } + if (completeCallback && typeof completeCallback === 'function') { + completeCallback(); + } + } + }; + + if (method !== 'GET' && data instanceof FormData) { + settings.processData = false; // don't process the data + settings.contentType = false; // set content type to false as jQuery will tell the server it's a query string request + } + + $.ajax(settings); +}; + +window.ajaxPost = function (url, data, successCallback, errorCallback, completeCallback) { + ajaxRequest(url, data, successCallback, errorCallback, completeCallback); +}; + +window.ajaxGet = function (url, data, successCallback, errorCallback, completeCallback) { + // For a GET request, we need to append data to the URL as query parameters + const queryParams = $.param(data); // Use jQuery's $.param to convert data object to query string + const fullUrl = url + (queryParams ? '?' + queryParams : ''); + + // Call the ajaxRequest function with the full URL including query parameters + // Since GET requests don't have a body, we pass an empty object ({}) for the data parameter + ajaxRequest(fullUrl, {}, successCallback, errorCallback, completeCallback, 'GET'); +}; + +window.ajaxPut = function (url, data, successCallback, errorCallback, completeCallback) { + data.append('_method', 'put'); // Add the _method field with value 'PATCH' + ajaxRequest(url, data, successCallback, errorCallback, completeCallback); +}; + +window.ajaxPatch = function (url, data, successCallback, errorCallback, completeCallback) { + data.append('_method', 'patch'); // Add the _method field with value 'PATCH' + ajaxRequest(url, data, successCallback, errorCallback, completeCallback); +}; + +window.executeAjaxCall = (method, url, data, successCallback, errorCallback, completeCallback) => { + // Determine the AJAX function to use based on the method + let ajaxFunction; + switch (method.toLowerCase()) { + case 'get': + ajaxFunction = window.ajaxGet; + break; + case 'put': + ajaxFunction = window.ajaxPut; + break; + case 'patch': + ajaxFunction = window.ajaxPatch; + break; + // Add more cases as needed + default: + ajaxFunction = window.ajaxPost; + } + + // Call the selected AJAX function + ajaxFunction(url, data, successCallback, errorCallback, completeCallback); +} + + +window.resetForm = function () { + +// Reset text, email, password fields + $('#ajax-form input[type="text"], #ajax-form input[type="email"], #ajax-form input[type="password"], #ajax-form textarea').val(''); + +// Reset radio buttons and checkboxes + $('#ajax-form input[type="radio"], #ajax-form input[type="checkbox"]').prop('checked', false); + +// Reset select dropdowns + $('#ajax-form select').prop('selectedIndex', 0); + +} + +$(document).delegate(".ajax-submit-button", "click", function (event) { + event.preventDefault(); + $("#alert").html(""); + const btn = $(this); + + $('[tinymce-id]').each(function () { + const tmcIdValue = $(this).attr('tinymce-id'); + tinymceEditorsMap[tmcIdValue].triggerSave(); + }); + + const form = btn.closest('form'); // Using .closest() to find the parent form + const formData = new FormData(form[0]); + + const requestMethod = form.attr('method'); // Using attr method + + executeAjaxCall(requestMethod, $(form).attr('action'), formData, function (data) { + console.log('Received data:', data); + // resetForm() + }, function (error) { + console.error('An unexpected error occurred:', error); + }, function () { + console.log('Request completed'); + }); +}); diff --git a/resources/js/modules/backtotop.js b/resources/js/modules/backtotop.js new file mode 100644 index 0000000..9e710c4 --- /dev/null +++ b/resources/js/modules/backtotop.js @@ -0,0 +1,15 @@ +import './../../css/modules/backtotop.css' + +$(document).scroll(function () { + const scrollPosition = $(this).scrollTop(); + if (scrollPosition > 200) { // Adjust 200 to when you want the button to appear + $('.back-to-top').addClass('show'); + } else { + $('.back-to-top').removeClass('show'); + } +}); + +$('.back-to-top').click(function (e) { + e.preventDefault(); + $('html, body').animate({scrollTop: 0}, 'smooth'); +}); diff --git a/resources/js/modules/buttonloader.js b/resources/js/modules/buttonloader.js new file mode 100644 index 0000000..aed53ec --- /dev/null +++ b/resources/js/modules/buttonloader.js @@ -0,0 +1,2 @@ +import './../plugins/button-loader/buttonLoader.css'; +import './../plugins/button-loader/jquery.buttonLoader'; diff --git a/resources/js/modules/chartjs.js b/resources/js/modules/chartjs.js new file mode 100644 index 0000000..68845d5 --- /dev/null +++ b/resources/js/modules/chartjs.js @@ -0,0 +1,9 @@ +// Usage: https://www.chartjs.org/ +// import Chart from "chart.js"; + +import { Chart, registerables } from 'chart.js'; +Chart.register(...registerables); + +// Chart.defaults.global.defaultFontFamily = "'Inter', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"; + +window.Chart = Chart; \ No newline at end of file diff --git a/resources/js/modules/common.js b/resources/js/modules/common.js new file mode 100644 index 0000000..95eb02f --- /dev/null +++ b/resources/js/modules/common.js @@ -0,0 +1,23 @@ +window.generateSlug = (text) => { + return text + .toString() + .toLowerCase() // Convert text to lower case + .replace(/\s+/g, '-') // Replace spaces with - + .replace(/[^\w\-\/]+/g, '') // Remove all non-word chars except underscores, hyphens, and slashes + .replace(/--+/g, '-') // Replace multiple - with single - + .replace(/^-+/, '') // Trim - from start of text + .replace(/-+$/, ''); // Trim - from end of text +} + +$(document).ready(function() { + $("#select-all-checkbox-id").click(function() { + // Toggle other checkboxes based on "select-all" state + $("input[type=checkbox][id^='id-checkbox-']").prop("checked", this.checked); + }); + + $("input[type=checkbox]").click(function() { + // Update "select-all" checkbox based on other checkboxes + const allChecked = $("input[type=checkbox][id^='id-checkbox-']:not(:checked)").length === 0; + $("#select-all-checkbox-id").prop("checked", allChecked); + }); +}); diff --git a/resources/js/modules/datepicker.js b/resources/js/modules/datepicker.js new file mode 100644 index 0000000..eb66502 --- /dev/null +++ b/resources/js/modules/datepicker.js @@ -0,0 +1,22 @@ +import 'bootstrap-datepicker'; +import 'bootstrap-datepicker/dist/css/bootstrap-datepicker.css'; +import 'admin-lte/plugins/daterangepicker/daterangepicker.css' + +$(document).ready(function () { + // Select all datepicker elements + $('.datepicker').each(function () { + // Check if the element is of type 'date' + if ($(this).attr('type') === 'date') { + // Change the type attribute to 'text' + $(this).attr('type', 'text'); + } + + // Initialize the datepicker + $(this).datepicker({ + autoclose: true, + todayHighlight: true, + format: 'dd/mm/yyyy', + }); + }); +}); + diff --git a/resources/js/modules/daterangepicker.js b/resources/js/modules/daterangepicker.js new file mode 100644 index 0000000..3c04e45 --- /dev/null +++ b/resources/js/modules/daterangepicker.js @@ -0,0 +1,4 @@ +//Added Date-range Picker +// import DateRangePicker from 'daterangepicker'; +import 'admin-lte/plugins/daterangepicker/daterangepicker' +import 'admin-lte/plugins/daterangepicker/daterangepicker.css' diff --git a/resources/js/modules/flatpickr.js b/resources/js/modules/flatpickr.js new file mode 100644 index 0000000..1144395 --- /dev/null +++ b/resources/js/modules/flatpickr.js @@ -0,0 +1,5 @@ +import flatpickr from "flatpickr"; + +window.flatpickr = flatpickr; + +import "../../../../../../node_modules/flatpickr/dist/flatpickr.min.css"; diff --git a/resources/js/modules/fontawesome.js b/resources/js/modules/fontawesome.js new file mode 100644 index 0000000..b957f40 --- /dev/null +++ b/resources/js/modules/fontawesome.js @@ -0,0 +1,4 @@ +import '@fortawesome/fontawesome-free/scss/fontawesome.scss' +import '@fortawesome/fontawesome-free/scss/solid.scss' +import '@fortawesome/fontawesome-free/scss/regular.scss' +import '@fortawesome/fontawesome-free/scss/brands.scss' diff --git a/resources/js/modules/modal.js b/resources/js/modules/modal.js new file mode 100644 index 0000000..3964a59 --- /dev/null +++ b/resources/js/modules/modal.js @@ -0,0 +1,10 @@ +$(document).delegate(".native-modal", "click", function (e) { + e.preventDefault(); + const url = $(this).attr('href'); + $("#modal-body-content").load(url, function (response, status, xhr) { + if (status === "error") { + const msg = "Sorry but there was an error: "; + $("#modal-body-content").html(msg + xhr.status + " " + xhr.statusText); + } + }); +}); diff --git a/resources/js/modules/notification.js b/resources/js/modules/notification.js new file mode 100644 index 0000000..d4342e5 --- /dev/null +++ b/resources/js/modules/notification.js @@ -0,0 +1,56 @@ +document.addEventListener('DOMContentLoaded', (event) => { + // Get all list items + const listItems = document.querySelectorAll('.list-group-item'); + + // Add click event listener to each list item + listItems.forEach(item => { + item.addEventListener('click', (e) => { + // Stop event propagation + e.stopPropagation(); + }); + }); + + // Code to handle clicking outside the dropdown to close it + window.addEventListener('click', (e) => { + const dropdownMenu = document.getElementById('notification-list'); + if (!dropdownMenu.contains(e.target)) { + dropdownMenu.classList.remove('show'); + } + }); +}); + +$(document).ready(function () { + $(".notification-button").click(function (event) { + event.preventDefault(); // Prevent the default action of the button + const btn = $(this); + btn.buttonLoader('start'); + const actionUrl = $(this).data('action'); + ajaxGet(actionUrl, [], function (data) { + console.log('Received data:', data); + btn.closest('.list-group-item').hide(); + + const notificationDropdown = $('#notification-dropdown-count'); + const notificationIndicator = $('#notification-indicator-count'); + + const dropdownCount = parseInt(notificationDropdown.text(), 10); + notificationDropdown.text(dropdownCount > 0 ? dropdownCount - 1 : 0); + + const indicatorCount = parseInt(notificationIndicator.text(), 10); + notificationIndicator.text(indicatorCount > 0 ? indicatorCount - 1 : 0); + + // resetForm() + }, function (error) { + btn.buttonLoader('stop'); + console.error('An unexpected error occurred:', error); + }, function () { + btn.buttonLoader('stop'); + console.log('Request completed'); + }); + }); + $(".notification-go").click(function (event) { + event.preventDefault(); // Prevent the default action of the button + const btn = $(this); + btn.buttonLoader('start'); + window.location = $(this).data('action') + }); +}); diff --git a/resources/js/modules/select2.js b/resources/js/modules/select2.js new file mode 100644 index 0000000..d5fda82 --- /dev/null +++ b/resources/js/modules/select2.js @@ -0,0 +1,5 @@ +import select2 from 'admin-lte/plugins/select2/js/select2.full.js' +import 'admin-lte/plugins/select2/css/select2.css' +import 'admin-lte/plugins/select2-bootstrap4-theme/select2-bootstrap4.css' +select2(); +// window.select2 = select2 diff --git a/resources/js/modules/stepper.js b/resources/js/modules/stepper.js new file mode 100644 index 0000000..a895030 --- /dev/null +++ b/resources/js/modules/stepper.js @@ -0,0 +1,2 @@ +import "jquery.steps/dist/jquery-steps"; +import "jquery.steps/dist/jquery-steps.css"; diff --git a/resources/js/modules/sweetalert2.js b/resources/js/modules/sweetalert2.js new file mode 100644 index 0000000..c74cd87 --- /dev/null +++ b/resources/js/modules/sweetalert2.js @@ -0,0 +1,61 @@ +import Swal from 'sweetalert2' + +window.swal = Swal; + +// var popupConfirmList = [].slice.call(document.querySelectorAll('[data-bl-popup="confirm"]')) +// var popupList = popupConfirmList.map(function (popupTriggerEl) { +// return new bootstrap.Tooltip(popupTriggerEl) +// }) + +window.gridDeleteConfirm = function(el) +{ + Swal.fire({ + icon: 'warning', + text: 'Do you want to delete this item?', + showCancelButton: true, + confirmButtonText: 'Delete', + confirmButtonColor: '#e3342f', + }).then((result) => { + if(result.isConfirmed){ + el.submit(); + } + }); + return false; +} + +window.toastFire = (type, msg) => { + Swal.fire({ + toast: true, + timer: 3000, + showCloseButton: true, + position: 'top-end', + showConfirmButton: false, + timerProgressBar: true, + text: msg, + icon: type, + onOpen: (toast) => { + toast.addEventListener('mouseenter', Swal.stopTimer); + toast.addEventListener('mouseleave', Swal.resumeTimer); + } + }); +} + +window.toastFireWithAction = (type, response) => { + Swal.fire({ + toast: true, + timer: 3000, + showCloseButton: true, + position: 'top-end', + showConfirmButton: false, + timerProgressBar: true, + text: response.message, + icon: type, + onOpen: (toast) => { + toast.addEventListener('mouseenter', Swal.stopTimer); + toast.addEventListener('mouseleave', Swal.resumeTimer); + } + }); + if (response.hasOwnProperty('redirect')) { + window.location.replace(response.redirect); + } +} diff --git a/resources/js/plugins/button-loader/README.md b/resources/js/plugins/button-loader/README.md new file mode 100644 index 0000000..37f9bfa --- /dev/null +++ b/resources/js/plugins/button-loader/README.md @@ -0,0 +1,25 @@ +# button-loader +A jquery plugin which add loading indicators into buttons + +## Instructions +You will need to include both the jquery.buttonloader.js and buttonLoader.css along with bootstrap.css and font-awesome.css + +### HTML +The buttons or links that should have the loading indication must be given the class "has-spinner". +Below is an example of a button and a link which have loading indication. + +```sh + +Button02 +``` +### Jquery +To start loading +```sh +$('.my-btn').buttonLoader('start'); +``` +To stop loading +```sh +$('.my-btn').buttonLoader('stop'); +``` +#### Example +http://jsfiddle.net/zcX4h/1154/ diff --git a/resources/js/plugins/button-loader/buttonLoader.css b/resources/js/plugins/button-loader/buttonLoader.css new file mode 100644 index 0000000..5cfb452 --- /dev/null +++ b/resources/js/plugins/button-loader/buttonLoader.css @@ -0,0 +1,22 @@ +.spinner { + display: inline-block; + opacity: 0; + width: 0; + -webkit-transition: opacity 0.25s, width 0.25s; + -moz-transition: opacity 0.25s, width 0.25s; + -o-transition: opacity 0.25s, width 0.25s; + transition: opacity 0.25s, width 0.25s; +} + +.has-spinner.active { + cursor:progress; +} + +.has-spinner.active .spinner { + opacity: 1; + width: auto; +} + +.has-spinner.btn.active .spinner { + min-width: 20px; +} diff --git a/resources/js/plugins/button-loader/jquery.buttonLoader.js b/resources/js/plugins/button-loader/jquery.buttonLoader.js new file mode 100644 index 0000000..16dea3b --- /dev/null +++ b/resources/js/plugins/button-loader/jquery.buttonLoader.js @@ -0,0 +1,33 @@ +/*A jQuery plugin which add loading indicators into buttons +* By Minoli Perera +* MIT Licensed. +*/ +(function ($) { + $.fn.buttonLoader = function (action) { + const selfBtn = $(this); + + if (!selfBtn.hasClass('has-spinner')) { + selfBtn.addClass('has-spinner'); + } + if (action == 'start') { + if (selfBtn.attr("disabled") == "disabled") { + console.log("Button already disabled"); + return false; + } + selfBtn.attr('disabled', true); + selfBtn.attr('data-btn-text', selfBtn.text()); + let btnText = 'Loading...'; + if (selfBtn.attr('data-load-text') != undefined && selfBtn.attr('data-load-text') != "") { + btnText = selfBtn.attr('data-load-text'); + } + selfBtn.html(' ' + btnText); + selfBtn.addClass('active'); + } + if (action == 'stop') { + selfBtn.html(selfBtn.attr('data-btn-text')); + selfBtn.removeClass('active'); + selfBtn.attr('disabled', false); + } + } +})(jQuery); + diff --git a/resources/js/plugins/button-loader/jquery.buttonLoader.min.js b/resources/js/plugins/button-loader/jquery.buttonLoader.min.js new file mode 100644 index 0000000..6e8c25e --- /dev/null +++ b/resources/js/plugins/button-loader/jquery.buttonLoader.min.js @@ -0,0 +1,5 @@ +/*A jQuery plugin which add loading indicators into buttons +* By Minoli Perera +* MIT Licensed. +*/ +!function(t){t(".has-spinner").attr("disabled",!1),t.fn.buttonLoader=function(a){var s=t(this);if("start"==a){if("disabled"==t(s).attr("disabled"))return!1;t(".has-spinner").attr("disabled",!0),t(s).attr("data-btn-text",t(s).text());var e="Loading";if(console.log(t(s).attr("data-load-text")),void 0!=t(s).attr("data-load-text")&&""!=t(s).attr("data-load-text"))var e=t(s).attr("data-load-text");t(s).html(' '+e),t(s).addClass("active")}"stop"==a&&(t(s).html(t(s).attr("data-btn-text")),t(s).removeClass("active"),t(".has-spinner").attr("disabled",!1))}}(jQuery); \ No newline at end of file diff --git a/resources/scss/_variables.scss b/resources/scss/_variables.scss new file mode 100644 index 0000000..3173b29 --- /dev/null +++ b/resources/scss/_variables.scss @@ -0,0 +1,40 @@ +@use "sass:math"; + +// Body +$body-bg: #f8fafc; + +// Typography +$font-family-sans-serif: "Nunito", sans-serif; +$font-size-base: 0.9rem; +$line-height-base: 1.6; + +// Colors +$blue: #3490dc; +$indigo: #6574cd; +$purple: #9561e2; +$pink: #f66D9b; +$orange: #f6993f; +$teal: #4dc0b5; +$cyan: #6cb2eb; + +//Function for BL Brand Guideline + +@function cmyk($c, $m, $y, $k) { + $c: math.div($c, 100); + $m: math.div($m, 100); + $y: math.div($y, 100); + $k: math.div($k, 100); + $r: 255 * (1 - $c) * (1 - $k); + $g: 255 * (1 - $m) * (1 - $k); + $b: 255 * (1 - $y) * (1 - $k); + @return rgb($r, $g, $b); +} + +$bl-color: cmyk(000, 075, 100, 000); +$bl-light: cmyk(000, 060, 100, 000); +$black: cmyk(000, 000, 000, 100); +$green: cmyk(050, 000, 100, 000); +$yellow: cmyk(000, 014, 100, 000); +$red: cmyk(000, 100, 000, 000); + + diff --git a/resources/scss/adminlte.scss b/resources/scss/adminlte.scss new file mode 100644 index 0000000..ef142a9 --- /dev/null +++ b/resources/scss/adminlte.scss @@ -0,0 +1,12 @@ +@import 'admin-lte/dist/css/adminlte.css'; +@import 'admin-lte/plugins/icheck-bootstrap/icheck-bootstrap.css'; +@import 'variables'; +//@import './bl'; +// Fonts +@import url('https://fonts.googleapis.com/css?family=Nunito'); + +.navbar-laravel { + background-color: #fff; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.04); +} + diff --git a/resources/views/alert/ajax-error.blade.php b/resources/views/alert/ajax-error.blade.php new file mode 100644 index 0000000..02fa866 --- /dev/null +++ b/resources/views/alert/ajax-error.blade.php @@ -0,0 +1,10 @@ +@props(['errors']) +@if (!empty($errors)) +
+ +
Alert!
+ @foreach ($errors->all() as $error) + {!! $error !!}
+ @endforeach +
+@endif diff --git a/resources/views/alert/danger.blade.php b/resources/views/alert/danger.blade.php new file mode 100644 index 0000000..6699526 --- /dev/null +++ b/resources/views/alert/danger.blade.php @@ -0,0 +1,24 @@ +@foreach ($errors as $error) + +@endforeach + diff --git a/resources/views/alert/info.blade.php b/resources/views/alert/info.blade.php new file mode 100644 index 0000000..c4bd8f1 --- /dev/null +++ b/resources/views/alert/info.blade.php @@ -0,0 +1,18 @@ + diff --git a/resources/views/alert/inline-validation-error.blade.php b/resources/views/alert/inline-validation-error.blade.php new file mode 100644 index 0000000..64bad04 --- /dev/null +++ b/resources/views/alert/inline-validation-error.blade.php @@ -0,0 +1,8 @@ +@props(['errors']) +@if(config('aim-admin.inline_validation_error', true)) + @if (!empty($errors)) + @foreach ($errors as $error) + {!! $error !!} + @endforeach + @endisset +@endif diff --git a/resources/views/alert/success.blade.php b/resources/views/alert/success.blade.php new file mode 100644 index 0000000..6f8593f --- /dev/null +++ b/resources/views/alert/success.blade.php @@ -0,0 +1,18 @@ + diff --git a/resources/views/alert/validation-error.blade.php b/resources/views/alert/validation-error.blade.php new file mode 100644 index 0000000..66d4a3c --- /dev/null +++ b/resources/views/alert/validation-error.blade.php @@ -0,0 +1,24 @@ +@if ($errors->any()) + @foreach ($errors->all() as $error) + + @endforeach +@endif diff --git a/resources/views/alert/warning.blade.php b/resources/views/alert/warning.blade.php new file mode 100644 index 0000000..0329004 --- /dev/null +++ b/resources/views/alert/warning.blade.php @@ -0,0 +1,22 @@ + + diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php new file mode 100644 index 0000000..212f1d4 --- /dev/null +++ b/resources/views/auth/login.blade.php @@ -0,0 +1,50 @@ + + + + Logo + +
+ @csrf +
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+
+
+ + +
+
+ +
+ +
+ +
+
+ +

+ I forgot my password +

+

+ Register a new membership +

+ +
+
diff --git a/resources/views/auth/password-reset.blade.php b/resources/views/auth/password-reset.blade.php new file mode 100644 index 0000000..4dfbb5c --- /dev/null +++ b/resources/views/auth/password-reset.blade.php @@ -0,0 +1,47 @@ + + + + {{-- cloudy4next --}} + +
+ + @csrf +
+ + + + +
+ {{-- --}} + + +
+ + + + +
+ {{-- --}} + +
+ + + + + + + +
+
+ +
+ +
+
diff --git a/resources/views/auth/register.blade.php b/resources/views/auth/register.blade.php new file mode 100644 index 0000000..3ee191d --- /dev/null +++ b/resources/views/auth/register.blade.php @@ -0,0 +1,58 @@ + + + + Logo + +
+

User Registration

+
+ +
+ @csrf +
+ +
+ +
+ + + +
+
+
+ +
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+ +
+ +
+
+ +
+
+
+
+ +
+ I already have a membership +
+ +
+
diff --git a/resources/views/components/field/id.blade.php b/resources/views/components/field/id.blade.php new file mode 100644 index 0000000..305c3a3 --- /dev/null +++ b/resources/views/components/field/id.blade.php @@ -0,0 +1,4 @@ +
+ + +
diff --git a/resources/views/components/footer/index.blade.php b/resources/views/components/footer/index.blade.php new file mode 100644 index 0000000..a2c933a --- /dev/null +++ b/resources/views/components/footer/index.blade.php @@ -0,0 +1,11 @@ +@php use Carbon\Carbon; @endphp + diff --git a/resources/views/components/layout/auth-card.blade.php b/resources/views/components/layout/auth-card.blade.php new file mode 100644 index 0000000..5e551d9 --- /dev/null +++ b/resources/views/components/layout/auth-card.blade.php @@ -0,0 +1,49 @@ +
+ @if(env('LOGIN_NOTICE')) +
+
+
+
+ @php + $loginNotice = env('LOGIN_NOTICE'); + if (str_contains($loginNotice, ':::')) { + $noticeParts = explode(':::', $loginNotice); + $noticeTitle = $noticeParts[0]; + $noticeMessage = $noticeParts[1]; + } else { + $noticeTitle = null; + $noticeMessage = $loginNotice; + } + @endphp +
+ @if($noticeTitle) + {!! $noticeTitle !!} + @else + Notice! + @endif +
+

{!! $noticeMessage !!}

+
+ +
+
+ @else +
+ @endif +
+
+
+
+ {{ $logo }} +
+
+ @if(config('aim-admin.show_inline_alert_box', true)) + + @endif + {{ $slot }} +
+
+
+
+
+
diff --git a/resources/views/components/layout/login.blade.php b/resources/views/components/layout/login.blade.php new file mode 100644 index 0000000..4dbd46e --- /dev/null +++ b/resources/views/components/layout/login.blade.php @@ -0,0 +1,50 @@ + + + + + + + + + {{ config('app.name', 'Laravel') }} + + + {{-- --}} + + + @vite('resources/js/app.js') + + + + +
+ {{ $slot }} +
+ +@php $toastTime = config('aim-admin.flash-timer', 2000); @endphp +@if (session('success')) + +@elseif((session('info'))) + +@elseif((session('warning'))) + +@elseif((session('error'))) + @if(config('aim-admin.show_toast_error', true)) + + @endif +@endif +@if(config('aim-admin.show_toast_error', true)) + @if($errors->any()) + @foreach ($errors->all() as $error) + + @endforeach + @endif +@endif +@stack('scripts') + + + diff --git a/resources/views/components/layout/main.blade.php b/resources/views/components/layout/main.blade.php new file mode 100644 index 0000000..90ce192 --- /dev/null +++ b/resources/views/components/layout/main.blade.php @@ -0,0 +1,102 @@ + + + + + + @if(isset($title)) + {{ env('APP_NAME').':: '.$title ?? 'AimAdmin Platform' }} + @else + {{env('APP_NAME')}} + @endif + + + @vite('resources/js/app.js') + @stack('styles') + @isset($injectedTop) + {!! $injectedTop !!} + @endisset + + + +
+ + + + + + + + + +
+ +
+
+
+
+ @isset($pageHeader) + {{$pageHeader}} + @endisset +
+
+ @isset($breadcrumb) + {{$breadcrumb}} + @endisset +
+
+
+
+ + + {{ $slot }} + + + @if(config('aim-admin.back_to_top', true)) + + + + @endif + +
+ + + + + + + + + + + + +
+ +@php $toastTime = config('aim-admin.flash-timer', 2000); @endphp +@if (session('success')) + +@elseif((session('info'))) + +@elseif((session('warning'))) + +@elseif((session('error'))) + @if(config('aim-admin.show_toast_error', true)) + + @endif +@endif +@if(config('aim-admin.show_toast_error', true)) + @if($errors->any()) + @foreach ($errors->all() as $error) + + @endforeach + @endif +@endif +@stack('scripts') +@isset($injectedBottom) + {!! $injectedBottom !!} +@endisset + + diff --git a/resources/views/components/mail/otp.blade.php b/resources/views/components/mail/otp.blade.php new file mode 100644 index 0000000..dcb8904 --- /dev/null +++ b/resources/views/components/mail/otp.blade.php @@ -0,0 +1,14 @@ +@component('mail::message') + # Reset Password + + You are receiving this email because we received a password reset request for your account. + + Your OTP : {{$otp}} + + This OTP will expire in {{ config('auth.passwords.'.config('auth.defaults.passwords').'.expire') }} minutes. + + If you did not request a password reset, no further action is required. + + Thanks,
+ {{ config('app.name') }} +@endcomponent diff --git a/resources/views/components/message/index.blade.php b/resources/views/components/message/index.blade.php new file mode 100644 index 0000000..af1dc58 --- /dev/null +++ b/resources/views/components/message/index.blade.php @@ -0,0 +1,57 @@ + diff --git a/resources/views/components/navbar/index.blade.php b/resources/views/components/navbar/index.blade.php new file mode 100644 index 0000000..6b875c8 --- /dev/null +++ b/resources/views/components/navbar/index.blade.php @@ -0,0 +1,59 @@ + + + diff --git a/resources/views/components/navbar/user-menu.blade.php b/resources/views/components/navbar/user-menu.blade.php new file mode 100644 index 0000000..f009d54 --- /dev/null +++ b/resources/views/components/navbar/user-menu.blade.php @@ -0,0 +1,25 @@ +@php + $name = Illuminate\Support\Facades\Auth::user()->name; +@endphp + diff --git a/resources/views/components/notification/index.blade.php b/resources/views/components/notification/index.blade.php new file mode 100644 index 0000000..b4c6c3c --- /dev/null +++ b/resources/views/components/notification/index.blade.php @@ -0,0 +1,42 @@ +@php + $notificationData = new stdClass(); + // Ensure $notificationData has 'notifications' as an array to avoid errors in the @forelse directive + $notificationData->notifications = $notificationData->notifications ?? []; +@endphp + + + diff --git a/resources/views/components/right-sidebar/index.blade.php b/resources/views/components/right-sidebar/index.blade.php new file mode 100644 index 0000000..838da54 --- /dev/null +++ b/resources/views/components/right-sidebar/index.blade.php @@ -0,0 +1,6 @@ + diff --git a/resources/views/components/sidebar/header.blade.php b/resources/views/components/sidebar/header.blade.php new file mode 100644 index 0000000..cf641d3 --- /dev/null +++ b/resources/views/components/sidebar/header.blade.php @@ -0,0 +1,5 @@ + diff --git a/resources/views/components/sidebar/index.blade.php b/resources/views/components/sidebar/index.blade.php new file mode 100644 index 0000000..704e576 --- /dev/null +++ b/resources/views/components/sidebar/index.blade.php @@ -0,0 +1,30 @@ +@php + use CodeCoz\AimAdmin\MenuBuilder\AimAdminMenu; + $applicationName = env('APP_NAME', 'Aim Admin'); + $menus = app(AimAdminMenu::class)->menu('sidebar'); +@endphp + + diff --git a/resources/views/components/sidebar/item.blade.php b/resources/views/components/sidebar/item.blade.php new file mode 100644 index 0000000..f6817ed --- /dev/null +++ b/resources/views/components/sidebar/item.blade.php @@ -0,0 +1,20 @@ +@props(['item']) + +@inject('sidebarItemHelper', 'CodeCoz\AimAdmin\MenuBuilder\SidebarItemHelper') + +@if ($sidebarItemHelper->isHeader($item)) + + {{-- Header --}} + + +@elseif ($sidebarItemHelper->isSubmenu($item)) + + {{-- Treeview menu --}} + + +@elseif ($sidebarItemHelper->isLink($item)) + + {{-- Link --}} + + +@endif diff --git a/resources/views/components/sidebar/link.blade.php b/resources/views/components/sidebar/link.blade.php new file mode 100644 index 0000000..6809480 --- /dev/null +++ b/resources/views/components/sidebar/link.blade.php @@ -0,0 +1,20 @@ +@props(['item']) + diff --git a/resources/views/components/sidebar/treeview.blade.php b/resources/views/components/sidebar/treeview.blade.php new file mode 100644 index 0000000..fd2fc2e --- /dev/null +++ b/resources/views/components/sidebar/treeview.blade.php @@ -0,0 +1,33 @@ +@props(['item']) + + diff --git a/resources/views/components/utils/error.blade.php b/resources/views/components/utils/error.blade.php new file mode 100644 index 0000000..a0cb24c --- /dev/null +++ b/resources/views/components/utils/error.blade.php @@ -0,0 +1,24 @@ +@props(['messages','type' => 'danger']) +@if(session('error')) + @php + $messages = session('error'); + @endphp +@elseif ($errors->any() && $errors->get('error')) + @php + $messages = $errors->get('error'); + @endphp +@endif +@if($messages) + @php + $messages = is_array($messages) ? $messages : [$messages]; + @endphp +
+ +
Alert!
+ @foreach ($messages as $message) + @if(is_string($message)) + {{ $message }}
+ @endif + @endforeach +
+@endif diff --git a/resources/views/components/utils/input-error.blade.php b/resources/views/components/utils/input-error.blade.php new file mode 100644 index 0000000..98fa586 --- /dev/null +++ b/resources/views/components/utils/input-error.blade.php @@ -0,0 +1,6 @@ +@props(['messages']) +@if ($messages) + @foreach ((array) $messages as $message) + {{ $message }}
+ @endforeach +@endif diff --git a/resources/views/components/utils/input-errors.blade.php b/resources/views/components/utils/input-errors.blade.php new file mode 100644 index 0000000..8737884 --- /dev/null +++ b/resources/views/components/utils/input-errors.blade.php @@ -0,0 +1,3 @@ +
+ +
\ No newline at end of file diff --git a/resources/views/components/utils/input-label.blade.php b/resources/views/components/utils/input-label.blade.php new file mode 100644 index 0000000..bfd2cbb --- /dev/null +++ b/resources/views/components/utils/input-label.blade.php @@ -0,0 +1,5 @@ +@props(['value']) + + diff --git a/resources/views/components/utils/input.blade.php b/resources/views/components/utils/input.blade.php new file mode 100644 index 0000000..ea370d4 --- /dev/null +++ b/resources/views/components/utils/input.blade.php @@ -0,0 +1,3 @@ +@props(['disabled' => false]) + +merge(['class' => 'form-control']) !!}> diff --git a/resources/views/components/utils/modal.blade.php b/resources/views/components/utils/modal.blade.php new file mode 100644 index 0000000..c3d4e01 --- /dev/null +++ b/resources/views/components/utils/modal.blade.php @@ -0,0 +1,25 @@ + + diff --git a/resources/views/components/utils/primary-button.blade.php b/resources/views/components/utils/primary-button.blade.php new file mode 100644 index 0000000..c492478 --- /dev/null +++ b/resources/views/components/utils/primary-button.blade.php @@ -0,0 +1,3 @@ + diff --git a/resources/views/components/utils/title.blade.php b/resources/views/components/utils/title.blade.php new file mode 100644 index 0000000..00060bb --- /dev/null +++ b/resources/views/components/utils/title.blade.php @@ -0,0 +1,3 @@ +
+

{{$slot}}

+
diff --git a/resources/views/create.blade.php b/resources/views/create.blade.php new file mode 100644 index 0000000..7667eeb --- /dev/null +++ b/resources/views/create.blade.php @@ -0,0 +1,6 @@ + + + {{ $pageTitle ?? 'Create Item' }} + + + diff --git a/resources/views/crudboard/actions/board-action.blade.php b/resources/views/crudboard/actions/board-action.blade.php new file mode 100644 index 0000000..162d390 --- /dev/null +++ b/resources/views/crudboard/actions/board-action.blade.php @@ -0,0 +1,8 @@ +@php $routeParams = $crudAction->getRouteParameters(); @endphp +@if($crudAction->shouldBeDisplayedFor(null)) + @if($crudAction->isButton()) + + @else + + @endif +@endif diff --git a/resources/views/crudboard/actions/board-button.blade.php b/resources/views/crudboard/actions/board-button.blade.php new file mode 100644 index 0000000..5493033 --- /dev/null +++ b/resources/views/crudboard/actions/board-button.blade.php @@ -0,0 +1,7 @@ + + @if ($crudAction->getIcon()) + + @endif + {{ $crudAction->getLabel() }} + \ No newline at end of file diff --git a/resources/views/crudboard/actions/btn.blade.php b/resources/views/crudboard/actions/btn.blade.php new file mode 100644 index 0000000..3645a05 --- /dev/null +++ b/resources/views/crudboard/actions/btn.blade.php @@ -0,0 +1,6 @@ + \ No newline at end of file diff --git a/resources/views/crudboard/actions/button.blade.php b/resources/views/crudboard/actions/button.blade.php new file mode 100644 index 0000000..fbe82a1 --- /dev/null +++ b/resources/views/crudboard/actions/button.blade.php @@ -0,0 +1,5 @@ +@if($action->isSubmitAction()) + +@else + {{$action->getLabel() }} +@endif \ No newline at end of file diff --git a/resources/views/crudboard/actions/filter-action.blade.php b/resources/views/crudboard/actions/filter-action.blade.php new file mode 100644 index 0000000..fc83fd3 --- /dev/null +++ b/resources/views/crudboard/actions/filter-action.blade.php @@ -0,0 +1,6 @@ +@php $routeParams = $action->getRouteParameters(); @endphp +@if($action->isButton()) + +@else + +@endif diff --git a/resources/views/crudboard/actions/form-action.blade.php b/resources/views/crudboard/actions/form-action.blade.php new file mode 100644 index 0000000..fc83fd3 --- /dev/null +++ b/resources/views/crudboard/actions/form-action.blade.php @@ -0,0 +1,6 @@ +@php $routeParams = $action->getRouteParameters(); @endphp +@if($action->isButton()) + +@else + +@endif diff --git a/resources/views/crudboard/actions/grid-button.blade.php b/resources/views/crudboard/actions/grid-button.blade.php new file mode 100644 index 0000000..d2246a5 --- /dev/null +++ b/resources/views/crudboard/actions/grid-button.blade.php @@ -0,0 +1,7 @@ +
+ @if($rowAction->isButton()) + + @else + + @endif +
diff --git a/resources/views/crudboard/actions/grid-delete-button.blade.php b/resources/views/crudboard/actions/grid-delete-button.blade.php new file mode 100644 index 0000000..06e28f9 --- /dev/null +++ b/resources/views/crudboard/actions/grid-delete-button.blade.php @@ -0,0 +1,15 @@ +
+
+ + @csrf +
+ +
diff --git a/resources/views/crudboard/actions/group.blade.php b/resources/views/crudboard/actions/group.blade.php new file mode 100644 index 0000000..f608aba --- /dev/null +++ b/resources/views/crudboard/actions/group.blade.php @@ -0,0 +1,14 @@ +
+ + + +
+ + +
+
\ No newline at end of file diff --git a/resources/views/crudboard/actions/link.blade.php b/resources/views/crudboard/actions/link.blade.php new file mode 100644 index 0000000..8ea2e18 --- /dev/null +++ b/resources/views/crudboard/actions/link.blade.php @@ -0,0 +1,16 @@ +getRouteName()) + href="{{ route($action->getRouteName(), $routeParams) }}" + @elseif($action->getUrl()) + href="{{$action->getUrl()}}" + @else + href="#" + @endif + {!! $htmlAttributes !!}> + @if ($action->getIcon()) + + @endif + {{ $action->getLabel() }} + + + diff --git a/resources/views/crudboard/actions/show-button.blade.php b/resources/views/crudboard/actions/show-button.blade.php new file mode 100644 index 0000000..0f1a75b --- /dev/null +++ b/resources/views/crudboard/actions/show-button.blade.php @@ -0,0 +1,9 @@ +@php + $routeParams = $action->getRouteParameters(); + $htmlAttributes = $action->getAttributesAsHtml(); +@endphp +@if($action->isButton()) + +@else + +@endif diff --git a/resources/views/crudboard/fields/chainselect.blade.php b/resources/views/crudboard/fields/chainselect.blade.php new file mode 100644 index 0000000..93604d4 --- /dev/null +++ b/resources/views/crudboard/fields/chainselect.blade.php @@ -0,0 +1,47 @@ +
+ @foreach ( $field->getCustomOption('children') as $key=>$child) + @php + if($dependant = $child->getCustomOption('dependant')){ + $child->setHtmlAttribute('onchange',"loadDependant(this,'$dependant')"); + } + $htmlAttributes = $child->getAttributesAsHtml() ; + @endphp +
+ +
+ @endforeach +
+ +@if($field->getHelp()) + {!! $field->getHelp() !!} +@endif + + + +@pushOnce('scripts') + +@endPushOnce diff --git a/resources/views/crudboard/fields/checkbox.blade.php b/resources/views/crudboard/fields/checkbox.blade.php new file mode 100644 index 0000000..a90c414 --- /dev/null +++ b/resources/views/crudboard/fields/checkbox.blade.php @@ -0,0 +1,24 @@ + +
+
+ isDisabled()) disabled @endif @if($field->isReadonly()) readonly + @endif @if($field->isRequired()) required @endif + /> + +
+
+
+ +@if($field->getHelp()) + {!! $field->getHelp() !!} +@endif + + + + diff --git a/resources/views/crudboard/fields/choice.blade.php b/resources/views/crudboard/fields/choice.blade.php new file mode 100644 index 0000000..d7bc5ed --- /dev/null +++ b/resources/views/crudboard/fields/choice.blade.php @@ -0,0 +1,67 @@ + +@switch($field->getInputType()) + @case("radio") + @case("checkbox") +
+ @foreach ($field->getCustomOption('choiceList') as $key=>$value) + @php $htmlAttributes = $field->getAttributesAsHtml(); @endphp +
+ getName(),$field->getValue())) + @php + $values = is_array($values) ? $values : [$values]; + @endphp + @if(in_array($key,$values)) + checked + @endif + @elseif($field->getCustomOption(CodeCoz\AimAdmin\Field\ChoiceField::SELECTED.".".$key) !==null ) + checked + @endif + @disabled($field->isDisabled()) + @readonly($field->isReadonly()) + > + +
+ @endforeach +
+ @break + @default + +@endswitch +@if($field->getHelp()) + {!! $field->getHelp() !!} +@endif + + + diff --git a/resources/views/crudboard/fields/datetime.blade.php b/resources/views/crudboard/fields/datetime.blade.php new file mode 100644 index 0000000..6a01556 --- /dev/null +++ b/resources/views/crudboard/fields/datetime.blade.php @@ -0,0 +1,15 @@ + +getAttributesAsHtml() }} class="form-control {{ $field->getCssClass() }}" + id="{{ $field->getName() }}" name="{{ $field->getName() }}" + type="date" placeholder="{{ $field->getPlaceholder('placeholder')}}" + value="{{ old($field->getName(),$field->getFormattedValue()) }}" + @if($field->isDisabled()) disabled @endif @if($field->isREadonly()) readonly @endif +/> +@if($field->getHelp()) + {!! $field->getHelp() !!} +@endif + + + diff --git a/resources/views/crudboard/fields/file.blade.php b/resources/views/crudboard/fields/file.blade.php new file mode 100644 index 0000000..916e439 --- /dev/null +++ b/resources/views/crudboard/fields/file.blade.php @@ -0,0 +1,11 @@ + +isRequired()) {!! $htmlAttributes !!} @disabled($field->isDisabled()) @readonly($field->isReadonly()) /> +@if($field->getHelp()) + {!! $field->getHelp() !!} +@endif + + diff --git a/resources/views/crudboard/fields/form-element.blade.php b/resources/views/crudboard/fields/form-element.blade.php new file mode 100644 index 0000000..a7735e7 --- /dev/null +++ b/resources/views/crudboard/fields/form-element.blade.php @@ -0,0 +1,11 @@ + +@php + $attributes = ""; +@endphp + +@switch($field->getHtmlElement()) + @case("input") + + @break; + diff --git a/resources/views/crudboard/fields/grid_cell.blade.php b/resources/views/crudboard/fields/grid_cell.blade.php new file mode 100644 index 0000000..aa91d0f --- /dev/null +++ b/resources/views/crudboard/fields/grid_cell.blade.php @@ -0,0 +1 @@ +{!! $value !!} \ No newline at end of file diff --git a/resources/views/crudboard/fields/group.blade.php b/resources/views/crudboard/fields/group.blade.php new file mode 100644 index 0000000..d4021a2 --- /dev/null +++ b/resources/views/crudboard/fields/group.blade.php @@ -0,0 +1,19 @@ + +
+ @foreach ( $field->getCustomOption('fields') as $key=>$childField) + @php + $htmlAttributes = $childField->getAttributesAsHtml() ; + @endphp +
+ +
+ @endforeach +
+@if($field->getHelp()) + {!! $field->getHelp() !!} +@endif + + + diff --git a/resources/views/crudboard/fields/hidden.blade.php b/resources/views/crudboard/fields/hidden.blade.php new file mode 100644 index 0000000..fb01bd6 --- /dev/null +++ b/resources/views/crudboard/fields/hidden.blade.php @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/resources/views/crudboard/fields/id.blade.php b/resources/views/crudboard/fields/id.blade.php new file mode 100644 index 0000000..143f035 --- /dev/null +++ b/resources/views/crudboard/fields/id.blade.php @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/views/crudboard/fields/input.blade.php b/resources/views/crudboard/fields/input.blade.php new file mode 100644 index 0000000..6b4cbb4 --- /dev/null +++ b/resources/views/crudboard/fields/input.blade.php @@ -0,0 +1,15 @@ + +isDisabled()) disabled @endif @if($field->isReadonly()) readonly + @endif @if($field->isRequired()) required @endif +/> +@if($field->getHelp()) + {!! $field->getHelp() !!} +@endif + + + diff --git a/resources/views/crudboard/fields/textarea.blade.php b/resources/views/crudboard/fields/textarea.blade.php new file mode 100644 index 0000000..d9abd4f --- /dev/null +++ b/resources/views/crudboard/fields/textarea.blade.php @@ -0,0 +1,12 @@ + + +@if($field->getHelp()) + {!! $field->getHelp() !!} +@endif + + diff --git a/resources/views/crudboard/filter.blade.php b/resources/views/crudboard/filter.blade.php new file mode 100644 index 0000000..052e773 --- /dev/null +++ b/resources/views/crudboard/filter.blade.php @@ -0,0 +1,48 @@ + + +@push('scripts') + +@endpush diff --git a/resources/views/crudboard/form.blade.php b/resources/views/crudboard/form.blade.php new file mode 100644 index 0000000..d3d16bf --- /dev/null +++ b/resources/views/crudboard/form.blade.php @@ -0,0 +1,51 @@ + +
+
+
+
+ @if($form->getTitle() ) + {!! $form->getTitle() !!} + @else + {{ $attributes['title'] }} + @endif +
+
+
+
+ + @if(config('aim-admin.show_inline_alert_box', true)) + + @endif + +
getAttributesAsHtml() }} + @if($form->getFields()->hasFileInput()) + enctype="multipart/form-data" + @endif + > +
+ @foreach($form->getFields() as $field) + @php $htmlAttributes = $field->getAttributesAsHtml() ; @endphp + @if($field->isHiddenInput()) + + @else +
+ +
+ @endif + @endforeach + @csrf +
+ +
+ @foreach($form->getActions()->getFormActions() as $action) + @if($action->shouldBeDisplayedFor(null)) + @php $htmlActionAttributes = $action->getAttributesAsHtml() ; @endphp + + @endif + @endforeach +
+
+
+
+
diff --git a/resources/views/crudboard/grid.blade.php b/resources/views/crudboard/grid.blade.php new file mode 100644 index 0000000..b8b1b03 --- /dev/null +++ b/resources/views/crudboard/grid.blade.php @@ -0,0 +1,115 @@ +
+ @if($grid->getFilter()->getFields()->count() > 0) + + @endif +
+
+

+ @if($grid->getTitle()) + {!! $grid->getTitle() !!} + @else + {!! $attributes['title'] !!} + @endif +

+
+
+ @foreach ($grid->getActions() as $crudAction) + @php + $htmlAttributes = $crudAction->getAttributesAsHtml(); + @endphp + + @endforeach +
+
+
+
+ @if(config('aim-admin.show_inline_alert_box', true)) + + @endif +
+ + + + @if(!$grid->isDisableSerialColumn()) + + @endif + @foreach ($grid->getColumns() as $column) + + @endforeach + @if ($grid->getRowActions()->count()) + + @endif + + + + @php + $gridData = $grid->getGridData(); + $perPage = $gridData->perPage()??10; + $currentPage = $gridData->currentPage(); + $indexCount = $perPage*($currentPage-1); + @endphp + @forelse($gridData as $k=>$row) + @php + $rowCssClass = $grid->getRowCssClass($row); + $rowCss = $grid->getRowCss($row); + @endphp + + @if(!$grid->isDisableSerialColumn()) + + @endif + @foreach ($grid->getColumns() as $column) + + @endforeach + @if($grid->getRowActions()->count()) + + @endif + + + @empty + + + + @endforelse + + +
#getAttributesAsHtml() !!} >{!! $column->getLabel() !!}{{$grid->getActionLevel() }}
{{ $indexCount + $k + 1 }} + @php + $value = $row[$column->getName()] ; + if($formaterFunc = $column->getFormatValueCallable()){ + $value = $formaterFunc($value,$row); + } + @endphp + + + @foreach ($grid->getRowActions() as $rowAction) + @php + $routeParams = $rowAction->getRouteParameters(); + if ($routeParams instanceof \Closure) { + $routeParams = $routeParams($row); + } else { + foreach ($routeParams as $key => $val) { + $routeParams[$key] = $row[$val]; + } + empty($routeParams) && ($routeParams['id'] = $row['id']); + } + $htmlAttributes = $rowAction->getAttributesAsHtml(); + @endphp + @if($rowAction->shouldBeDisplayedFor($row)) + + @endif + @endforeach +
getRowActions()->count()) + colspan="{{ $grid->getColumns()->count() + 2 }}" + @else + colspan="{{ $grid->getColumns()->count() + 1 }}" + @endif + class="text-center">No record is + found +
+ +
+
+
+
diff --git a/resources/views/crudboard/pagination.blade.php b/resources/views/crudboard/pagination.blade.php new file mode 100644 index 0000000..ff5c4c3 --- /dev/null +++ b/resources/views/crudboard/pagination.blade.php @@ -0,0 +1,12 @@ +
+
+
+ {{ $data->links() }} +
+
+
+
+ Showing {{ $data->firstItem() }} to {{ $data->lastItem() }} of {{ $data->total() }} entries +
+
+
diff --git a/resources/views/crudboard/show.blade.php b/resources/views/crudboard/show.blade.php new file mode 100644 index 0000000..d9feb1a --- /dev/null +++ b/resources/views/crudboard/show.blade.php @@ -0,0 +1,42 @@ + +
+
+
+
+ @if($show->getTitle()) + {{ $show->getTitle() }} + @else + {{ $attributes['title']}} + @endif +
+
+
+ + + @foreach($show->getFields() as $field) + + + + + @endforeach + +
{{$field->getLabel()}} + @if($component = $field->getComponent()) + @php + $value = $field->getValue() ; + $record = $show->getRecord(); + @endphp + + @else + {{$field->getValue()}} + @endif +
+ @foreach($show->getActions()->getShowActions() as $action) + @if($action->shouldBeDisplayedFor($show->getRecord())) + @php $htmlActionAttributes = $action->getAttributesAsHtml() ; @endphp + + @endif + @endforeach +
+
+
diff --git a/resources/views/crudboard/toast.blade.php b/resources/views/crudboard/toast.blade.php new file mode 100644 index 0000000..b9ee30a --- /dev/null +++ b/resources/views/crudboard/toast.blade.php @@ -0,0 +1,38 @@ + + +{{-- Toast.fire({ + icon: 'success', + title: 'Success' + }) + Toast.fire({ + icon: 'error', + title: 'Error' + }) + Toast.fire({ + icon: 'warning', + title: 'Warning' + }) + Toast.fire({ + icon: 'info', + title: 'Info' + }) + Toast.fire({ + icon: 'question', + title: 'Question' + }) --}} diff --git a/resources/views/edit.blade.php b/resources/views/edit.blade.php new file mode 100644 index 0000000..0fd46b0 --- /dev/null +++ b/resources/views/edit.blade.php @@ -0,0 +1,7 @@ + + + {{ $pageTitle ?? 'Edit Item' }} + + + + diff --git a/resources/views/list.blade.php b/resources/views/list.blade.php new file mode 100644 index 0000000..18eca78 --- /dev/null +++ b/resources/views/list.blade.php @@ -0,0 +1,7 @@ + + + {{ $pageTitle ?? 'Item List' }} + + + + diff --git a/resources/views/show.blade.php b/resources/views/show.blade.php new file mode 100644 index 0000000..c89dbd9 --- /dev/null +++ b/resources/views/show.blade.php @@ -0,0 +1,7 @@ + + + {{ $pageTitle ?? 'Item Show' }} + + + + diff --git a/routes/web.php b/routes/web.php new file mode 100644 index 0000000..d2c13e7 --- /dev/null +++ b/routes/web.php @@ -0,0 +1,19 @@ + config('aim-admin.middleware', ['guest', 'web'])], function () { + Route::get(config('aim-admin.auth.url', 'login'), [config('aim-admin.auth.controller', LoginController::class), 'login'])->name('login'); + Route::post(config('aim-admin.auth.url', 'login'), [config('aim-admin.auth.controller', LoginController::class), 'authenticate']); + + Route::get('forget-password', [LoginController::class, 'passwordReset'])->name('forget.password'); + Route::get('password/reset/{token}', [LoginController::class, 'passwordReset'])->name('password.reset'); + Route::post('password/reset', [LoginController::class, 'reset'])->name('password.reset.update'); + + Route::get('registration', [config('aim-admin.registration.controller', RegistrationController::class), 'registration'])->name('registration'); + Route::post('registration', [config('aim-admin.registration.controller', RegistrationController::class), 'register']); + Route::get(config('aim-admin.auth.logout_url', 'logout'), [config('aim-admin.auth.controller', LoginController::class), 'logout']) + ->name('logout'); +}); diff --git a/src/Admin.php b/src/Admin.php new file mode 100644 index 0000000..8d36f6c --- /dev/null +++ b/src/Admin.php @@ -0,0 +1,14 @@ +viewsPath = Admin::packagePath('resources/views'); + $this->configPath = Admin::packagePath('config/aim-admin.php'); + $this->routePath = Admin::packagePath('routes/web.php'); + $this->migrationPath = Admin::packagePath('database/migrations'); + + // Internal Interfaces Bindings. + $this->interfacesBindings(); + + // Register user service provider. +// $this->app->register(UserServiceProvider::class); + + // Register the Menu service provider. + $this->app->register(MenuServiceProvider::class); + + // Register the Repo service provider. + $this->app->register(RepositoryServiceProvider::class); + + // Register View Service Provider for inject Process + $this->app->register(ViewServiceProvider::class); + + // cd pa$this->callAfterResolving('blade.compiler', fn(BladeCompiler $bladeCompiler) => $this->registerBladeExtensions($bladeCompiler)); + + } + + protected function registerBladeExtensions($bladeCompiler): void + { + $bladeMethodWrapper = '\\CodeCoz\\AimAdmin\\AdminServiceProvider::bladeMethodWrapper'; + + $bladeCompiler->directive('hasanypermission', fn($args) => ""); + $bladeCompiler->directive('elsehasanypermission', fn($args) => ""); + $bladeCompiler->directive('endhasanypermission', fn() => ''); + + $bladeCompiler->directive('hasrole', fn($args) => ""); + $bladeCompiler->directive('endhasrole', fn() => ''); + + $bladeCompiler->directive('hasanyrole', fn($args) => ""); + $bladeCompiler->directive('endhasanyrole', fn() => ''); + + } + + public static function bladeMethodWrapper($method, $role, $guard = null): bool + { + return true; + } + + /** + * Interfaces bindings + * + * @return void + */ + private function interfacesBindings(): void + { + // Register the service the package provides. + $this->app->singleton($this->packagePrefix, function ($app) { + return new Admin; + }); + + // Register the extra Interfaces to the package provides. + $this->app->singleton(CrudBoardInterface::class, CrudBoard::class); + + } + + /** + * Perform post-registration booting of services. + * + * @param Factory $view + * @return void + */ + public function boot(Factory $view): void + { + $this->loadConfig(); + $this->loadViews(); + $this->loadRoutes(); + $this->loadMigration(); + $this->loadBladeComponentNamespace(); + $this->registerCommands(); + $this->registerGlobalMiddleware(); + Paginator::useBootstrapFour(); + + $this->publishMigrations(); + + } + + /** + * Load the Blade Component Namespace. + * + * @return void + */ + private function loadBladeComponentNamespace(): void + { + Blade::componentNamespace('CodeCoz\\AimAdmin\View\\Components', $this->packagePrefix); + Blade::anonymousComponentPath($this->viewsPath, $this->packagePrefix); + } + + + /** + * Load the package views. + * + * @return void + */ + private function loadViews(): void + { + // Use the default package views path + $this->loadViewsFrom($this->viewsPath, $this->packagePrefix); + } + + /** + * Publish the migrations. + * + * @return void + */ + private function publishMigrations(): void + { + + $this->publishes([$this->migrationPath => database_path('migrations'), + ], 'migrations'); + + } + + /** + * Load the package config. + * + * @return void + */ + private function loadConfig(): void + { + $this->mergeConfigFrom($this->configPath, $this->packagePrefix); + } + + /** + * Load the package routes. + * @return void + */ + private function loadRoutes(): void + { + $this->loadRoutesFrom($this->routePath); + } + + /** + * Load the package migration. + * @return void + */ + private function loadMigration(): void + { + $this->loadMigrationsFrom($this->migrationPath); + } + + + /** + * Register the package's Global Middleware. + * + * @return void + */ + private function registerGlobalMiddleware(): void + { +// $this->app['router']->aliasMiddleware('role', RoleMiddleware::class); +// $this->app['router']->aliasMiddleware('acl', CheckPermission::class); +// $this->app['router']->pushMiddlewareToGroup('web', ForcePasswordChange::class); + } + + /** + * Register the package's artisan commands. + * + * @return void + */ + private function registerCommands(): void + { + $this->commands([ + InstallAimAdminCommand::class, + UpgradeAimAdminCommand::class, + MakeAimController::class, + MakeAimRepositoryInterface::class, + MakeAimServiceInterface::class, + MakeAimRepository::class, + MakeAimService::class, + AimAdminCommand::class, + AddToRouteCommand::class, + ]); + } + + + /** + * Get the services provided by the provider. + * + * @return array + */ + public function provides() + { + return [$this->packagePrefix]; + } + +} diff --git a/src/Collection/AbstractCollection.php b/src/Collection/AbstractCollection.php new file mode 100644 index 0000000..6ec71c8 --- /dev/null +++ b/src/Collection/AbstractCollection.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace CodeCoz\AimAdmin\Collection; + + +/** + * This class is a collection abstract class abstract. + * + * @author CodeCoz + */ +abstract class AbstractCollection implements \ArrayAccess, \Countable, \IteratorAggregate +{ + protected array $items; + + abstract function offsetGet(mixed $offset): mixed; + + public function isEmpty(): bool + { + return 0 === \count($this->items); + } + + public function offsetExists(mixed $offset): bool + { + return \array_key_exists($offset, $this->items); + } + + + public function offsetSet(mixed $offset, mixed $value): void + { + $this->items[$offset] = $value; + } + + public function offsetUnset(mixed $offset): void + { + unset($this->items[$offset]); + } + + public function getIterator(): \ArrayIterator + { + return new \ArrayIterator($this->items); + } + + public function count(): int + { + return \count($this->items); + } + + public function all(): array + { + return $this->items; + } + +} diff --git a/src/Collection/ActionCollection.php b/src/Collection/ActionCollection.php new file mode 100644 index 0000000..afff8f0 --- /dev/null +++ b/src/Collection/ActionCollection.php @@ -0,0 +1,108 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace CodeCoz\AimAdmin\Collection; + +use CodeCoz\AimAdmin\Dto\CrudBoard\ActionDto; + + +/** + * This class is for action collection in crudboard . + * + * @author CodeCoz + */ +final class ActionCollection extends AbstractCollection +{ + + private function __construct(array $actions) + { + $this->items = $actions; + } + + public function __clone() + { + foreach ($this->items as $actionName => $actionDto) { + $this->items[$actionName] = clone $actionDto; + } + } + + /** + * @param ActionDto[] $actions + */ + public static function init(array $actions): self + { + return new self($actions); + } + + public function get(string $actionName): ?ActionDto + { + return $this->items[$actionName] ?? null; + } + + public function offsetExists(mixed $offset): bool + { + return \array_key_exists($offset, $this->items); + } + + public function offsetGet(mixed $offset): ActionDto + { + return $this->items[$offset]; + } + + public function getRowActions(): self + { + return self::init(array_filter( + $this->items, + static fn(ActionDto $action): bool => $action->isRowAction() + )); + } + + public function getCrudBoardActions(): self + { + return self::init(array_filter( + $this->items, + static fn(ActionDto $action): bool => $action->isCrudBoardAction() + )); + } + + public function getBatchActions(): self + { + return self::init(array_filter( + $this->items, + static fn(ActionDto $action): bool => $action->isBatchAction() + )); + } + + public function getFormActions(): self + { + return self::init(array_filter( + $this->items, + static fn(ActionDto $action): bool => $action->isFormAction() + )); + } + + public function getShowActions(): self + { + return self::init(array_filter( + $this->items, + static fn(ActionDto $action): bool => $action->isShowAction() + )); + } + + public function getFilterActions(): static + { + return self::init(array_filter( + $this->items, + static fn(ActionDto $action): bool => $action->isFilterAction() + )); + } + +} diff --git a/src/Collection/FieldCollection.php b/src/Collection/FieldCollection.php new file mode 100644 index 0000000..ddf8ffd --- /dev/null +++ b/src/Collection/FieldCollection.php @@ -0,0 +1,94 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace CodeCoz\AimAdmin\Collection; + +use CodeCoz\AimAdmin\Dto\CrudBoard\FieldDto; +use CodeCoz\AimAdmin\Field\Field; + +/** + * This class is for field creation in crudboard . + * + * @author CodeCoz + */ +final class FieldCollection extends AbstractCollection +{ + + private function __construct(iterable $fields) + { + $this->items = $this->processFields($fields); + } + + public static function init(iterable $fields): self + { + return new self($fields); + } + + public function offsetGet(mixed $offset): FieldDto + { + return $this->items[$offset]; + } + + private function processFields(iterable $fields): array + { + $dtos = []; + foreach ($fields as $field) { + if (\is_string($field)) { + $field = Field::init($field, $field); + } + + $dto = $field->getDto(); + $dtos[$dto->getName()] = $dto; + } + + return $dtos; + } + + public function prepend(FieldDto $field): void + { + $this->items = \array_unshift($this->items, $field); + } + + public function first(): ?FieldDto + { + if (0 === \count($this->items)) { + return null; + } + + return $this->items[array_key_first($this->items)]; + } + + public function get(string $name): ?FieldDto + { + return $this->items[$name] ?? null; + } + + public function set(FieldDto $field): void + { + $this->items[$field->getName()] = $field; + } + + public function unset(FieldDto $field): void + { + unset($this->items[$field->getName()]); + } + + public function add($field) + { + if (\is_string($field)) { + $field = Field::init($field, $field); + } + $dto = $field->getDto(); + $this->set($dto); + } + + +} diff --git a/src/Collection/FormFieldCollection.php b/src/Collection/FormFieldCollection.php new file mode 100644 index 0000000..acf22ec --- /dev/null +++ b/src/Collection/FormFieldCollection.php @@ -0,0 +1,104 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace CodeCoz\AimAdmin\Collection; + +use CodeCoz\AimAdmin\Dto\CrudBoard\FormFieldDto; +use CodeCoz\AimAdmin\Field\Field; + +/** + * This class is for field creation in crudboard . + * + * @author CodeCoz + */ +final class FormFieldCollection extends AbstractCollection +{ + + private function __construct(iterable $fields) + { + $this->items = $this->processFields($fields); + } + + public static function init(iterable $fields): self + { + return new self($fields); + } + + public function offsetGet(mixed $offset): FormFieldDto + { + return $this->items[$offset]; + } + + private function processFields(iterable $fields): array + { + $dtos = []; + foreach ($fields as $field) { + if (\is_string($field)) { + $field = Field::init($field, $field); + } + + $dto = $field->getDto(); + $dtos[$dto->getName()] = $dto; + } + + return $dtos; + } + + public function prepend(FormFieldDto $field): void + { + $this->items = \array_unshift($this->items, $field); + } + + public function first(): ?FormFieldDto + { + if (0 === \count($this->items)) { + return null; + } + + return $this->items[array_key_first($this->items)]; + } + + public function get(string $name): ?FormFieldDto + { + return $this->items[$name] ?? null; + } + + public function set(FormFieldDto $field): void + { + $this->items[$field->getName()] = $field; + } + + public function unset(FormFieldDto $field): void + { + unset($this->items[$field->getName()]); + } + + public function add($field) + { + if (\is_string($field)) { + $field = Field::init($field, $field); + } + $dto = $field->getDto(); + $this->set($dto); + } + + public function hasFileInput(): bool + { + $result = false; + foreach ($this->items as $FieldDto) { + if ($result = $FieldDto->isFileInput()) { + break; + } + } + return $result; + } + +} diff --git a/src/Console/Commands/AddToRouteCommand.php b/src/Console/Commands/AddToRouteCommand.php new file mode 100644 index 0000000..54ec102 --- /dev/null +++ b/src/Console/Commands/AddToRouteCommand.php @@ -0,0 +1,56 @@ +argument('name')); + $controllerName = ucfirst($name) . 'Controller'; + + $routes = << '{$name}', 'middleware' => ['web', 'auth']], function () { + Route::get('/', [{$controllerName}::class, 'list'])->name('{$name}.list'); + Route::get('/show/{id}', [{$controllerName}::class, 'show'])->name('{$name}.show'); + Route::get('/edit/{id}', [{$controllerName}::class, 'edit'])->name('{$name}.edit'); + Route::post('/update', [{$controllerName}::class, 'update'])->name('{$name}.update'); + Route::post('/delete/{id}', [{$controllerName}::class, 'delete'])->name('{$name}.delete'); + Route::get('/create', [{$controllerName}::class, 'create'])->name('{$name}.create'); + Route::post('/create', [{$controllerName}::class, 'store'])->name('{$name}.store'); + }); + EOT; + $routeFile = base_path('routes/web.php'); + $currentRoutes = File::get($routeFile); + if (!str_contains($currentRoutes, $routes)) { + if (File::append($routeFile, "\n" . $routes)) { + $this->info(ucfirst($name) . ' routes have been added successfully.'); + } else { + $this->error('Failed to add ' . $name . ' routes.'); + } + } else { + $this->info('The routes for ' . ucfirst($name) . ' already exist in the routes file.'); + } + } + +} diff --git a/src/Console/Commands/AimAdminCommand.php b/src/Console/Commands/AimAdminCommand.php new file mode 100644 index 0000000..dc8e6a2 --- /dev/null +++ b/src/Console/Commands/AimAdminCommand.php @@ -0,0 +1,126 @@ +choice( + 'What do you want to create ?', + [self::TYPE_CONTROLLER, self::TYPE_REPOSITORY, self::TYPE_SERVICE, self::TYPE_Request, self::TYPE_ROUTE, self::TYPE_ALL], + 5, + $maxAttempts = null, + $allowMultipleSelections = true + ); + + $name = $this->ask("What is the name ?", 'Product'); + + foreach ($types as $type) { + switch ($type) { + case self::TYPE_CONTROLLER: + $this->createController($name); + break; + + case self::TYPE_REPOSITORY: + $this->createRepository($name); + break; + + case self::TYPE_SERVICE: + $this->createService($name); + break; + + case self::TYPE_Request: + $this->createRequest($name); + break; + + case self::TYPE_ALL: + $this->createAll($name); + break; + + case self::TYPE_ROUTE: + $this->addToRoute($name); + break; + } + } + + } + + protected function createController($name): void + { + $this->call('aim-admin:make-controller', ['name' => $name]); + } + + protected function createRepository($name): void + { + $this->call('aim-admin:make-repo-interface', ['name' => $name]); + $this->call('aim-admin:make-repo', ['name' => $name]); + } + + protected function createService($name): void + { + $this->call('aim-admin:make-service-interface', ['name' => $name]); + $this->call('aim-admin:make-service', ['name' => $name]); + } + + protected function createRequest($name): void + { + Artisan::call("make:request", ['name' => $name . 'Request']); + } + + protected function addToRoute($name): void + { + $this->call('aim-admin:add-to-route', ['name' => $name]); + } + + protected function createAll($name) + { + $this->createController($name); + $this->createRepository($name); + $this->createService($name); + $this->createRequest($name); + $this->addToRoute($name); + } + + /** + * Get the console command arguments. + * + * @return array + */ + protected function getArguments() + { + return [ + ['name', InputArgument::REQUIRED, 'The name of the model to which the repository will be generated'], + ]; + } +} diff --git a/src/Console/Commands/MakeAimController.php b/src/Console/Commands/MakeAimController.php new file mode 100644 index 0000000..7311610 --- /dev/null +++ b/src/Console/Commands/MakeAimController.php @@ -0,0 +1,124 @@ +resolveStubPath('/stubs/aim-admin.controller.stub'); + } + + /** + * Resolve the fully-qualified path to the stub. + * + * @param string $stub + * @return string + */ + protected function resolveStubPath($stub): string + { + return file_exists($customPath = $this->laravel->basePath(trim($stub, '/'))) + ? $customPath + : __DIR__ . $stub; + } + + /** + * Build the class with the given name. + * + * @param string $name + * @return string + */ + protected function buildClass($name): string + { + $stub = parent::buildClass($name); + + $fullName = Str::singular(class_basename($name)); + + $cleanedName = str_replace("Controller", "", $fullName); + + $camelModel = Str::camel($cleanedName); + + // Replace placeholders in one go + return str_replace( + ["{{ camelName }}", "{{ cleanName }}"], + [$camelModel, $cleanedName], + $stub + ); + } + + /** + * Get the default namespace for the class. + * + * @param string $rootNamespace + * @return string + */ + protected function getDefaultNamespace($rootNamespace): string + { + return $rootNamespace . '\Http\Controllers'; + } + + protected function getNameInput(): string + { + $name = trim($this->argument('name')); + + // Append 'Controller' to the name if it doesn't already end with it + if (!str_ends_with($name, 'Controller')) { + $name .= 'Controller'; + } + + return $name; + } + + /** + * Get the console command options. + * + * @return array + */ + protected function getOptions(): array + { + return [ + ['force', 'f', InputOption::VALUE_NONE, 'Create the class even if the class already exists'], + ]; + } +} diff --git a/src/Console/Commands/MakeAimRepository.php b/src/Console/Commands/MakeAimRepository.php new file mode 100644 index 0000000..b0573a1 --- /dev/null +++ b/src/Console/Commands/MakeAimRepository.php @@ -0,0 +1,135 @@ +resolveStubPath('/stubs/aim-admin.repository.stub'); + } + + /** + * Build the class with the given name. + * + * @param string $name + * @return string + */ + protected function buildClass($name): string + { + // Extract model name and namespace from input name + $parts = explode('\\', trim($this->argument('name'))); + $model = Str::singular(end($parts)); + $namespace = implode('\\', array_slice($parts, 0, -1)); + + // Construct repository namespace + $repoContractsNamespace = "App\Contracts\Repositories"; + if (!empty($namespace)) { + $repoContractsNamespace .= "\\" . $namespace; + } + + // Replace placeholders and return stub + return str_replace( + [ + "{{ tableModel }}", + "{{ repoContractsNamespace }}" + ], + [ + $model, + $repoContractsNamespace + ], + parent::buildClass($name) + ); + } + + + /** + * Resolve the fully-qualified path to the stub. + * + * @param string $stub + * @return string + */ + protected function resolveStubPath($stub): string + { + return file_exists($customPath = $this->laravel->basePath(trim($stub, '/'))) + ? $customPath + : __DIR__ . $stub; + } + + /** + * Get the default namespace for the class. + * + * @param string $rootNamespace + * @return string + */ + protected function getDefaultNamespace($rootNamespace): string + { + return $rootNamespace . '\Repositories'; + } + + + protected function getNameInput(): string + { + $name = trim($this->argument('name')); + + // Append 'Controller' to the name if it doesn't already end with it + if (!str_ends_with($name, 'Repository')) { + $name .= 'Repository'; + } + + return $name; + } + + /** + * Get the console command options. + * + * @return array + */ + protected function getOptions(): array + { + return [ + ['force', 'f', InputOption::VALUE_NONE, 'Create the class even if the class already exists'], + ]; + } +} diff --git a/src/Console/Commands/MakeAimRepositoryInterface.php b/src/Console/Commands/MakeAimRepositoryInterface.php new file mode 100644 index 0000000..84ba531 --- /dev/null +++ b/src/Console/Commands/MakeAimRepositoryInterface.php @@ -0,0 +1,99 @@ +resolveStubPath('/stubs/aim-admin.repository.interface.stub'); + } + + /** + * Resolve the fully-qualified path to the stub. + * + * @param string $stub + * @return string + */ + protected function resolveStubPath($stub): string + { + return file_exists($customPath = $this->laravel->basePath(trim($stub, '/'))) + ? $customPath + : __DIR__ . $stub; + } + + /** + * Get the default namespace for the class. + * + * @param string $rootNamespace + * @return string + */ + protected function getDefaultNamespace($rootNamespace): string + { + return $rootNamespace . '\Contracts\Repositories'; + } + + protected function getNameInput(): string + { + $name = trim($this->argument('name')); + + // Append 'Controller' to the name if it doesn't already end with it + if (!str_ends_with($name, 'RepositoryInterface')) { + $name .= 'RepositoryInterface'; + } + + return $name; + } + + /** + * Get the console command options. + * + * @return array + */ + protected function getOptions(): array + { + return [ + ['force', 'f', InputOption::VALUE_NONE, 'Create the class even if the class already exists'], + ]; + } +} diff --git a/src/Console/Commands/MakeAimService.php b/src/Console/Commands/MakeAimService.php new file mode 100644 index 0000000..da32c36 --- /dev/null +++ b/src/Console/Commands/MakeAimService.php @@ -0,0 +1,143 @@ +resolveStubPath('/stubs/aim-admin.service.stub'); + } + + /** + * Build the class with the given name. + * + * @param string $name + * @return string + */ + protected function buildClass($name): string + { + // Extract model and namespace from input name + $parts = explode('\\', trim($this->argument('name'))); + $namespace = implode('\\', array_slice($parts, 0, -1)); + $model = Str::studly(Str::replace("Service", "", end($parts))); + + // Repository namespace + $repoNamespace = "App\Repositories"; + if (!empty($namespace)) { + $repoNamespace .= "\\" . $namespace; + } + + // Construct repository namespace + $repoConstructNamespace = "App\Contracts\Repositories"; + if (!empty($namespace)) { + $repoConstructNamespace .= "\\" . $namespace; + } + + $serviceContractsNamespace = "App\Contracts\Services"; + if (!empty($namespace)) { + $serviceContractsNamespace .= "\\" . $namespace; + } + + // Combine placeholders and values for replacement + $replacements = [ + "{{ camelName }}" => Str::camel($model), + "{{ tableModel }}" => $model, + "{{ repoNamespace }}" => $repoNamespace, + "{{ repoConstructNamespace }}" => $repoConstructNamespace, + "{{ serviceContractsNamespace }}" => $serviceContractsNamespace, + ]; + + // Replace placeholders in stub and return + return str_replace(array_keys($replacements), array_values($replacements), parent::buildClass($name)); + } + + /** + * Resolve the fully-qualified path to the stub. + * + * @param string $stub + * @return string + */ + protected function resolveStubPath($stub): string + { + return file_exists($customPath = $this->laravel->basePath(trim($stub, '/'))) + ? $customPath + : __DIR__ . $stub; + } + + /** + * Get the default namespace for the class. + * + * @param string $rootNamespace + * @return string + */ + protected function getDefaultNamespace($rootNamespace): string + { + return $rootNamespace . '\Services'; + } + + protected function getNameInput(): string + { + $name = trim($this->argument('name')); + + // Append 'Controller' to the name if it doesn't already end with it + if (!str_ends_with($name, 'Service')) { + $name .= 'Service'; + } + + return $name; + } + + /** + * Get the console command options. + * + * @return array + */ + protected function getOptions(): array + { + return [ + ['force', 'f', InputOption::VALUE_NONE, 'Create the class even if the class already exists'], + ]; + } +} diff --git a/src/Console/Commands/MakeAimServiceInterface.php b/src/Console/Commands/MakeAimServiceInterface.php new file mode 100644 index 0000000..8ae2362 --- /dev/null +++ b/src/Console/Commands/MakeAimServiceInterface.php @@ -0,0 +1,100 @@ +resolveStubPath('/stubs/aim-admin.service.interface.stub'); + } + + /** + * Resolve the fully-qualified path to the stub. + * + * @param string $stub + * @return string + */ + protected function resolveStubPath($stub): string + { + return file_exists($customPath = $this->laravel->basePath(trim($stub, '/'))) + ? $customPath + : __DIR__ . $stub; + } + + /** + * Get the default namespace for the class. + * + * @param string $rootNamespace + * @return string + */ + protected function getDefaultNamespace($rootNamespace): string + { + return $rootNamespace . '\Contracts\Services'; + } + + protected function getNameInput(): string + { + $name = trim($this->argument('name')); + + // Append 'Controller' to the name if it doesn't already end with it + if (!str_ends_with($name, 'ServiceInterface')) { + $name .= 'ServiceInterface'; + } + + return $name; + } + + /** + * Get the console command options. + * + * @return array + */ + protected function getOptions(): array + { + return [ + ['force', 'f', InputOption::VALUE_NONE, 'Create the class even if the class already exists'], + ]; + } +} diff --git a/src/Console/Commands/stubs/aim-admin.controller.stub b/src/Console/Commands/stubs/aim-admin.controller.stub new file mode 100644 index 0000000..8368ebe --- /dev/null +++ b/src/Console/Commands/stubs/aim-admin.controller.stub @@ -0,0 +1,92 @@ +{{ camelName }}Service = ${{ camelName }}Service; + } + + public function getRepository() + { + return $this->repo; + } + + public function configureActions(): iterable + { + return [ + + ]; + } + + public function configureForm() + { + $fields = [ + IdField::init('id'), + // TextareaField::init('my_remarks') + ]; + $this->getForm($fields) + ->setName('form_name') + ->setMethod('post') + ->setActionUrl(route('set_route')); + } + + public function create() + { + $this->initCreate(); + return view('aim-admin::create'); + } + + public function store(Request $request): RedirectResponse + { + $this->initStore($request); + + } + + public function configureFilter(): void + { + $fields = [ + TextField::init('id'), + // TextField::init('other') + ]; + $this->getFilter($fields); + } + + public function list() + { + $this->initGrid(['id'], pagination: 10); + return view('aim-admin::list'); + } + + public function show($id) + { + $this->initShow($id, ['id', 'created_at']); + return view('aim-admin::show'); + } + + public function edit($id) + { + $this->initEdit($id); + return view('aim-admin::edit'); + } + + public function delete($id) + { + // Set your delete functionality + } + +} diff --git a/src/Console/Commands/stubs/aim-admin.repository.interface.stub b/src/Console/Commands/stubs/aim-admin.repository.interface.stub new file mode 100644 index 0000000..71c212f --- /dev/null +++ b/src/Console/Commands/stubs/aim-admin.repository.interface.stub @@ -0,0 +1,9 @@ +orderBy('created_at', 'desc'); + } + + public function applyFilterQuery(Builder $query, array $filters): Builder + { + // $filters_name = isset($filters['name']) ? $filters['name'] : ""; + // $query->where(DB::raw('lower(name)'), 'like', '%'.strtolower($filters_name).'%'); + + return parent::applyFilterQuery($query, $filters); + } + +} diff --git a/src/Console/Commands/stubs/aim-admin.request.stub b/src/Console/Commands/stubs/aim-admin.request.stub new file mode 100644 index 0000000..cebf7cc --- /dev/null +++ b/src/Console/Commands/stubs/aim-admin.request.stub @@ -0,0 +1,28 @@ +|string> + */ + public function rules(): array + { + return [ + // + ]; + } +} diff --git a/src/Console/Commands/stubs/aim-admin.service.interface.stub b/src/Console/Commands/stubs/aim-admin.service.interface.stub new file mode 100644 index 0000000..437ac70 --- /dev/null +++ b/src/Console/Commands/stubs/aim-admin.service.interface.stub @@ -0,0 +1,9 @@ +error('package.json not found.'); + return; + } + + // File path to the 'adminlte.js' + $filePath = base_path('resources/js/app.js'); + + // Check if the file exists + if (!file_exists($filePath)) { + $this->error("app.js File does not exist: {$filePath}"); + return; + } + + self::makeFolders(); + $this->info('Contracts Folders have been created.'); +// self::replacePackageJson(); +// $this->info('Package.json has been replaced.'); + self::updatePackages(); + $this->info('package.json has been updated.'); + self::updateAimAdminAssets(); + $this->info('app.js has been updated.'); + self::exportConfig(); + $this->info('Aim Admin Config file has been exported.'); + self::exportMigrations(); + $this->info('Migrations have been exported.'); + self::exportModel(); + $this->info('User model has been exported.'); + self::addedDashboardRoute(); + $this->info('Home route has been added.'); + self::addedDashboardBlade(); + $this->info('Home blade has been added.'); + self::exportVite(); + $this->info('vite.config.js has been exported.'); + $this->info('Aim Admin has been installed successfully. Please run npm i && npm run build'); + } + + + /** + * Make the Contracts folders + * @return void + */ + protected static function makeFolders(): void + { + tap(new Filesystem, function ($filesystem) { + + $contractsServicesPath = app_path('Contracts/Services'); + if (!$filesystem->isDirectory($contractsServicesPath)) { + $filesystem->makeDirectory($contractsServicesPath, 0755, true); + } + + $contractsRepositoriesPath = app_path('Contracts/Repositories'); + if (!$filesystem->isDirectory($contractsRepositoriesPath)) { + $filesystem->makeDirectory($contractsRepositoriesPath, 0755, true); + } + + $repositoriesPath = app_path('Repositories'); + if (!$filesystem->isDirectory($repositoriesPath)) { + $filesystem->makeDirectory($repositoriesPath, 0755, true); + } + + $servicesPath = app_path('Services'); + if (!$filesystem->isDirectory($servicesPath)) { + $filesystem->makeDirectory($servicesPath, 0755, true); + } + + }); + + } + + /** + * @param array $packages + * @param array $requiredPackages + * @return array + */ + protected static function updatePackageArray(array $packages, array $requiredPackages): array + { + foreach ($requiredPackages as $package => $version) { + if (!array_key_exists($package, $packages)) { + $packages[$package] = $version; + } + } + + return $packages; + } + + /** + * Replace the package.json in case of using webpack + * @return void + */ + protected static function replacePackageJson(): void + { + $filesystem = new Filesystem; + + $destinationPath = base_path('package.json'); + if ($filesystem->exists($destinationPath)) { + $filesystem->delete($destinationPath); + } + + $sourcePath = __DIR__ . '../../stubs/package.json'; + $filesystem->copy($sourcePath, $destinationPath); + } + + /** + * Merge package.json dependency + * @return void + */ + protected static function updatePackages(): void + { + $packagesFile = json_decode(file_get_contents(base_path('package.json')), true); + + $requiredPackages = Helper::requiredPackages(); + + // Combine existing dependencies and devDependencies + $existingPackages = array_merge( + $packagesFile['dependencies'] ?? [], + $packagesFile['devDependencies'] ?? [] + ); + + // Filter out the required packages that already exist + $packagesToAdd = array_diff_key($requiredPackages, $existingPackages); + + // Update devDependencies with the filtered packages + $packagesFile['devDependencies'] = static::updatePackageArray( + $packagesFile['devDependencies'] ?? [], + $packagesToAdd + ); + + ksort($packagesFile['devDependencies']); + + file_put_contents( + base_path('package.json'), + json_encode($packagesFile, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . PHP_EOL + ); + } + + /** + * Added the required assets for Aim Admin + * @return void + */ + protected static function updateAimAdminAssets(): void + { + // File path to the 'app.js' + $appFilePath = base_path('resources/js/app.js'); + $boostrapFilePath = base_path('resources/js/bootstrap.js'); + + // app.js Line to be added + $appLineToAdd = "import '../../vendor/codecoz/aim-admin/resources/js/adminlte.js';\n"; + $appLineToAdd .= "import '../../vendor/codecoz/aim-admin/resources/scss/adminlte.scss';\n"; + + // Check if the import has already been added (using a more robust method) + if (!self::hasImportBeenAdded($appFilePath, $appLineToAdd)) { + // Append the line if it does not exist + file_put_contents($appFilePath, $appLineToAdd, FILE_APPEND); + } + + // bootstrap.js Line to be added + $boostrapLineToAdd = "import $ from 'jquery';\n"; + $boostrapLineToAdd .= "window.$ = window.jQuery = $;\n"; + + // Check if the import has already been added (using a more robust method) + if (!self::hasImportBeenAdded($boostrapFilePath, $boostrapLineToAdd)) { + // Append the line if it does not exist + file_put_contents($boostrapFilePath, $boostrapLineToAdd, FILE_APPEND); + } + } + + /** + * Export the Config file. + */ + protected static function exportConfig(): void + { + copy(__DIR__ . '../../../config/aim-admin.php', base_path('config/aim-admin.php')); + } + + /** + * Export the migrations + * @return void + */ + protected static function exportMigrations(): void + { + $filesystem = new Filesystem(); + $sourcePath = Admin::packagePath('database/migrations'); + $destinationPath = database_path('migrations'); + + // Check if the destination directory exists + if ($filesystem->exists($destinationPath)) { + // Delete the existing directory + $filesystem->deleteDirectory($destinationPath); + } + + if (!$filesystem->copyDirectory($sourcePath, $destinationPath)) { + error_log("Failed to copy directory from $sourcePath to $destinationPath"); + } + } + + /** + * Export User Model + * @return void + */ + + protected static function exportModel(): void + { + if (!is_dir($directory = app_path('Models'))) { + mkdir($directory, 0755, true); + } + + $filesystem = new Filesystem; + + collect($filesystem->allFiles(base_path('vendor/codecoz/aim-admin/src/stubs/app/Models'))) + ->each(function (SplFileInfo $file) use ($filesystem) { + $filesystem->copy( + $file->getPathname(), + app_path('Models/' . Str::replaceLast('.stub', '.php', $file->getFilename())) + ); + }); + } + + /** + * Added Home as named route + * @return void + */ + protected static function addedDashboardRoute(): void + { + // Read the content of web.php.stub + $stubContent = file_get_contents(__DIR__ . '../../stubs/route/web.php.stub'); + + // Get the path to web.php + $webPath = base_path('routes/web.php'); + + // Read the current content of web.php + $webContent = file_get_contents($webPath); + + // Check if stub content is already in web.php + if (!str_contains($webContent, "name('home')")) { + // If not, append the stub content to web.php + file_put_contents($webPath, $stubContent, FILE_APPEND); + } + } + + /** + * Added home blade + * @return void + */ + protected static function addedDashboardBlade(): void + { + copy(__DIR__ . '../../stubs/resources/views/dashboard.blade.php', resource_path('views/dashboard.blade.php')); + } + + /** + * Export the vite.config.js + * @return void + */ + protected static function exportVite(): void + { + copy(__DIR__ . '../../stubs/vite.config.js', base_path('vite.config.js')); + } + + + /** + * Helper function to check for the import more reliably + * @param $filePath + * @param $lineToAdd + * @return bool + */ + protected static function hasImportBeenAdded($filePath, $lineToAdd): bool + { + $fileContent = file_get_contents($filePath); + return str_contains($fileContent, $lineToAdd); + } + +} diff --git a/src/Console/UpgradeAimAdminCommand.php b/src/Console/UpgradeAimAdminCommand.php new file mode 100644 index 0000000..72c4bde --- /dev/null +++ b/src/Console/UpgradeAimAdminCommand.php @@ -0,0 +1,142 @@ +info('Aim Admin is upgrading. Please wait....'); + self::exportLoadingGif(); + self::checkMissingOrUpdatedPackages(); + sleep(5); + $this->info('Aim Admin upgrade complete !'); + } + + + /** + * Export the migrations + * @return void + */ + protected static function exportLoadingGif(): void + { + $filesystem = new Filesystem(); + $sourcePath = Admin::packagePath('resources/img/loader.gif'); + $destinationPath = public_path('img/loader.gif'); + + // Check if the destination directory exists + if (!$filesystem->exists(dirname($destinationPath))) { + // Create the directory if it does not exist + $filesystem->makeDirectory(dirname($destinationPath), 0755, true); + } + + // Copy the loader.gif file to the destination directory + if (!$filesystem->copy($sourcePath, $destinationPath)) { + error_log("Failed to copy loading gif from $sourcePath to $destinationPath"); + } + } + + /** + * Check for missing or updated packages and update package.json + * @return void + */ + protected function checkMissingOrUpdatedPackages(): void + { + $this->info('Checking for missing or updated packages...'); + + $packagesFile = json_decode(file_get_contents(base_path('package.json')), true); + + $requiredPackages = Helper::requiredPackages(); + + $existingPackages = array_merge( + $packagesFile['dependencies'] ?? [], + $packagesFile['devDependencies'] ?? [] + ); + + $missingPackages = []; + $updatedPackages = []; + + foreach ($requiredPackages as $package => $version) { + if (!isset($existingPackages[$package])) { + $missingPackages[$package] = $version; + } elseif ($existingPackages[$package] !== $version) { + $updatedPackages[$package] = [ + 'current' => $existingPackages[$package], + 'required' => $version, + ]; + } + } + + if (empty($missingPackages) && empty($updatedPackages)) { + $this->info('All packages are up to date.'); + } else { + if (!empty($missingPackages)) { + $this->info('Missing packages:'); + foreach ($missingPackages as $package => $version) { + $this->line(" - $package: $version"); + } + } + + if (!empty($updatedPackages)) { + $this->info('Packages to be updated:'); + foreach ($updatedPackages as $package => $versions) { + $this->line(" - $package: current version {$versions['current']}, required version {$versions['required']}"); + } + } + // Ask for user permission before updating package.json + if ($this->confirm('Do you want to update package.json with the missing and updated packages?')) { + $this->info('Up gradation is on progress....'); + $this->upgradePackageJson($missingPackages, $updatedPackages); + } else { + $this->info('package.json was not updated.'); + } + } + } + + /** + * Update package.json with missing and updated packages + * @param array $missingPackages + * @param array $updatedPackages + * @return void + */ + protected function upgradePackageJson(array $missingPackages, array $updatedPackages): void + { + $packagesFile = json_decode(file_get_contents(base_path('package.json')), true); + + // Combine all the packages to be updated + $packagesToUpdate = array_merge( + $missingPackages, + array_map(function ($versions) { + return $versions['required']; + }, $updatedPackages) + ); + + // Update devDependencies with the packages to be updated + $packagesFile['devDependencies'] = array_merge( + $packagesFile['devDependencies'] ?? [], + $packagesToUpdate + ); + + ksort($packagesFile['devDependencies']); + + file_put_contents( + base_path('package.json'), + json_encode($packagesFile, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . PHP_EOL + ); + + $this->info('package.json has been updated with missing and updated packages.'); + } +} diff --git a/src/Contracts/Controller/AimAdminControllerInterface.php b/src/Contracts/Controller/AimAdminControllerInterface.php new file mode 100644 index 0000000..a522d30 --- /dev/null +++ b/src/Contracts/Controller/AimAdminControllerInterface.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace CodeCoz\AimAdmin\Contracts\Controller; + +/** + * This interface defines blueprints of AimAdmin admin controller. + * as well as filter and sorting options. + * + * @author CodeCoz + */ +interface AimAdminControllerInterface +{ + function getRepository(); + function initGrid(array $columns); +} diff --git a/src/Contracts/Field/FieldInterface.php b/src/Contracts/Field/FieldInterface.php new file mode 100644 index 0000000..7964be0 --- /dev/null +++ b/src/Contracts/Field/FieldInterface.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace CodeCoz\AimAdmin\Contracts\Field; + +/** + * This interface defines blueprints of field for curd boad components i.e. grid, form and filters + * + * + * @author CodeCoz + */ +interface FieldInterface +{ + public static function init(string $name, ?string $label = null, mixed ...$params): self; + +} diff --git a/src/Contracts/Repository/AimAdminRepositoryInterface.php b/src/Contracts/Repository/AimAdminRepositoryInterface.php new file mode 100644 index 0000000..21feb03 --- /dev/null +++ b/src/Contracts/Repository/AimAdminRepositoryInterface.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace CodeCoz\AimAdmin\Contracts\Repository; + +/** + * This interface defines blueprints of AimAdmin Repository. + * + * + * @author CodeCoz + */ +interface AimAdminRepositoryInterface +{ + public function getModelFqcn(): string; + public function crudShow(int|string $id): ?\ArrayAccess; +} diff --git a/src/Contracts/Service/CrudBoard/CrudBoardInterface.php b/src/Contracts/Service/CrudBoard/CrudBoardInterface.php new file mode 100644 index 0000000..69b33dd --- /dev/null +++ b/src/Contracts/Service/CrudBoard/CrudBoardInterface.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace CodeCoz\AimAdmin\Contracts\Service\CrudBoard; + +use CodeCoz\AimAdmin\Contracts\Repository\AimAdminRepositoryInterface; +use CodeCoz\AimAdmin\Form\AbstractForm; + +/** + * This interface defines blueprints of CRUD Board. CRUD Board is will handle CRUD operations + * as well as filter and sorting options. + * + * @author CodeCoz + */ +interface CrudBoardInterface +{ + public function createGrid(CrudGridLoaderInterface $dataLoader, array $params = []): CrudGridInterface; + + public function getRepository(): AimAdminRepositoryInterface; + + public function setRepository(AimAdminRepositoryInterface $repo): self; + + public function getGrid(): CrudGridInterface; + + public function addGridActions(array $actions = []); + + public function getForm(): AbstractForm; + + public function createForm(array $fields); + + public function getCrudShow(): CrudShowInterface; + +} diff --git a/src/Contracts/Service/CrudBoard/CrudFormHandlerInterface.php b/src/Contracts/Service/CrudBoard/CrudFormHandlerInterface.php new file mode 100644 index 0000000..7d71ca8 --- /dev/null +++ b/src/Contracts/Service/CrudBoard/CrudFormHandlerInterface.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace CodeCoz\AimAdmin\Contracts\Service\CrudBoard; + +/** + * This interface defines blueprints of AimAdmin CRUD form data handler. + * It defines how to validate and store data to DB + * It will ensure to provide record for CrudBoard grid. + * + * @author CodeCoz + */ +interface CrudFormHandlerInterface +{ + function saveFormData(array $data = []); +} diff --git a/src/Contracts/Service/CrudBoard/CrudGridInterface.php b/src/Contracts/Service/CrudBoard/CrudGridInterface.php new file mode 100644 index 0000000..30fcff9 --- /dev/null +++ b/src/Contracts/Service/CrudBoard/CrudGridInterface.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace CodeCoz\AimAdmin\Contracts\Service\CrudBoard; + +use CodeCoz\AimAdmin\Services\CrudBoard\GridFilter; +use Illuminate\Pagination\LengthAwarePaginator; + +/** + * This interface defines blueprints of AimAdmin CRUD grid. + * It will ensure to provide record for CrudBoard grid. + * + * @author CodeCoz + */ +interface CrudGridInterface +{ + function getGridDataLoader(): CrudGridLoaderInterface; + + function getGridData(): LengthAwarePaginator; + + function addColumns(array $columns); + + function getColumns(); + + function addActions(array $actions = []); + + public function getFilter(): GridFilter; + + public function setTitle(string $title): self; + + public function getTitle(): ?string; + + public static function init(CrudGridLoaderInterface $gridDataLoader, array $params): CrudGridInterface; + +} diff --git a/src/Contracts/Service/CrudBoard/CrudGridLoaderInterface.php b/src/Contracts/Service/CrudBoard/CrudGridLoaderInterface.php new file mode 100644 index 0000000..1d2e658 --- /dev/null +++ b/src/Contracts/Service/CrudBoard/CrudGridLoaderInterface.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace CodeCoz\AimAdmin\Contracts\Service\CrudBoard; + +/** + * This interface defines blueprints of AimAdmin CRUD grid loader. + * It will ensure to provide record for CrudBoard grid. + * + * @author CodeCoz + */ + +use Illuminate\Pagination\CursorPaginator; +use Illuminate\Pagination\LengthAwarePaginator; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Support\Collection; + +interface CrudGridLoaderInterface +{ + public function getGridData(array $filters = []): ?iterable; + + public function getGridQuery(): ?Builder; + + public function getGridPaginator(array $filters): ?LengthAwarePaginator; + + public function getGridCursorPaginator(array $filters): ?CursorPaginator; + + public function applyFilterQuery(Builder $query, array $filters): ?Builder; + + public function applyFilterData(Collection $data, array $filters): Collection; +} diff --git a/src/Contracts/Service/CrudBoard/CrudShowInterface.php b/src/Contracts/Service/CrudBoard/CrudShowInterface.php new file mode 100644 index 0000000..ab6a09e --- /dev/null +++ b/src/Contracts/Service/CrudBoard/CrudShowInterface.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace CodeCoz\AimAdmin\Contracts\Service\CrudBoard; + +use CodeCoz\AimAdmin\Collection\ActionCollection; +use CodeCoz\AimAdmin\Collection\FieldCollection; + +/** + * This interface defines blueprints of AimAdmin CRUD grid loader. + * It will ensure to provide record for CrudBoard grid. + * + * @author CodeCoz + */ +interface CrudShowInterface +{ + function getActions(): ActionCollection; + + function getFields(): FieldCollection; + + function getRecord(): \ArrayAccess; +} diff --git a/src/Controller/AbstractAimAdminController.php b/src/Controller/AbstractAimAdminController.php new file mode 100644 index 0000000..0924531 --- /dev/null +++ b/src/Controller/AbstractAimAdminController.php @@ -0,0 +1,109 @@ +actionList = $this->actionList ?? $this->configureActions(); + $grid = CrudBoardFacade::createGrid($this->getRepository(), $params) + ->addColumns($columns) + ->addActions($this->actionList); + $this->configureFilter(); + return $grid; + } + + protected function getForm(array $fields) + { + $this->actionList = $this->actionList ?? $this->configureActions(); + return CrudBoardFacade::createForm($fields) + ->setFormStat(CrudForm::STAT_NEW) + ->setActions($this->actionList); + } + + protected function getFilter(array $fields) + { + $this->actionList = $this->actionList ?? $this->configureActions(); + return CrudBoardFacade::getGrid() + ->getFilter() + ->addFields($fields) + ->setFormStat(CrudForm::STAT_NEW) + ->setActions($this->actionList) + ->assignQueryData(); + } + + protected function initCreate() + { + $this->configureForm(); + return CrudBoardFacade::getForm(); + } + + + protected function initStore(Request $request) + { + $this->configureForm(); + return CrudBoardFacade::getForm() + ->setFormHandler($this->getRepository()) + ->processData($request); + } + + protected function getDefaultCrudActions(): iterable + { + return [ + + ]; + } + + protected function initEdit(mixed $row): CrudForm + { + $repo = $this->getRepository(); + $model = ($row instanceof Model) ? $row : $repo->getRecordForEdit($row); + $this->configureForm(); + return CrudBoardFacade::getForm() + ->setFormStat(CrudForm::STAT_EDIT) + ->setData($model); + } + + protected function initShow(mixed $row, array $fields) + { + $this->actionList = $this->actionList ?? $this->configureActions(); + return CrudBoardFacade::setRepository($this->getRepository()) + ->createShow($row, $fields) + ->addActions($this->actionList); + } + + public function configureActions(): iterable + { + return $this->getDefaultCrudActions(); + } + +} + diff --git a/src/Driver/AimAdminUserProviderDriver.php b/src/Driver/AimAdminUserProviderDriver.php new file mode 100644 index 0000000..8d39e0a --- /dev/null +++ b/src/Driver/AimAdminUserProviderDriver.php @@ -0,0 +1,58 @@ +is_successful ?? true; + } catch (\Exception $e) { + return false; + } + } + + public function updateRememberToken(Authenticatable $user, $token): void + { + $userData = session('user'); + if ($userData && $userData['id'] == $user->getAuthIdentifier()) { + $userData['remember_token'] = $token; + session(['user' => $userData]); + } + } + + public function rehashPasswordIfRequired(Authenticatable $user, array $credentials, bool $force = false) + { + + } +} + diff --git a/src/Dto/CrudBoard/AbstractHtmlElementDto.php b/src/Dto/CrudBoard/AbstractHtmlElementDto.php new file mode 100644 index 0000000..731e1c8 --- /dev/null +++ b/src/Dto/CrudBoard/AbstractHtmlElementDto.php @@ -0,0 +1,312 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace CodeCoz\AimAdmin\Dto\CrudBoard; + + +use CodeCoz\AimAdmin\Support\OptionValueStore; + +/** + * This base class is for crudboard DTO. This is a base class and contains + * common methods and attributes of all dto. + * + * @author CodeCoz + */ +abstract class AbstractHtmlElementDto +{ + // these are the actions applied to a specific row + public const TYPE_ROW = 'row'; + // these are the actions that are not associated to an crud board + // (they are available only in the INDEX page) + public const TYPE_CRUD_BOARD = 'crud_board'; + // these are actions that can be applied to one or more rows at the same time + public const TYPE_BATCH = 'batch'; + + public const TYPE_FORM = 'form'; + + public const TYPE_SHOW = 'show'; + + // these are actions that can be applied to filter + public const TYPE_FILTER = 'filter'; + + const INPUT_TYPE_HIDDEN = 'hidden'; + const INPUT_TYPE_FILE = 'file'; + + public const GRID_DELETE_ACTION = 'grid-delete'; + + private ?string $name = null; + private $label; + private $help; + private ?string $icon = null; + private string $cssClass = ''; + private ?string $textAlign = null; + private OptionValueStore $htmlAttributes; + private ?string $htmlElement = null; + private $formatValueCallable; + private $formattedValue; + private $displayCallable; + private string $component; + private ?bool $disable = null; + private ?bool $readonly = null; + private ?string $placeholder = null; + private mixed $value = null; + private ?string $layoutClass = null; + private bool $required = false; + private OptionValueStore $options; + + public function __construct() + { + $this->htmlAttributes = OptionValueStore::init(); + $this->options = OptionValueStore::init(); + + } + + public function getName(): string + { + return $this->name; + } + + public function setName(string $name): void + { + $this->name = $name; + } + + public function getLabel() + { + return $this->label; + } + + public function getValue(): mixed + { + return $this->value; + } + + public function setValue(mixed $value): void + { + $this->value = $value; + } + + public function setLabel(mixed $label): void + { + $this->label = \is_string($label) ? \ucwords(\str_replace('_', ' ', $label)) : $label; + } + + public function getCssClass(): string + { + return $this->cssClass; + } + + public function setCssClass(string $cssClass): void + { + $this->cssClass = $cssClass; + } + + public function getComponent(): string + { + return $this->component; + } + + public function setComponent(string $component): void + { + $this->component = $component; + } + + public function getFormatValueCallable(): ?callable + { + return $this->formatValueCallable; + } + + public function setFormatValueCallable(?callable $callable): void + { + $this->formatValueCallable = $callable; + } + + public function getFormattedValue(): mixed + { + $this->formattedValue = ($this->formatValueCallable && $this->value) ? \call_user_func($this->formatValueCallable, $this->value) + : $this->value ?? $this->formattedValue; + return $this->formattedValue; + } + + public function setFormattedValue(mixed $formattedValue): void + { + $this->formattedValue = $formattedValue; + } + + public function getTextAlign(): ?string + { + return $this->textAlign; + } + + public function setTextAlign(string $textAlign): void + { + $this->textAlign = $textAlign; + } + + public function shouldBeDisplayedFor(mixed $row): bool + { + return null === $this->displayCallable || (bool)\call_user_func($this->displayCallable, $row); + } + + public function setDisplayCallable(callable $displayCallable): void + { + $this->displayCallable = $displayCallable; + } + + public function isDisabled(): ?bool + { + return $this->disable; + } + + public function setDisable(bool $disable): void + { + $this->disable = $disable; + } + + public function isReadonly(): ?bool + { + return $this->readonly; + } + + public function setReadonly(bool $readonly): void + { + $this->readonly = $readonly; + } + + public function isRequired(): ?bool + { + return $this->required; + } + + public function setRequired(bool $required): void + { + $this->required = $required; + } + + + public function getHtmlElement(): string + { + return $this->htmlElement; + } + + public function setHtmlElement(string $htmlElement): void + { + $this->htmlElement = $htmlElement; + } + + public function getHtmlAttributes(): OptionValueStore + { + return $this->htmlAttributes; + } + + public function addHtmlAttributes(array $htmlAttributes): void + { + $this->htmlAttributes->add($this->htmlAttributes, $htmlAttributes); + } + + public function setHtmlAttributes(array $htmlAttributes): void + { + $this->htmlAttributes->set($htmlAttributes); + } + + public function setHtmlAttribute(string $attributeName, mixed $attributeValue): void + { + $this->htmlAttributes->set($attributeName, $attributeValue); + } + + public function getIcon(): ?string + { + return $this->icon; + } + + public function setIcon(?string $icon): void + { + $this->icon = $icon; + } + + public function getHelp(): string|null + { + return $this->help; + } + + public function setHelp(string $help): void + { + $this->help = $help; + } + + public function getAttributesAsHtml(): string + { + $html = ''; + $attributes = $this->htmlAttributes->get(); + foreach ($attributes as $attr => $val) { + $html .= $this->prepareHtmlAttributes($attr, $val); + } + // echo $html; + return $html; + } + + private function prepareHtmlAttributes(string $attr, mixed $val): string + { + $html = ''; + switch (strtolower($attr)) { + case 'class': + $this->cssClass .= " $val "; + break; + case 'readonly': + $this->readonly = (bool)$val; + break; + case 'disabled': + $this->disable = (bool)$val; + break; + case 'required': + $this->required = (bool)$val; + break; + case 'placeholder': + $this->placeholder = $val; + break; + default: + $html = $attr . '="' . $val . '"'; + } + return $html; + } + + + public function setPlaceholder(string $placeholder): void + { + $this->placeholder = $placeholder; + } + + public function getPlaceholder(): ?string + { + return $this->placeholder; + } + + public function setLayoutClass(string $class): void + { + $this->layoutClass = $class; + } + + public function getLayoutClass(): ?string + { + return $this->layoutClass; + } + + public function getOption($name): mixed + { + return $this->options->get($name); + } + + public function setOption(mixed $name, mixed $value): void + { + $this->options->set($name, $value); + } + +} diff --git a/src/Dto/CrudBoard/ActionDto.php b/src/Dto/CrudBoard/ActionDto.php new file mode 100644 index 0000000..7ed36f8 --- /dev/null +++ b/src/Dto/CrudBoard/ActionDto.php @@ -0,0 +1,113 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace CodeCoz\AimAdmin\Dto\CrudBoard; + + +/** + * This class is for action DTO of grid board . + * + * @author CodeCoz + */ +final class ActionDto extends AbstractHtmlElementDto +{ + private ?string $type = null; + private $url; + private ?string $routeName = null; + private $routeParameters = []; + + public function getType(): string + { + return $this->type; + } + + public function setType(string $type): void + { + $this->type = $type; + } + + public function getUrl(): string|callable + { + return $this->url; + } + + public function setUrl(string|callable $url): void + { + $this->url = $url; + } + + public function isRowAction(): bool + { + return self::TYPE_ROW === $this->type; + } + + public function isCrudBoardAction(): bool + { + return self::TYPE_CRUD_BOARD === $this->type; + } + + public function isBatchAction(): bool + { + return self::TYPE_BATCH === $this->type; + } + + public function isFormAction(): bool + { + return self::TYPE_FORM === $this->type; + } + + public function isShowAction(): bool + { + return self::TYPE_SHOW === $this->type; + } + + public function isGridDeleteAction(): bool + { + return self::GRID_DELETE_ACTION == $this->getOption('role'); + } + + public function getRouteName(): ?string + { + return $this->routeName; + } + + public function setRouteName(string $routeName): void + { + $this->routeName = $routeName; + } + + public function getRouteParameters(): array|\Closure + { + return $this->routeParameters; + } + + public function setRouteParameters(array|\Closure $routeParameters): void + { + $this->routeParameters = $routeParameters; + } + + public function isSubmitAction(): bool + { + return $this->getHtmlAttributes()->get('type', '') === 'submit'; + } + + public function isFilterAction(): bool + { + return self::TYPE_FILTER === $this->type; + } + + public function isButton(): bool + { + return $this->getHtmlElement() == 'button'; + } + +} + diff --git a/docs/contents/filter.md b/src/Dto/CrudBoard/FieldCollectionDto.php similarity index 100% rename from docs/contents/filter.md rename to src/Dto/CrudBoard/FieldCollectionDto.php diff --git a/src/Dto/CrudBoard/FieldDto.php b/src/Dto/CrudBoard/FieldDto.php new file mode 100644 index 0000000..181f11f --- /dev/null +++ b/src/Dto/CrudBoard/FieldDto.php @@ -0,0 +1,269 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace CodeCoz\AimAdmin\Dto\CrudBoard; + +use CodeCoz\AimAdmin\Support\OptionValueStore; + +/** + * This class is for field creation in crudboard . + * + * @author CodeCoz + */ +final class FieldDto +{ + private ?string $name = null; + private mixed $value = null; + private mixed $formattedValue = null; + private $formatValueCallable; + private string $label; + private string $cssClass = ''; + private ?string $textAlign = null; + private ?string $help; + private ?bool $sortable = null; + private ?bool $virtual = null; + private ?string $component = null; + private ?bool $hidden = null; + private ?bool $disable = null; + private ?string $inputType = null; + private OptionValueStore $customOptions; + private string $validationRule = ''; + private string $placeholder; + private OptionValueStore $htmlAttributes; + + public function __construct() + { + $this->customOptions = OptionValueStore::init(); + $this->htmlAttributes = OptionValueStore::init(); + + } + + public function getName(): string + { + return $this->name; + } + + public function setName(string $name): void + { + $this->name = $name; + } + + public function getValue(): mixed + { + return $this->value; + } + + public function setValue(mixed $value): void + { + $this->value = $value; + } + + + public function getLabel() + { + return $this->label; + } + + public function setLabel($label): void + { + $this->label = \ucwords(\str_replace('_', ' ', $label)); + } + + public function getFormatValueCallable(): ?callable + { + return $this->formatValueCallable; + } + + public function setFormatValueCallable(?callable $callable): void + { + $this->formatValueCallable = $callable; + } + + public function getTextAlign(): ?string + { + return $this->textAlign; + } + + public function setTextAlign(string $textAlign): void + { + $this->textAlign = $textAlign; + } + + public function getCssClass(): string + { + return $this->cssClass; + } + + public function setCssClass(string $cssClass): void + { + $this->cssClass = trim($cssClass); + } + + public function getComponent(): ?string + { + return $this->component; + } + + public function setComponent(?string $component): void + { + $this->component = $component; + } + + public function getHelp(): string|null + { + return $this->help; + } + + public function setHelp(string $help): void + { + $this->help = $help; + } + + public function isSortable(): ?bool + { + return $this->sortable; + } + + public function setSortable(bool $isSortable): void + { + $this->sortable = $isSortable; + } + + public function isHidden(): ?bool + { + return $this->hidden; + } + + public function setHidden(bool $hidden): void + { + $this->hidden = $hidden; + } + + public function isDisabled(): ?bool + { + return $this->disable; + } + + public function setDisable(bool $disable): void + { + $this->disable = $disable; + } + + public function isVirtual(): ?bool + { + return $this->virtual; + } + + public function setVirtual(bool $isVirtual): void + { + $this->virtual = $isVirtual; + } + + public function getFormattedValue(): mixed + { + return $this->formattedValue; + } + + public function setFormattedValue(mixed $formattedValue): void + { + $this->formattedValue = $formattedValue; + } + + public function setInputType(string $inputType): void + { + $this->inputType = $inputType; + } + + public function getInputType(): ?string + { + return $this->inputType; + } + + public function setCustomOptions(array $customOptions): void + { + $this->customOptions = OptionValueStore::init($customOptions); + } + + public function setCustomOption(string $optionName, mixed $optionValue): void + { + $this->customOptions->set($optionName, $optionValue); + } + + public function getCustomOption(string $optionName): string|int|null + { + return $this->customOptions->get($optionName); + } + + public function getCustomOptions(): mixed + { + return $this->customOptions->all(); + } + + public function setValidationRule(string $rule): void + { + $this->validationRule = $rule; + } + + public function getValidationRule(): string + { + return $this->validationRule; + } + + public function getHtmlAttributes(): OptionValueStore + { + return $this->htmlAttributes; + } + + public function addHtmlAttributes(array $htmlAttributes): void + { + $this->htmlAttributes->add($this->htmlAttributes, $htmlAttributes); + } + + public function setHtmlAttributes(array $htmlAttributes): void + { + $this->htmlAttributes->set($htmlAttributes); + } + + public function setHtmlAttribute(string $attributeName, mixed $attributeValue): void + { + $this->htmlAttributes->set($attributeName, $attributeValue); + } + + public function getAttributesAsHtml(): string + { + $html = ''; + $attributes = $this->htmlAttributes->get(); + foreach ($attributes as $attr => $val) { + $html .= $this->prepareHtmlAttributes($attr, $val); + } + return $html; + } + + private function prepareHtmlAttributes(string $attr, mixed $val): string + { + $html = ''; + switch (strtolower($attr)) { + case 'class': + $this->cssClass .= " $val "; + break; + case 'disabled': + $this->disable = (bool)$val; + break; + case 'placeholder': + $this->placeholder = $val; + break; + default: + $html = " $attr = $val "; + } + return $html; + } + +} diff --git a/src/Dto/CrudBoard/FormFieldDto.php b/src/Dto/CrudBoard/FormFieldDto.php new file mode 100644 index 0000000..799d0bb --- /dev/null +++ b/src/Dto/CrudBoard/FormFieldDto.php @@ -0,0 +1,83 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace CodeCoz\AimAdmin\Dto\CrudBoard; + +use CodeCoz\AimAdmin\Support\OptionValueStore; + +final class FormFieldDto extends AbstractHtmlElementDto +{ + private OptionValueStore $customOptions; + private $validationRule; + private ?string $inputType = null; + + public function __construct() + { + parent::__construct(); + $this->customOptions = OptionValueStore::init(); + } + + public function setCustomOptions(array $customOptions): void + { + $this->customOptions->set($customOptions); + } + + public function setCustomOption(string $optionName, mixed $optionValue): void + { + $this->customOptions->set($optionName, $optionValue); + } + + public function getCustomOptions(): array + { + return $this->customOptions->all(); + } + + public function getCustomOptionStore(): OptionValueStore + { + return $this->customOptions; + } + + public function getCustomOption(string $optionName): mixed + { + return $this->customOptions->get($optionName); + } + + public function setValidationRule(array|string $rules) + { + $this->validationRule = $rules; + } + + public function getValidationRule(): null|array|string + { + return $this->validationRule; + } + + public function setInputType(string $inputType): void + { + $this->inputType = $inputType; + } + + public function getInputType(): ?string + { + return $this->inputType; + } + + public function isHiddenInput() + { + return $this->inputType == self::INPUT_TYPE_HIDDEN; + } + + public function isFileInput() + { + return $this->inputType == self::INPUT_TYPE_FILE; + } + +} diff --git a/src/Facades/Admin.php b/src/Facades/Admin.php new file mode 100644 index 0000000..c4bcb5a --- /dev/null +++ b/src/Facades/Admin.php @@ -0,0 +1,18 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace CodeCoz\AimAdmin\Field; + +use function Symfony\Component\String\u; +use CodeCoz\AimAdmin\Dto\CrudBoard\ActionDto; + + +/** + * This class is for field creation in crudboard . + * + * @author Muhammad Abdullah Ibne Masud + */ +final class ButtonField +{ + public const DELETE = 'delete'; + public const DETAIL = 'detail'; + public const EDIT = 'edit'; + public const INDEX = 'index'; + public const NEW = 'new'; + + + private ActionDto $dto; + + private function __construct(ActionDto $actionDto) + { + $this->dto = $actionDto; + } + + public function __toString() + { + return $this->dto->getName(); + } + + /** + * @param string $name + * @param string|null $label + * @param mixed ...$params + * @return self + */ + public static function init(string $name, ?string $label = null, mixed ...$params): self + { + $dto = new ActionDto(); + $dto->setType($params['type'] ?? ActionDto::TYPE_ROW); + $dto->setName($name); + $dto->setValue($name); + $dto->setLabel($label); + $dto->setComponent('aim-admin::crudboard.actions.grid-button'); + switch ($name) { + case self::EDIT : + $dto->setIcon('fa-pen-to-square'); + $dto->setCssClass('btn btn-block btn-sm btn-primary'); + break; + case self::DELETE : + $dto->setIcon('fa-times'); + $dto->setOption('role', ActionDto::GRID_DELETE_ACTION); + $dto->setCssClass('btn btn-block btn-sm btn-danger'); + $dto->setComponent('aim-admin::crudboard.actions.grid-delete-button'); + break; + case self::DETAIL : + $dto->setIcon('fa-file-lines'); + $dto->setCssClass('btn btn-block btn-sm btn-info'); + break; + default: + $dto->setCssClass('btn btn-block btn-sm btn-primary'); + break; + } + isset($params['icon']) && $dto->setIcon($params['icon']); + isset($params['cssClass']) && $dto->setCssClass($params['cssClass']); + $dto->setHtmlElement('a'); + $dto->setHtmlAttribute('title', $label ?? self::humanizeString($name)); + return new self($dto); + } + + public function createAsCrudBoardAction(): self + { + $this->dto->setType(ActionDto::TYPE_CRUD_BOARD); + $this->dto->setCssClass('btn btn-sm btn-primary'); + $this->iconForNew(); + $this->dto->setComponent('aim-admin::crudboard.actions.board-action'); + return $this; + } + + public function iconForNew(): self + { + $this->dto->setIcon('fa-plus'); + return $this; + } + + public function createAsBatchAction(): self + { + $this->dto->setType(ActionDto::TYPE_BATCH); + return $this; + } + + public function createAsFormAction(): self + { + $this->dto->setType(ActionDto::TYPE_FORM); + $this->dto->setCssClass('btn btn-sm btn-secondary'); + $label = $this->dto->getLabel() ?? $this->dto->getHtmlAttributes()->get('title'); + $this->dto->setLabel($label); + $this->dto->setComponent("aim-admin::crudboard.actions.form-action"); + return $this; + } + + public function createAsFilterAction(): self + { + $this->dto->setType(ActionDto::TYPE_FILTER); + $this->dto->setCssClass('btn btn-sm btn-secondary'); + $this->setComponent("aim-admin::crudboard.actions.filter-action"); + return $this; + } + + public function createAsFilterSubmitAction(): self + { + $this->dto->setType(ActionDto::TYPE_FILTER); + $this->dto->setCssClass('btn btn-sm btn-success float-right'); + $this->dto->setHtmlElement('button'); + $this->setHtmlAttributes(['type' => 'submit']) + ->setComponent("aim-admin::crudboard.actions.filter-action"); + return $this; + } + + + public function createAsFormSubmitAction(): self + { + $this->dto->setType(ActionDto::TYPE_FORM); + $this->dto->setHtmlElement('button'); + $this->dto->setCssClass('btn btn-sm float-right btn-primary'); + $label = $this->dto->getLabel() ?? $this->dto->getHtmlAttributes()->get('title'); + $this->dto->setLabel($label); + $this->setHtmlAttributes(['type' => 'submit']) + ->setComponent("aim-admin::crudboard.actions.form-action"); + return $this; + } + + public function createAsShowAction(): self + { + $this->dto->setType(ActionDto::TYPE_SHOW); + $this->dto->setCssClass('btn btn-sm btn-secondary'); + $this->dto->setComponent('aim-admin::crudboard.actions.show-button'); + return $this; + } + + /** + * @param mixed $label Use FALSE to hide the label; use NULL to autogenerate it + */ + public function setLabel(string|false|null $label): self + { + $this->dto->setLabel($label ?? self::humanizeString($this->dto->getName())); + return $this; + } + + public function setIcon(?string $icon): self + { + $this->dto->setIcon($icon); + + return $this; + } + + public function removeCssClass(string $cssClass): self + { + $classStr = $this->dto->getCssClass(); + if ($classStr) { + $classes = explode(' ', $classStr); + $newClasses = array_filter($classes, function ($value) use ($cssClass) { + return (trim($value) != $cssClass); + }); + $this->dto->setCssClass(implode(' ', $newClasses)); + } + return $this; + } + + /** + * If you set your own CSS classes, the default CSS classes are not applied. + * You may want to also add the 'btn' (and 'btn-primary', etc.) classes to make + * your action look like a button. + */ + public function setCssClass(string $cssClass): self + { + $this->dto->setCssClass($cssClass); + return $this; + } + + /** + * If you add a custom CSS class, the default CSS classes are not applied. + * You may want to also add the 'btn' (and 'btn-primary', etc.) classes to make + * your action look like a button. + */ + public function addCssClass(string $cssClass): self + { + $this->dto->setCssClass(trim($this->dto->getCssClass() . ' ' . $cssClass)); + + return $this; + } + + public function displayAsLink(): self + { + $this->dto->setHtmlElement('a'); + return $this; + } + + public function displayAsButton(): self + { + $this->dto->setHtmlElement('button'); + + return $this; + } + + public function setHtmlAttributes(array $attributes): self + { + $this->dto->setHtmlAttributes($attributes); + + return $this; + } + + public function setComponent(string $component): self + { + $this->dto->setComponent($component); + + return $this; + } + + /** + * @param string $routeName + * @param array|\Closure $routeParameters The callable has the signature: function ($row): array + * + * Route parameters can be defined as a callable with the signature: function ($entityInstance): array + * Example: ->linkToRoute('invoice_send', fn (Invoice $entity) => ['uuid' => $entity->getId()]); + * @return ButtonField + */ + public function linkToRoute(string $routeName, array|\Closure $routeParameters = []): self + { + $this->dto->setRouteName($routeName); + $this->dto->setRouteParameters($routeParameters); + return $this; + } + + public function linkToUrl(string|callable $url): self + { + + if (is_callable($url)) { + $url = $url(); + } + + $this->dto->setUrl($url); + + return $this; + } + + public function displayIf(callable $callable): self + { + $this->dto->setDisplayCallable($callable); + + return $this; + } + + public function getDto(): ActionDto + { + // if (null === $this->dto->getLabel() || null === $this->dto->getIcon()) { + // throw new \InvalidArgumentException(sprintf('The label or icon of an action cannot be null at the same time. Either set the label, the icon or both for the "%s" action.', $this->dto->getName())); + // } + return $this->dto; + } + + public function setStyle(string $style): self + { + $this->dto->setHtmlAttribute('style', $style); + return $this; + } + + public function addStyle(string $style): self + { + $this->dto->addHtmlAttributes(['style' => $style]); + return $this; + } + + public function setTitle(string $title): self + { + $this->dto->setHtmlAttribute('title', $title); + return $this; + } + + + private static function humanizeString(string $string): string + { + $uString = u($string); + $upperString = $uString->upper()->toString(); + + // this prevents humanizing all-uppercase labels (e.g. 'UUID' -> 'U u i d') + // and other special labels which look better in uppercase + if ($uString->toString() === $upperString) { + return $upperString; + } + + return $uString + ->replaceMatches('/([A-Z])/', '_$1') + ->replaceMatches('/[_\s]+/', ' ') + ->trim() + ->lower() + ->title(true) + ->toString(); + } + + public function asModal(): self + { + $this->dto->setHtmlAttributes(['data-target' => '#AimAdmin-modal', 'data-toggle' => 'modal']); + $this->dto->setCssClass(trim($this->dto->getCssClass() . ' AimAdmin-modal')); + return $this; + } +} diff --git a/src/Field/ChainSelectField.php b/src/Field/ChainSelectField.php new file mode 100644 index 0000000..041ddf5 --- /dev/null +++ b/src/Field/ChainSelectField.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace CodeCoz\AimAdmin\Field; + +use CodeCoz\AimAdmin\Collection\FieldCollection; + +/** + * This class is for creating chai select field . + * + * @author Muhammad Abdullah Ibne Masud + */ +final class ChainSelectField +{ + use FormFieldTrait; + + public static function init(iterable $fields, string $route, ?string $name = null): self + { + foreach ($fields as $key => $child) { + $prefix = $name ? $name . '-' : ""; + $prefix .= 'chain-'; + if (isset($fields[($key + 1)])) { + $child->setCustomOption('dependant', $prefix . ($key + 1)); + } + $child->setAttribute('data-chain-select', $prefix . $key); + } + $children = FieldCollection::init($fields); + return (new self()) + ->setName($name ?? 'chain') + ->setCssClass('row') + ->setComponent('aim-admin::crudboard.fields.chainselect') + ->setCustomOption('dependant-route', $route) + ->setCustomOption('children', $children); + + } + +} diff --git a/src/Field/CheckBoxField.php b/src/Field/CheckBoxField.php new file mode 100644 index 0000000..fbccd67 --- /dev/null +++ b/src/Field/CheckBoxField.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace CodeCoz\AimAdmin\Field; + + +use CodeCoz\AimAdmin\Contracts\Field\FieldInterface; + + +/** + * This class is for creating text field . + * + * @author Muhammad Abdullah Ibne Masud + */ +final class CheckBoxField implements FieldInterface +{ + use FormFieldTrait; + + public static function init(string $name, ?string $label = null, ...$params): self + { + return (new self()) + ->setName($name) + ->setComponent('aim-admin::crudboard.fields.checkbox') + ->setHtmlElementName('input') + ->setPlaceholder($label ?? self::humanizeString($name)) + ->setInputType('checkbox') + ->setLabel($label ?? self::humanizeString($name)) + ->setAttribute('id', $name); + } + + +} diff --git a/src/Field/ChoiceField.php b/src/Field/ChoiceField.php new file mode 100644 index 0000000..73d0748 --- /dev/null +++ b/src/Field/ChoiceField.php @@ -0,0 +1,69 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace CodeCoz\AimAdmin\Field; + + +use CodeCoz\AimAdmin\Contracts\Field\FieldInterface; + + +/** + * This class is for creating text field . + * + * @author Muhammad Abdullah Ibne Masud + */ +final class ChoiceField implements FieldInterface +{ + final const LIST = 'choiceList'; + final const TYPE = 'choiceType'; + final const EMPTY = 'empty'; + final const SELECTED = 'selected'; + + use FormFieldTrait; + + public static function init(string $name, ?string $label = null, ...$params): self + { + $type = $params[0] ?? ($params[self::TYPE] ?? 'select'); + $choiceList = $params[1] ?? ($params[self::LIST] ?? null); + $empty = $params[2] ?? ($params[self::EMPTY] ?? null); + $selected = $params[3] ?? ($params[self::SELECTED] ?? null); + (null === $choiceList) && throw new \InvalidArgumentException(self::LIST . ':[] is a mandatory parameter'); + return (new self()) + ->setName($name) + ->setComponent('aim-admin::crudboard.fields.choice') + ->setLabel($label ?? self::humanizeString($name)) + ->setCustomOption(self::LIST, $choiceList) + ->setCustomOption(self::EMPTY, $empty) + ->setDefault($selected) + ->setInputType($type); + } + + public function setDefault(mixed $defaultValue): self + { + + if (is_array($defaultValue)) { + $this->setAttribute('multiple', false); + $values = \array_flip($defaultValue); + } else { + $values[$defaultValue] = 1; + } + $this->setCustomOption(self::SELECTED, $values); + return $this; + } + + public function setList(array $list): self + { + $this->setCustomOption(self::LIST, $list); + return $this; + } + + +} diff --git a/src/Field/DateTimeField.php b/src/Field/DateTimeField.php new file mode 100644 index 0000000..322a0e2 --- /dev/null +++ b/src/Field/DateTimeField.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace CodeCoz\AimAdmin\Field; + + +use CodeCoz\AimAdmin\Contracts\Field\FieldInterface; + + +/** + * This class is for field creation in crudboard . + * + * @author Muhammad Abdullah Ibne Masud + */ +final class DateTimeField implements FieldInterface +{ + use FormFieldTrait; + + public static function init(string $name, ?string $label = null, ...$params): self + { + return (new self()) + ->setName($name) + ->setLabel($label ?? self::humanizeString($name)) + ->setComponent('aim-admin::crudboard.fields.datetime') + ->formatValue(fn($value): string => (new \DateTime($value))->format('Y-m-d')); + } + +} diff --git a/src/Field/Field.php b/src/Field/Field.php new file mode 100644 index 0000000..55f899f --- /dev/null +++ b/src/Field/Field.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace CodeCoz\AimAdmin\Field; + + +use CodeCoz\AimAdmin\Contracts\Field\FieldInterface; + + +/** + * This class is for field creation in crudboard . + * + * @author Muhammad Abdullah Ibne Masud + */ + +final class Field implements FieldInterface +{ + use FieldTrait; + + public static function init(string $name, ?string $label = null, ...$params): self + { + return (new self()) + ->setName($name) + ->setLabel($label) + ->setComponent('aim-admin::crudboard.fields.grid_cell'); + } + +} diff --git a/src/Field/FieldGroup.php b/src/Field/FieldGroup.php new file mode 100644 index 0000000..c6bf0a3 --- /dev/null +++ b/src/Field/FieldGroup.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace CodeCoz\AimAdmin\Field; + +use CodeCoz\AimAdmin\Collection\FieldCollection; + +/** + * This class is for creating text field . + * + * @author Muhammad Abdullah Ibne Masud + */ +final class FieldGroup +{ + use FormFieldTrait; + + public static function init(iterable $fields, ?string $name = null, ?string $label = null): self + { + $fields = FieldCollection::init($fields); + + return (new self()) + ->setCssClass('border rounded m-auto row') + ->setComponent('aim-admin::crudboard.fields.group') + ->setName($name ?? 'group') + ->setLabel($label ?? $name ?? 'Group') + ->setCustomOption('fields', $fields); + } + +} diff --git a/src/Field/FieldTrait.php b/src/Field/FieldTrait.php new file mode 100644 index 0000000..9ff01d9 --- /dev/null +++ b/src/Field/FieldTrait.php @@ -0,0 +1,154 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace CodeCoz\AimAdmin\Field; + +use CodeCoz\AimAdmin\Dto\CrudBoard\FieldDto; +use function Symfony\Component\String\u; + +/** + * This is a trait for field . + * + * @author Muhammad Abdullah Ibne Masud + */ +trait FieldTrait +{ + private FieldDto $dto; + + private function __construct() + { + $this->dto = new FieldDto(); + } + + public function setName(string $name): self + { + $this->dto->setName($name); + + return $this; + } + + public function setInputType(string $inputType): self + { + $this->dto->setInputType($inputType); + + return $this; + } + + public function setLabel(string|false|null $label): self + { + $this->dto->setLabel($label ?? self::humanizeString($this->dto->getName())); + + return $this; + } + + public function setFormattedValue($value): self + { + $this->dto->setFormattedValue($value); + + return $this; + } + + public function formatValue(?callable $callable): self + { + $this->dto->setFormatValueCallable($callable); + + return $this; + } + + public function setVirtual(bool $isVirtual): self + { + $this->dto->setVirtual($isVirtual); + + return $this; + } + + public function setDisabled(bool $disabled = true): self + { + $this->dto->setDisable($disabled); + return $this; + } + + public function setComponent(string $component): self + { + $this->dto->setComponent($component); + return $this; + } + + public function setHelp(string $help): self + { + $this->dto->setHelp($help); + return $this; + } + + public function addCssClass(string $cssClass): self + { + $this->dto->setCssClass($this->dto->getCssClass() . ' ' . $cssClass); + return $this; + } + + public function setCssClass(string $cssClass): self + { + $this->dto->setCssClass($cssClass); + return $this; + } + + public function getDto(): FieldDto + { + return $this->dto; + } + + public function setCustomOption(string $optionName, $optionValue): self + { + $this->dto->setCustomOption($optionName, $optionValue); + + return $this; + } + + public function setCustomOptions(array $options): self + { + $this->dto->setCustomOptions($options); + + return $this; + } + + private static function humanizeString(string $string): string + { + $uString = u($string); + $upperString = $uString->upper()->toString(); + + // this prevents humanizing all-uppercase labels (e.g. 'UUID' -> 'U u i d') + // and other special labels which look better in uppercase + if ($uString->toString() === $upperString) { + return $upperString; + } + + return $uString + ->replaceMatches('/([A-Z])/', '_$1') + ->replaceMatches('/[_\s]+/', ' ') + ->trim() + ->lower() + ->title(true) + ->toString(); + } + + public function setStyle(string $style): self + { + $this->dto->setHtmlAttribute('style', $style); + return $this; + } + + public function addStyle(string $style): self + { + $this->dto->addHtmlAttributes(['style' => $style]); + return $this; + } + +} diff --git a/src/Field/FileField.php b/src/Field/FileField.php new file mode 100644 index 0000000..a4578e1 --- /dev/null +++ b/src/Field/FileField.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace CodeCoz\AimAdmin\Field; + + +use CodeCoz\AimAdmin\Contracts\Field\FieldInterface; + + +/** + * This class is for creating text field . + * + * @author Muhammad Abdullah Ibne Masud + */ +final class FileField implements FieldInterface +{ + use FormFieldTrait; + + public static function init(string $name, ?string $label = null, mixed ...$params): self + { + return (new self()) + ->setName($name) + ->setComponent('aim-admin::crudboard.fields.file') + ->setHtmlElementName('input') + ->makeFileType() + ->setPlaceholder($label ?? self::humanizeString($name)) + ->setLabel($label ?? self::humanizeString($name)); + } + +} diff --git a/src/Field/FormField.php b/src/Field/FormField.php new file mode 100644 index 0000000..03cb85f --- /dev/null +++ b/src/Field/FormField.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace CodeCoz\AimAdmin\Field; + + +use CodeCoz\AimAdmin\Contracts\Field\FieldInterface; + + +/** + * This class is for field creation in crudboard . + * + * @author Muhammad Abdullah Ibne Masud + */ + +final class FormField implements FieldInterface +{ + use FieldTrait; + + public static function init(string $name, ?string $label = null, ...$params) : self + { + return (new self()) + ->setName($name) + ->setLabel($label); + } + +} diff --git a/src/Field/FormFieldTrait.php b/src/Field/FormFieldTrait.php new file mode 100644 index 0000000..b6d82c4 --- /dev/null +++ b/src/Field/FormFieldTrait.php @@ -0,0 +1,218 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace CodeCoz\AimAdmin\Field; + + +use CodeCoz\AimAdmin\Dto\CrudBoard\FormFieldDto; +use function Symfony\Component\String\u; + +/** + * This is a trait for field . + * + * @author CodeCoz + */ +trait FormFieldTrait +{ + + private FormFieldDto $dto; + + private function __construct() + { + $this->dto = new FormFieldDto(); + $this->setLayoutClass('col-lg-6'); + } + + public function setName(string $name): self + { + $this->dto->setName($name); + return $this; + } + + public function setHtmlElementName(string $htmlElementName): self + { + $this->dto->setHtmlElement($htmlElementName); + return $this; + } + + public function setLabel(string|false|null $label): self + { + $this->dto->setLabel($label ?? self::humanizeString($this->dto->getName())); + return $this; + } + + public function setFormattedValue($value): self + { + $this->dto->setFormattedValue($value); + return $this; + } + + public function formatValue(?callable $callable): self + { + $this->dto->setFormatValueCallable($callable); + return $this; + } + + public function setDisabled(bool $disabled = true): self + { + $this->dto->setDisable($disabled); + return $this; + } + + public function setReadonly(bool $readonly = true): self + { + $this->dto->setReadonly($readonly); + return $this; + } + + public function setComponent(string $component): self + { + $this->dto->setComponent($component); + return $this; + } + + public function setHelp(string $help): self + { + $this->dto->setHelp($help); + return $this; + } + + public function addCssClass(string $cssClass): self + { + $this->dto->setCssClass($this->dto->getCssClass() . ' ' . $cssClass); + return $this; + } + + public function setCssClass(string $cssClass): self + { + $this->dto->setCssClass($cssClass); + return $this; + } + + public function getDto(): FormFieldDto + { + return $this->dto; + } + + public function setCustomOption(string $optionName, $optionValue): self + { + $this->dto->setCustomOption($optionName, $optionValue); + return $this; + } + + public function setCustomOptions(array $options): self + { + $this->dto->setCustomOptions($options); + return $this; + } + + public function setHtmlAttributes(array $htmlAttributes): self + { + $this->dto->setHtmlAttributes($htmlAttributes); + + return $this; + } + + public function setAttribute(string $name, mixed $value): self + { + $this->dto->setHtmlAttribute($name, $value); + return $this; + } + + public function getHtmlAttributes(string|null $attributeName) + { + return $this->dto->getHtmlAttributes($attributeName); + } + + + private static function humanizeString(string $string): string + { + $uString = u($string); + $upperString = $uString->upper()->toString(); + + // this prevents humanizing all-uppercase labels (e.g. 'UUID' -> 'U u i d') + // and other special labels which look better in uppercase + if ($uString->toString() === $upperString) { + return $upperString; + } + + return $uString + ->replaceMatches('/([A-Z])/', '_$1') + ->replaceMatches('/[_\s]+/', ' ') + ->trim() + ->lower() + ->title(true) + ->toString(); + } + + public function setPlaceholder(string $text): self + { + $this->dto->setHtmlAttribute('placeholder', $text); + + return $this; + } + + public function validate(array|string $rules) + { + $this->dto->setValidationRule($rules); + return $this; + } + + public function setInputType(string $inputType): self + { + $this->dto->setInputType($inputType); + return $this; + + } + + public function getInputType(): ?string + { + return $this->dto->getInputType(); + + } + + public function setDefaultValue(?string $value = null): self + { + $this->dto->setValue($value); + return $this; + } + + public function setLayoutClass(string $class): self + { + $this->dto->setLayoutClass($class); + return $this; + } + + public function makeHiddenType(): self + { + $this->dto->setInputType(FormFieldDto::INPUT_TYPE_HIDDEN); + return $this; + } + + public function makeFileType(): self + { + $this->dto->setInputType(FormFieldDto::INPUT_TYPE_FILE); + return $this; + } + + public function setStyle(string $style): self + { + $this->dto->setHtmlAttribute('style', $style); + return $this; + } + + public function addStyle(string $style): self + { + $this->dto->addHtmlAttributes(['style' => $style]); + return $this; + } + +} diff --git a/src/Field/GroupActionField.php b/src/Field/GroupActionField.php new file mode 100644 index 0000000..114eae5 --- /dev/null +++ b/src/Field/GroupActionField.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace CodeCoz\AimAdmin\Field; + +use function Symfony\Component\String\u; + +use CodeCoz\AimAdmin\Collection\ActionCollection; +use CodeCoz\AimAdmin\Dto\CrudBoard\ActionDto; + + +/** + * This class is for group button creation in crudboard . + * + * @author Muhammad Abdullah Ibne Masud + */ +final class GroupActionField +{ + private ActionDto $dto; + private ActionCollection $children; + + private function __construct(ActionDto $actionDto) + { + $this->dto = $actionDto; + } + + public function __toString() + { + return $this->dto->getName(); + } + + /** + * @param mixed $label Use FALSE to hide the label; use NULL to autogenerate it + * @param string|null $icon The full CSS classes of the FontAwesome icon to render (see https://fontawesome.com/v6/search?m=free) + */ + public static function init(string $name, ?string $label = null, mixed ...$prarms): self + { + $dto = new ActionDto(); + $dto->setName($name); + $dto->setLabel($label); + $dto->setComponent('aim-admin::crudboard.actions.group'); + return new self($dto); + } + +} diff --git a/src/Field/HiddenField.php b/src/Field/HiddenField.php new file mode 100644 index 0000000..92d3008 --- /dev/null +++ b/src/Field/HiddenField.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace CodeCoz\AimAdmin\Field; + + +use CodeCoz\AimAdmin\Contracts\Field\FieldInterface; + + +/** + * This class is for creating text field . + * + * @author Muhammad Abdullah Ibne Masud + */ +final class HiddenField implements FieldInterface +{ + use FormFieldTrait; + + public static function init(string $name, ?string $label = null, ...$params) :self + { + $value = $params[0] ?? ($params['value'] ?? null); + return (new self()) + ->setName($name) + ->setComponent('aim-admin::crudboard.fields.hidden') + ->setDefaultValue($value) + ; + } + + +} diff --git a/src/Field/IdField.php b/src/Field/IdField.php new file mode 100644 index 0000000..a65cb08 --- /dev/null +++ b/src/Field/IdField.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace CodeCoz\AimAdmin\Field; + + +use CodeCoz\AimAdmin\Contracts\Field\FieldInterface; + + +/** + * This class is for creating text field . + * + * @author Muhammad Abdullah Ibne Masud + */ +final class IdField implements FieldInterface +{ + use FormFieldTrait; + + public static function init(string $name, ?string $label = null, mixed ...$params): self + { + return (new self()) + ->setName($name) + ->setComponent('aim-admin::crudboard.fields.hidden') + ->makeHiddenType() + ->setLabel($label ?? self::humanizeString($name)); + } + + +} diff --git a/src/Field/InputField.php b/src/Field/InputField.php new file mode 100644 index 0000000..2c10e1d --- /dev/null +++ b/src/Field/InputField.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace CodeCoz\AimAdmin\Field; + + +use CodeCoz\AimAdmin\Contracts\Field\FieldInterface; + + +/** + * This class is for creating text field . + * + * @author Muhammad Abdullah Ibne Masud + */ +final class InputField implements FieldInterface +{ + use FormFieldTrait; + + public static function init(string $name, ?string $label = null, ...$params): self + { + $type = $params[0] ?? ($params['type'] ?? 'text'); + $value = $params[1] ?? ($params['value'] ?? null); + return (new self()) + ->setName($name) + ->setComponent('aim-admin::crudboard.fields.input') + ->setHtmlElementName('input') + ->setPlaceholder($label ?? self::humanizeString($name)) + ->setInputType($type) + ->setLabel($label ?? self::humanizeString($name)) + ->setDefaultValue($value) + ; + } + + + + +} diff --git a/src/Field/TextField.php b/src/Field/TextField.php new file mode 100644 index 0000000..b27cf32 --- /dev/null +++ b/src/Field/TextField.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace CodeCoz\AimAdmin\Field; + + +use CodeCoz\AimAdmin\Contracts\Field\FieldInterface; + + +/** + * This class is for creating text field . + * + * @author Muhammad Abdullah Ibne Masud + */ +final class TextField implements FieldInterface +{ + use FormFieldTrait; + + public static function init(string $name, ?string $label = null, ...$params): self + { + return (new self()) + ->setName($name) + ->setComponent('aim-admin::crudboard.fields.input') + ->setHtmlElementName('input') + ->setPlaceholder($label ?? self::humanizeString($name)) + ->setInputType('text') + ->setLabel($label ?? self::humanizeString($name)) + ->setAttribute('id', $name); + } + + +} diff --git a/src/Field/TextareaField.php b/src/Field/TextareaField.php new file mode 100644 index 0000000..c27f39c --- /dev/null +++ b/src/Field/TextareaField.php @@ -0,0 +1,54 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace CodeCoz\AimAdmin\Field; + + +use CodeCoz\AimAdmin\Contracts\Field\FieldInterface; + + +/** + * This class is for creating text field . + * + * @author Muhammad Abdullah Ibne Masud + */ +final class TextareaField implements FieldInterface +{ + public final const ROW = 'rows'; + public final const COL = 'cols'; + private static array $defaultParams = [self::ROW => 2, self::COL => null]; + + use FormFieldTrait; + + public static function init(string $name, ?string $label = null, ...$params): self + { + $label = $label ?? self::humanizeString($name); + $finalParams = $params + self::$defaultParams; + return (new self()) + ->setName($name) + ->setComponent('aim-admin::crudboard.fields.textarea') + ->setLabel($label) + ->setCustomOption(self::ROW, $finalParams[self::ROW]) + ->setPlaceholder($label) + ->setAttribute('id', $name); + } + + public function setNumOfRows(int $rows): self + { + if ($rows < 1) { + throw new \InvalidArgumentException(sprintf('The argument of the "%s()" method must be 1 or higher (%d given).', __METHOD__, $rows)); + } + $this->setCustomOption(self::ROW, $rows); + return $this; + } + + +} diff --git a/src/Form/AbstractForm.php b/src/Form/AbstractForm.php new file mode 100644 index 0000000..59dc420 --- /dev/null +++ b/src/Form/AbstractForm.php @@ -0,0 +1,229 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace CodeCoz\AimAdmin\Form; + +use ArrayAccess; +use CodeCoz\AimAdmin\Collection\ActionCollection; +use CodeCoz\AimAdmin\Collection\FormFieldCollection; +use CodeCoz\AimAdmin\Contracts\Field\FieldInterface; +use CodeCoz\AimAdmin\Contracts\Service\CrudBoard\CrudFormHandlerInterface; +use CodeCoz\AimAdmin\Field\IdField; + +/** + * This is an abstract form class for crud board + * + * @author Muhammad Abdullah Ibne Masud + */ +abstract class AbstractForm +{ + const STAT_NEW = 'new'; + const STAT_EDIT = 'edit'; + const STAT_NEW_SUBMIT = 'new_submit'; + const STAT_EDIT_SUBMIT = 'edit_submit'; + private $formStat; + private string $actionUrl; + private string $name; + private array $attributes = []; + private string $cssClass = ''; + private string $method = 'post'; + private FormFieldCollection $fields; + private array $rawFields; + + private CrudFormHandlerInterface $handler; + protected ActionCollection $actions; + private $data; + private ?string $title = null; + + public function addFields(array $fields) + { + $this->fields = FormFieldCollection::init($fields); + $this->rawFields = $fields; + return $this; + } + + public function addField($field) + { + $this->fields->add($field); + } + + function getFields(): FormFieldCollection + { + return $this->fields; + } + + public function setFormHandler(CrudFormHandlerInterface $handler) + { + $this->handler = $handler; + return $this; + } + + public function saveData(array $data) + { + + $this->data = $this->handler->saveFormData($data); + return $this; + } + + public function setName(string $name) + { + $this->name = $name; + return $this; + } + + public function getName(): string + { + return $this->name; + } + + public function setActionUrl(string $url) + { + $this->actionUrl = $url; + return $this; + } + + public function getActionUrl(): string + { + return $this->actionUrl; + } + + public function setCssClass(string $cssClass) + { + $this->cssClass .= ' ' . $cssClass; + return $this; + } + + public function getCssClass(): string + { + return $this->cssClass; + } + + public function getAttributesAsHtml(): string + { + $html = ''; + foreach ($this->attributes as $formAttr => $val) { + $html .= ($formAttr == 'class') ? $this->setCssClass($val) : "$formAttr='$val' "; + }; + return $html; + } + + public function setAttributes(array $attributes) + { + $this->attributes = \array_merge($this->attributes, $attributes); + return $this; + } + + public function getAttributes(): array + { + return $this->attributes; + } + + public function setMethod(string $method) + { + $this->method = $method; + return $this; + } + + public function getMethod(): string + { + return $this->method; + } + + public function setActions(array $actions): static + { + $dtos = []; + foreach ($actions as $action) { + $dto = $action->getDto(); + $dto->isFormAction() && $dtos[] = $dto; + } + $this->actions = ActionCollection::init($dtos); + return $this; + } + + + public function setData(\ArrayAccess $data): static + { + foreach ($this->fields as $name => &$dto) { + $dto->setValue($data[$name]); + } + return $this; + } + + public function setFields(array $fieldUpdates): static + { + $updatedFields = array_reduce($this->rawFields, function ($carry, $field) { + $carry[$field->getDto()->getName()] = $field; + return $carry; + }, []); + + foreach ($fieldUpdates as $name => $updateAction) { + if (isset($updatedFields[$name])) { + if (is_callable($updateAction)) { + $updatedFields[$name] = $updateAction($updatedFields[$name]); + } elseif ($updateAction instanceof FieldInterface) { + // This allows replacing the field with a new type + $updatedFields[$name] = $updateAction; + } + } elseif ($updateAction instanceof FieldInterface) { + $updatedFields[$name] = $updateAction; + } else { + throw new \InvalidArgumentException("Invalid update action for field '$name'"); + } + } + + $this->addFields(array_values($updatedFields)); + + return $this; + } + + + public function addIdField(string $name = 'id'): static + { + $field = IdField::init($name); + $this->addField($field); + return $this; + } + + public function getData() + { + return $this->data; + } + + public function setFormStat(string $stat): static + { + $this->formStat = $stat; + return $this; + } + + public function isNew() + { + return (self::STAT_NEW === $this->formStat); + } + + + public function isEdit() + { + return (self::STAT_EDIT === $this->formStat); + } + + public function setTitle(string $title): self + { + $this->title = $title; + return $this; + } + + public function getTitle(): ?string + { + return $this->title; + } + + +} diff --git a/src/Form/CrudForm.php b/src/Form/CrudForm.php new file mode 100644 index 0000000..2daaf73 --- /dev/null +++ b/src/Form/CrudForm.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace CodeCoz\AimAdmin\Form; + +use Illuminate\Http\Request; +use CodeCoz\AimAdmin\Collection\ActionCollection; + + +/** + * This is an CRUD form class in crud board. Main purpose of this form is to build form to create DB object + * + * @author Muhammad Abdullah Ibne Masud + */ +class CrudForm extends AbstractForm +{ + private Request $request; + + + public function getActions(): ActionCollection + { + return $this->actions->getFormActions(); + } + + + public function processData(Request $request) + { + $this->request = $request; + $this->validate(); + return $this->save(); + } + + private function validate() + { + $rules = $this->prepareValidationRule(); + $this->request->validate($rules); + } + + private function prepareValidationRule(): array + { + $rules = []; + foreach ($this->getFields() as $field) { + $field->getValidationRule() && $rules[$field->getName()] = $field->getValidationRule(); + } + return $rules; + } + + private function save() + { + $data = []; + foreach ($this->getFields() as $field) { + $data[$field->getName()] = $this->request->input($field->getName()); + } + return $this->saveData($data); + } +} diff --git a/src/Guard/AimGuard.php b/src/Guard/AimGuard.php new file mode 100644 index 0000000..73afcca --- /dev/null +++ b/src/Guard/AimGuard.php @@ -0,0 +1,93 @@ +provider = $provider; + $this->request = $request; + $this->session = $session; + } + + public function check(): bool + { + return !is_null($this->user()); + } + + public function guest(): bool + { + return !$this->check(); + } + + public function user() + { + if (isset($this->user)) { + return $this->user; + } + + $user = $this->session->get('user'); + + if (!is_null($user)) { + $this->user = $this->provider->retrieveById($user['id']); + return $this->user; + } + + return null; + } + + + public function id() + { + if ($this->user()) { + return $this->user->getAuthIdentifier(); + } + return null; + } + + public function validate(array $credentials = []) + { + $user = $this->provider->retrieveByCredentials($credentials); + + if ($user && $this->provider->validateCredentials($user, $credentials)) { + $this->session->put('user_id', $user->getAuthIdentifier()); + $this->setUser($user); + return true; + } + + return false; + } + + public function setUser(Authenticatable $user) + { + $this->user = $user; + return $this; + } + + public function hasUser() + { + if ($this->user()) { + return true; + } + return false; + } + + public function logout() + { + $this->user = null; + $this->session->remove('user'); + $this->session->remove('user_id'); + } +} diff --git a/src/Helpers/Helper.php b/src/Helpers/Helper.php new file mode 100644 index 0000000..2b80c43 --- /dev/null +++ b/src/Helpers/Helper.php @@ -0,0 +1,1474 @@ +attributes->get($key); + } + + static function setAttribute($key, $value): void + { + request()->attributes->set($key, $value); + } + + static function requiredPackages(): array + { + return [ + "@fortawesome/fontawesome-free" => "~6.5.2", + "admin-lte" => "~3.2.0", + "chart.js" => "~4.4.3", + "jquery" => "~3.7.1", + "flatpickr" => "~4.6.13", + "sweetalert2" => "~11.11.0", + "jstree" => "~3.3.16", + "laravel-vite-plugin" => "~1.0.4", + "vite-plugin-static-copy" => "~1.0.5", + "sass" => "~1.77.2", + "vite" => "~5.2.11" + ]; + } + + static function badge($badge, $class = 'bg-info'): string + { + return '' . $badge . ''; + } + + static function formatPhoneNumber($phone): string + { + // Remove any non-numeric characters + $phone = preg_replace('/\D/', '', $phone); + // Ensure the remaining string is numeric + if (!is_numeric($phone)) { + return "NO"; + } + // Check if the phone contains a valid prefix followed by 8 digits + $pattern = '/.*(1[3-9]\d{8}).*/'; + if (preg_match($pattern, $phone, $matches)) { + // Return the formatted number with "880" prepended + return "880" . $matches[1]; + } + return "NO"; + } + +} + diff --git a/src/Http/Controllers/Auth/LoginController.php b/src/Http/Controllers/Auth/LoginController.php new file mode 100644 index 0000000..0b0e88a --- /dev/null +++ b/src/Http/Controllers/Auth/LoginController.php @@ -0,0 +1,46 @@ +validated(); + + if (Auth::attempt($credentials)) { + $request->session()->regenerate(); + + return redirect()->intended('dashboard'); + } + + return back()->withErrors([ + 'email' => 'The provided credentials do not match our records.', + ])->onlyInput('email'); + } + + public function logout(Request $request) + { + Auth::logout(); + + $request->session()->invalidate(); + + $request->session()->regenerateToken(); + + return $request->wantsJson() + ? new JsonResponse([], 204) + : redirect('/'); + } +} diff --git a/src/Http/Controllers/Auth/RegistrationController.php b/src/Http/Controllers/Auth/RegistrationController.php new file mode 100644 index 0000000..7d6cbe1 --- /dev/null +++ b/src/Http/Controllers/Auth/RegistrationController.php @@ -0,0 +1,38 @@ +validated(); + + $user = User::create([ + 'name' => $credentials['name'], + 'email' => $credentials['email'], + 'password' => Hash::make($credentials['password']), + ]); + + if ($user) { + Auth::login($user); + return redirect()->intended('dashboard'); + } + + return back()->withErrors([ + 'email' => 'The provided credentials do not match our records.', + ])->onlyInput('email'); + } +} diff --git a/src/Http/Requests/AuthenticateRequest.php b/src/Http/Requests/AuthenticateRequest.php new file mode 100644 index 0000000..a38d9f7 --- /dev/null +++ b/src/Http/Requests/AuthenticateRequest.php @@ -0,0 +1,29 @@ +|string> + */ + public function rules(): array + { + return [ + 'email' => ['required', 'email'], + 'password' => ['required'], + ]; + } +} diff --git a/src/Http/Requests/RegisterRequest.php b/src/Http/Requests/RegisterRequest.php new file mode 100644 index 0000000..d215576 --- /dev/null +++ b/src/Http/Requests/RegisterRequest.php @@ -0,0 +1,31 @@ +|string> + */ + public function rules(): array + { + return [ + 'name' => ['required', 'max:254'], + 'email' => ['required', Rule::unique('users')], + 'password' => ['required', 'string', 'min:6', 'confirmed'] + ]; + } +} diff --git a/src/MenuBuilder/ActiveChecker.php b/src/MenuBuilder/ActiveChecker.php new file mode 100644 index 0000000..08be7ca --- /dev/null +++ b/src/MenuBuilder/ActiveChecker.php @@ -0,0 +1,147 @@ +request = $url->getRequest(); + $this->url = $url; + + // Fill the map with tests. These tests will check if a menu item is + // active or not. + + $this->tests = [ + 'submenu' => [$this, 'containsActive'], + 'active' => [$this, 'isExplicitActive'], + 'href' => [$this, 'checkPattern'], + 'url' => [$this, 'checkPattern'], + ]; + } + + /** + * Checks if a menu item is currently active. Active items will be + * highlighted. + * + * @param mixed $item The menu item to check + * @return bool + */ + public function isActive($item): bool + { + // Return true if any of the verification tests is met. + + foreach ($this->tests as $prop => $testFunc) { + if (isset($item[$prop]) && $testFunc($item[$prop])) { + return true; + } + } + + // Otherwise, returns false. + + return false; + } + + /** + * Checks if an array of items contains an active item. + * + * @param array $items The items to check + * @return bool + */ + protected function containsActive($items): bool + { + foreach ($items as $item) { + if ($this->isActive($item)) { + return true; + } + } + + return false; + } + + /** + * Checks if an item is active by explicit definition of 'active' state. + * + * @param bool|array $activeDef + * @return bool + */ + protected function isExplicitActive($activeDef) + { + // If the active definition is a bool, return it. + + if (is_bool($activeDef)) { + return $activeDef; + } + + // Otherwise, check if any of the url patterns that defines the active + // state matches the requested url. + + foreach ($activeDef as $pattern) { + if ($this->checkPattern($pattern)) { + return true; + } + } + + return false; + } + + /** + * Checks if an url pattern matches the requested url. + * + * @param string $pattern + * @return bool + */ + protected function checkPattern($pattern) + { + // First, check if the pattern is a regular expression. + + if (Str::startsWith($pattern, 'regex:')) { + $regex = Str::substr($pattern, 6); + + return (bool)preg_match($regex, $this->request->path()); + } + + // If pattern is not a regex, check if the requested url matches the + // absolute path to the given pattern. When the pattern uses query + // parameters, compare with the full url request. + + $pattern = preg_replace('@^https?://@', '*', $this->url->to($pattern)); + $request = $this->request->url(); + + if (isset(parse_url($pattern)['query'])) { + $request = $this->request->fullUrl(); + } + + return Str::is(trim($pattern), trim($request)); + } +} diff --git a/src/MenuBuilder/AimAdminMenu.php b/src/MenuBuilder/AimAdminMenu.php new file mode 100644 index 0000000..8be8687 --- /dev/null +++ b/src/MenuBuilder/AimAdminMenu.php @@ -0,0 +1,135 @@ +filters = $filters; + $this->container = $container; + $this->events = $events; + + // Fill the map with filters methods. + + $this->menuFilterMap = [ + 'sidebar' => [$this, 'sidebarFilter'], + ]; + } + + /** + * Get all the menu items, or a specific set of these. + * + * @param string $filterToken Token representing a subset of the menu items + * @return array A set of menu items + */ + public function menu($filterToken = null) + { + if (empty($this->menu)) { + $this->menu = $this->buildMenu(); + } + + // Check for filter token. + + if (isset($this->menuFilterMap[$filterToken])) { + return array_filter( + $this->menu, + $this->menuFilterMap[$filterToken] + ); + } + + // No filter token provided, return the complete menu. + + return $this->menu; + } + + /** + * Build the menu. + * + * @return array The set of menu items + */ + protected function buildMenu() + { + // Create the menu builder instance. + + $builder = new Builder($this->buildFilters()); + + // Dispatch the BuildingMenu event. Listeners of this event will fill + // the menu. + + $this->events->dispatch(new BuildingMenu($builder)); + + // Return the set of menu items. + + return $builder->menu; + } + + /** + * Build the menu filters. + * + * @return array The set of filters that will apply on each menu item + */ + protected function buildFilters() + { + return array_map([$this->container, 'make'], $this->filters); + } + + /** + * Filter method used to get the sidebar menu items. + * + * @param mixed $item A menu item + * @return bool + */ + private function sidebarFilter($item) + { + return SidebarItemHelper::isValidItem($item); + } + +} diff --git a/src/MenuBuilder/Builder.php b/src/MenuBuilder/Builder.php new file mode 100644 index 0000000..65fa704 --- /dev/null +++ b/src/MenuBuilder/Builder.php @@ -0,0 +1,241 @@ +filters = $filters; + } + + /** + * Add new items at the end of the menu. + * + * @param mixed $newItems Items to be added + */ + public function add(...$newItems) + { + $items = $this->transformItems($newItems); + + if (!empty($items)) { + foreach ($items as $item) { + $this->menu[] = $item; + } + } + } + + + /** + * Add new items after a specific menu item. + * + * @param mixed $itemKey The key that represents the specific menu item + * @param mixed $newItems Items to be added + */ + public function addAfter($itemKey, ...$newItems) + { + $this->addItem($itemKey, self::ADD_AFTER, ...$newItems); + } + + /** + * Add new items before a specific menu item. + * + * @param mixed $itemKey The key that represents the specific menu item + * @param mixed $newItems Items to be added + */ + public function addBefore($itemKey, ...$newItems) + { + $this->addItem($itemKey, self::ADD_BEFORE, ...$newItems); + } + + /** + * Add new submenu items inside a specific menu item. + * + * @param mixed $itemKey The key that represents the specific menu item + * @param mixed $newItems Items to be added + */ + public function addIn($itemKey, ...$newItems) + { + $this->addItem($itemKey, self::ADD_INSIDE, ...$newItems); + } + + /** + * Remove a specific menu item. + * + * @param mixed $itemKey The key of the menu item to remove + */ + public function remove($itemKey) + { + // Find the specific menu item. Return if not found. + + if (!($itemPath = $this->findItem($itemKey, $this->menu))) { + return; + } + + // Remove the item. + + Arr::forget($this->menu, implode('.', $itemPath)); + + // Normalize the menu (remove holes in the numeric indexes). + + $holedArrPath = implode('.', array_slice($itemPath, 0, -1)) ?: null; + $holedArr = Arr::get($this->menu, $holedArrPath, $this->menu); + Arr::set($this->menu, $holedArrPath, array_values($holedArr)); + } + + /** + * Check if exists a menu item with the specified key. + * + * @param mixed $itemKey The key of the menu item to check for + * @return bool + */ + public function itemKeyExists($itemKey) + { + return (bool)$this->findItem($itemKey, $this->menu); + } + + /** + * Transform the items by applying the filters. + * + * @param array $items An array with items to be transformed + * @return array Array with the new transformed items + */ + protected function transformItems($items) + { + return array_filter( + array_map([$this, 'applyFilters'], $items), + [MenuItemHelper::class, 'isAllowed'] + ); + } + + /** + * Find a menu item by the item key and return the path to it. + * + * @param mixed $itemKey The key of the item to find + * @param array $items The array to look up for the item + * @return mixed Array with the path sequence, or empty array if not found + */ + protected function findItem($itemKey, $items) + { + // Look up on all the items. + + foreach ($items as $key => $item) { + if (isset($item['key']) && $item['key'] === $itemKey) { + return [$key]; + } elseif (MenuItemHelper::isSubmenu($item)) { + // Do the recursive call to search on submenu. If we found the + // item, merge the path with the current one. + + if ($subPath = $this->findItem($itemKey, $item['submenu'])) { + return array_merge([$key, 'submenu'], $subPath); + } + } + } + + // Return empty array when the item is not found. + + return []; + } + + /** + * Apply all the available filters to a menu item. + * + * @param mixed $item A menu item + * @return mixed A new item with all the filters applied + */ + protected function applyFilters($item) + { + // Filters are only applied to array type menu items. + + if (!is_array($item)) { + return $item; + } + + // If the item is a submenu, transform all the submenu items first. + // These items need to be transformed first because some of the submenu + // filters (like the ActiveFilter) depends on these results. + + if (MenuItemHelper::isSubmenu($item)) { + $item['submenu'] = $this->transformItems($item['submenu']); + } + + // Now, apply all the filters on the item. + + foreach ($this->filters as $filter) { + // If the item is not allowed to be shown, there is no sense to + // continue applying the filters. + + if (!MenuItemHelper::isAllowed($item)) { + return $item; + } + + $item = $filter->transform($item); + } + + return $item; + } + + /** + * Add new items to the menu in a particular place, relative to a + * specific menu item. + * + * @param mixed $itemKey The key that represents the specific menu item + * @param int $where Where to add the new items + * @param mixed $items Items to be added + */ + protected function addItem($itemKey, $where, ...$items) + { + // Find the specific menu item. Return if not found. + + if (!($itemPath = $this->findItem($itemKey, $this->menu))) { + return; + } + + // Get the target array and add the new items there. + + $itemKeyIdx = end($itemPath); + reset($itemPath); + + if ($where === self::ADD_INSIDE) { + $targetPath = implode('.', array_merge($itemPath, ['submenu'])); + $targetArr = Arr::get($this->menu, $targetPath, []); + array_push($targetArr, ...$items); + } else { + $targetPath = implode('.', array_slice($itemPath, 0, -1)) ?: null; + $targetArr = Arr::get($this->menu, $targetPath, $this->menu); + $offset = ($where === self::ADD_AFTER) ? 1 : 0; + array_splice($targetArr, $itemKeyIdx + $offset, 0, $items); + } + + Arr::set($this->menu, $targetPath, $targetArr); + + // Apply the filters because the menu now have new items. + + $this->menu = $this->transformItems($this->menu); + } +} diff --git a/src/MenuBuilder/BuildingMenu.php b/src/MenuBuilder/BuildingMenu.php new file mode 100644 index 0000000..6b5e3f0 --- /dev/null +++ b/src/MenuBuilder/BuildingMenu.php @@ -0,0 +1,23 @@ +menu = $menu; + } +} diff --git a/src/MenuBuilder/Filters/ActiveFilter.php b/src/MenuBuilder/Filters/ActiveFilter.php new file mode 100644 index 0000000..895d8a1 --- /dev/null +++ b/src/MenuBuilder/Filters/ActiveFilter.php @@ -0,0 +1,38 @@ +activeChecker = $activeChecker; + } + + /** + * Transforms a menu item. Adds the active attribute when suitable. + * + * @param array $item A menu item + * @return array The transformed menu item + */ + public function transform($item): array + { + $item['active'] = $this->activeChecker->isActive($item); + + return $item; + } +} diff --git a/src/MenuBuilder/Filters/ClassesFilter.php b/src/MenuBuilder/Filters/ClassesFilter.php new file mode 100644 index 0000000..b49356b --- /dev/null +++ b/src/MenuBuilder/Filters/ClassesFilter.php @@ -0,0 +1,72 @@ +makeClasses($item)); + + if (MenuItemHelper::isSubmenu($item)) { + $item['submenu_class'] = implode(' ', $this->makeSubmenuClasses($item)); + } + + return $item; + } + + /** + * Make classes related to the components of a menu item. + * + * @param array $item A menu item + * @return array The array of classes + */ + protected function makeClasses($item) + { + $classes = []; + + // Add custom classes (from menu item configuration). + + if (!empty($item['classes'])) { + $classes[] = $item['classes']; + } + + // Add the active class when the item is active. + + if (!empty($item['active'])) { + $classes[] = 'active'; + } + + return $classes; + } + + /** + * Make classes related to the components of a submenu item. + * + * @param array $item A menu item + * @return array The array of classes + */ + protected function makeSubmenuClasses($item) + { + $classes = []; + + // Add the menu-open class when a sidebar submenu is active. Note we + // need to add the class to sidebar submenu items only. + + if (SidebarItemHelper::isValidItem($item) && $item['active']) { + $classes[] = 'menu-open'; + } + + return $classes; + } +} diff --git a/src/MenuBuilder/Filters/FilterInterface.php b/src/MenuBuilder/Filters/FilterInterface.php new file mode 100644 index 0000000..38e7e0c --- /dev/null +++ b/src/MenuBuilder/Filters/FilterInterface.php @@ -0,0 +1,14 @@ +urlGenerator = $urlGenerator; + } + + /** + * Transforms a menu item. Make the href attribute when situable. + * + * @param array $item A menu item + * @return array The transformed menu item + */ + public function transform($item) + { + if (!MenuItemHelper::isHeader($item)) { + $item['href'] = $this->makeHref($item); + } + + return $item; + } + + /** + * Make the href attribute for a menu item. + * + * @param array $item A menu item + * @return string The href attribute + */ + protected function makeHref($item) + { + // If url attribute is available, use it to make the href. + + if (isset($item['url'])) { + return $this->urlGenerator->to($item['url']); + } + + // When url is not available, check for route attribute. + + if (isset($item['route'])) { + if (is_array($item['route'])) { + $route = $item['route'][0]; + $params = is_array($item['route'][1]) ? $item['route'][1] : []; + + return $this->urlGenerator->route($route, $params); + } + + return $this->urlGenerator->route($item['route']); + } + + // When no url or route, return a default value. + + return '#'; + } +} diff --git a/src/MenuBuilder/MenuItemHelper.php b/src/MenuBuilder/MenuItemHelper.php new file mode 100644 index 0000000..6ba9cd8 --- /dev/null +++ b/src/MenuBuilder/MenuItemHelper.php @@ -0,0 +1,59 @@ + + */ + protected $hidden = [ + 'password', + 'remember_token', + ]; + + + /** + * Get the attributes that should be cast. + * + * @return array + */ + protected function casts(): array + { + return [ + 'email_verified_at' => 'datetime', + 'password' => 'hashed', + ]; + } + + public function userImage(): ?string + { + return ""; + } + +} diff --git a/src/Providers/MenuServiceProvider.php b/src/Providers/MenuServiceProvider.php new file mode 100644 index 0000000..47e099e --- /dev/null +++ b/src/Providers/MenuServiceProvider.php @@ -0,0 +1,63 @@ +app['config']['aim-admin'])) { + // Merge the package's default configuration if it doesn't exist + $this->mergeConfigFrom( + Admin::packagePath('config/aim-admin.php'), 'aim-admin' + ); + } + + // Register the service as a singleton + $this->app->singleton(AimAdminMenu::class, function ($app) { + $config = $app['config']->get('aim-admin.menu_filters', []); + $events = $app['events']; + + return new AimAdminMenu($config, $events, $app); + }); + } + + + /** + * Bootstrap the package's services. + * + * @return void + */ + public function boot(Dispatcher $events, Repository $config) + { + $this->registerMenu($events, $config); + } + + /** + * Register the menu events handlers. + * + * @return void + */ + private function registerMenu(Dispatcher $events, Repository $config) + { + $events->listen(BuildingMenu::class, function (BuildingMenu $event) use ($config) { + $menu = $config->get('aim-admin.menu', []); + $menu = is_array($menu) ? $menu : []; + $event->menu->add(...$menu); + }); + } + +} diff --git a/src/Providers/RepositoryServiceProvider.php b/src/Providers/RepositoryServiceProvider.php new file mode 100644 index 0000000..c003054 --- /dev/null +++ b/src/Providers/RepositoryServiceProvider.php @@ -0,0 +1,83 @@ +isDirectory(app_path('Contracts/Services'))) { + $this->autoBindInterfaces(app_path('Contracts/Services'), app_path('Services')); + } + if ($filesystem->isDirectory(app_path('Contracts/Repositories'))) { + $this->autoBindInterfaces(app_path('Contracts/Repositories'), app_path('Repositories')); + } + }); + } + + + /** + * @param $contractsPath + * @param $implementationPath + * @return void + */ + private function autoBindInterfaces($contractsPath, $implementationPath): void + { + $files = app(Filesystem::class)->allFiles($contractsPath); + + foreach ($files as $file) { + + $contractNamespace = $this->extractNamespace($file->getPathname()); + + $contractClass = $contractNamespace . "\\" . $file->getFilenameWithoutExtension(); + + // Ensure class existence and interface nature + if (!interface_exists($contractClass)) { + continue; // Skip if not a valid interface + } + + $implementationClass = $this->getImplementationClass($contractClass); + + if (class_exists($implementationClass)) { + $this->app->bind($contractClass, $implementationClass); + } + + } + } + + private function getImplementationClass($contractClass): array|string|null + { + // Remove the 'Contracts' namespace segment and 'Interface' suffix + $implementationClass = str_replace('Contracts\\', '', $contractClass); + return preg_replace('/Interface$/', '', $implementationClass); + } + + private function extractNamespace(string $filePath): string + { + // Optimized namespace extraction using a single regular expression + $contents = file_get_contents($filePath); + preg_match('/namespace\s+(.*);/', $contents, $matches); + return trim($matches[1]) ?? ''; + } + + /** + * Bootstrap services. + * + * @return void + */ + public function boot() + { + // + } +} diff --git a/src/Providers/UserServiceProvider.php b/src/Providers/UserServiceProvider.php new file mode 100644 index 0000000..e3c8264 --- /dev/null +++ b/src/Providers/UserServiceProvider.php @@ -0,0 +1,71 @@ +registerUserProviderDriver(); + $this->registerGuard(); + } + + /** + * Register the User Provider handlers. + * + * @return void + */ + private function registerUserProviderDriver(): void + { + Auth::provider('AimAdminUserProviderDriver', function ($app, array $config) { + return new AimAdminUserProviderDriver(); + }); + +// Overriding the default users driver + config([ + 'auth.providers.users.driver' => env('USER_PROVIDER_DRIVER', 'AimAdminUserProviderDriver') + ]); + } + + /** + * Register the Guard handlers. + * + * @return void + */ + private function registerGuard(): void + { + Auth::extend('AimAdminGuard', function ($app, $config) { + return new AimGuard( + Auth::createUserProvider('users'), + $app['request'], + $app['session.store']); + }); + +// Overriding the default guard configuration + config([ + 'auth.guards.web.driver' => env('DEFAULT_GUARD', 'AimAdminGuard') + ]); + } + +} diff --git a/src/Providers/ViewServiceProvider.php b/src/Providers/ViewServiceProvider.php new file mode 100644 index 0000000..34c3885 --- /dev/null +++ b/src/Providers/ViewServiceProvider.php @@ -0,0 +1,39 @@ +exists($file)) { + $content .= view($file)->render(); + } else { + Log::warning("JS file not found: {$file}"); // Log if file is missing + } + } + return $this->with('injectedTop', $content); + }); + + // Macro for injecting CSS + View::macro('injectBottom', function ($files) { + $content = ''; + foreach ((array)$files as $file) { + if (view()->exists($file)) { + $content .= view($file)->render(); + } else { + Log::warning("CSS file not found: {$file}"); // Log if file is missing + } + } + return $this->with('injectedBottom', $content); + }); + } +} diff --git a/src/Repository/AbstractAimAdminRepository.php b/src/Repository/AbstractAimAdminRepository.php new file mode 100644 index 0000000..c4b886a --- /dev/null +++ b/src/Repository/AbstractAimAdminRepository.php @@ -0,0 +1,120 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace CodeCoz\AimAdmin\Repository; + +use CodeCoz\AimAdmin\Contracts\Repository\AimAdminRepositoryInterface; +use CodeCoz\AimAdmin\Contracts\Service\CrudBoard\CrudFormHandlerInterface; +use CodeCoz\AimAdmin\Contracts\Service\CrudBoard\CrudGridLoaderInterface; +use Illuminate\Pagination\CursorPaginator; +use Illuminate\Pagination\Paginator; +use Illuminate\Pagination\LengthAwarePaginator; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Support\Collection; + +/** + * This interface defines blueprints of AimAdmin Repository. + * + * + * @author CodeCoz + */ +abstract class AbstractAimAdminRepository implements AimAdminRepositoryInterface, CrudFormHandlerInterface, CrudGridLoaderInterface +{ + public function all(): iterable + { + return $this->getModelFqcn()::all(); + } + + public function findOrFail(int|string $id): object + { + return $this->getModelFqcn()::findOrFail($id); + } + + public function find($id) + { + return $this->getModelFqcn()::find($id); + } + + public function findOneBy(string $field, $value) + { + return $this->getModelFqcn()::where($field, $value)->first(); + } + + protected function findBy($field, $value): iterable + { + return $this->getModelFqcn()::where($field, $value)->get(); + } + + public function saveFormData(array $data = []) + { + $class = $this->getModelFqcn(); + $model = new $class; + foreach ($data as $key => $value) { + $model->$key = $value; + } + $model->save(); + return $model; + } + + public function crudShow(int|string $id): ?\ArrayAccess + { + return $this->find($id); + } + + public function getRecordForEdit(int|string $id): object + { + $record = $this->findOrFail($id); + return $record; + } + + public function getGridData(array $filters = []): ?iterable + { + return null; + } + + public function getGridQuery(): ?Builder + { + return null; + } + + public function getGridPagination(): ?Paginator + { + return null; + } + + public function getGridCursorPaginator(array $filters): ?CursorPaginator + { + return null; + } + + public function getGridPaginator(array $filters): ?LengthAwarePaginator + { + return null; + } + + public function applyFilterQuery(Builder $query, array $filters): Builder + { + foreach ($filters as $field => $value) { + $query->where($field, $value); + } + return $query; + } + + public function applyFilterData(Collection $data, array $filters): Collection + { + foreach ($filters as $field => $value) { + $filtered = $data->where($field, $value); + $data = $filtered; + } + return $data; + } + +} diff --git a/src/Services/CrudBoard/AbstractCrudBoard.php b/src/Services/CrudBoard/AbstractCrudBoard.php new file mode 100644 index 0000000..f964d61 --- /dev/null +++ b/src/Services/CrudBoard/AbstractCrudBoard.php @@ -0,0 +1,88 @@ +grid = CrudGrid::init($dataLoader, $params); + return $this->grid; + } + + public function getRepository(): AimAdminRepositoryInterface + { + return $this->repo; + } + + public function getGrid(): CrudGridInterface + { + return $this->grid; + } + + public function addGridColumns(array $columns) + { + $this->grid->addColumns($columns); + return $this; + } + + public function getForm(): AbstractForm + { + return $this->form; + } + + + public function addGridActions(array $actions = []) + { + $this->grid->addActions($actions); + return $this; + } + + protected function defaultGridRowActions() + { + + } + + public function createForm(array $fields): CrudForm + { + $this->form = (new CrudForm()) + ->addFields($fields); + return $this->form; + } + + /** + * @throws \Exception + */ + public function createShow(mixed $row, array $fields) + { + $record = ($row instanceof Model) ? $row :$this->getRecordForShow($row); + if ($record === null) { + throw new \Exception("No record is found for details"); + } + $this->crudShow = CrudShow::init($fields, $record); + return $this->crudShow; + } + + public function getCrudShow(): CrudShowInterface + { + return $this->crudShow; + } + +} diff --git a/src/Services/CrudBoard/CrudBoard.php b/src/Services/CrudBoard/CrudBoard.php new file mode 100644 index 0000000..4074243 --- /dev/null +++ b/src/Services/CrudBoard/CrudBoard.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace CodeCoz\AimAdmin\Services\CrudBoard; + + +/** + * This interface defines blueprints of AimAdmin CRUD grid loader. + * It will ensure to provide record for CrudBoard grid. + * + * @author Muhammad Abdullah Ibne Masud + */ + + use CodeCoz\AimAdmin\Contracts\Repository\AimAdminRepositoryInterface; + +final class CrudBoard extends AbstractCrudBoard +{ + private $params = []; + public function setParam($param) + { + $this->params[] = $param; + } + + public function getParams() : array + { + return $this->params; + } + + public function setRepository(AimAdminRepositoryInterface $repo) : self + { + $this->repo = $repo; + return $this; + } + + protected function getRecordForShow(int|string $id): ?\ArrayAccess + { + return $this->getRepository()->crudShow($id); + } +} diff --git a/src/Services/CrudBoard/CrudForm.php b/src/Services/CrudBoard/CrudForm.php new file mode 100644 index 0000000..3543f7b --- /dev/null +++ b/src/Services/CrudBoard/CrudForm.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace CodeCoz\AimAdmin\Services\CrudBoard; + +use Illuminate\Http\Request; +use CodeCoz\AimAdmin\Collection\ActionCollection; +use CodeCoz\AimAdmin\Form\AbstractForm; + +/** + * This is an CRUD form class in crud board. Main purpose of this form is to build form to create DB object + * + * @author Muhammad Abdullah Ibne Masud + */ +class CrudForm extends AbstractForm +{ + private Request $request; + + + public function getActions(): ActionCollection + { + return $this->actions->getFormActions(); + } + + + public function processData(Request $request) + { + $this->request = $request; + $this->validate(); + return $this->save(); + } + + private function validate() + { + $rules = $this->prepareValidationRule(); + $this->request->validate($rules); + } + + private function prepareValidationRule(): array + { + $rules = []; + foreach ($this->getFields() as $field) { + $field->getValidationRule() && $rules[$field->getName()] = $field->getValidationRule(); + } + return $rules; + } + + private function save() + { + $data = []; + foreach ($this->getFields() as $field) { + $data[$field->getName()] = $this->request->input($field->getName()); + } + return $this->saveData($data); + } +} diff --git a/src/Services/CrudBoard/CrudGrid.php b/src/Services/CrudBoard/CrudGrid.php new file mode 100644 index 0000000..252685e --- /dev/null +++ b/src/Services/CrudBoard/CrudGrid.php @@ -0,0 +1,196 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace CodeCoz\AimAdmin\Services\CrudBoard; + +use CodeCoz\AimAdmin\Collection\ActionCollection; +use CodeCoz\AimAdmin\Collection\FieldCollection; +use CodeCoz\AimAdmin\Contracts\Service\CrudBoard\CrudGridInterface; +use CodeCoz\AimAdmin\Contracts\Service\CrudBoard\CrudGridLoaderInterface; +use Illuminate\Support\Facades\Request; +use Illuminate\Support\Collection; +use Illuminate\Pagination\LengthAwarePaginator; +use CodeCoz\AimAdmin\Dto\CrudBoard\FieldDto; +use CodeCoz\AimAdmin\Field\Field; + +/** + * This class implement AimAdmin CRUD grid Interface . + * It is responsible to generate grid/table view. + * + * @author Muhammad Abdullah Ibne Masud + */ +final class CrudGrid implements CrudGridInterface +{ + private ?string $title = null; + private ?bool $disableSerialColumn = false; + private FieldCollection $fields; + private ActionCollection $actions; + private GridFilter $filter; + private GridPaginator $paginator; + private ?LengthAwarePaginator $gridData = null; + private array $config = [ + 'actionHeader' => 'Action', + 'rowCssCallable' => null, + 'rowCssClassCallable' => null, + 'headerRowCssClass' => null, + 'tableCssClass' => 'table-bordered table-hover text-nowrap table-sm', + ]; + + private function __construct(private CrudGridLoaderInterface $gridDataLoader, array $params) + { + $this->filter = new GridFilter(); + $this->paginator = new GridPaginator($params['pagination'] ?? 10); + $this->config = ($params['config'] ?? []) + $this->config; + } + + public static function init(CrudGridLoaderInterface $gridDataLoader, array $params): CrudGridInterface + { + return new self($gridDataLoader, $params); + } + + public function setTitle(string $title): self + { + $this->title = $title; + return $this; + } + + public function getTitle(): ?string + { + return $this->title; + } + + public function getFilter(): GridFilter + { + return $this->filter; + } + + function getGridDataLoader(): CrudGridLoaderInterface + { + return $this->gridDataLoader; + } + + public function addColumns(array $columns): self + { + $this->fields = FieldCollection::init($columns); + return $this; + } + + public function addActions(array $actions = []) + { + $dtos = []; + foreach ($actions as $name => $action) { + $dtos[$name] = $action->getDto(); + } + $this->actions = ActionCollection::init($dtos); + return $this; + } + + public function getColumns(): FieldCollection + { + return $this->fields; + } + + public function getGridData(): LengthAwarePaginator + { + if (!$this->gridData) { + $filters = $this->filter->getData(); + $queryBuilder = $this->gridDataLoader->getGridQuery(); + if (null !== $queryBuilder) { + $queryBuilder = $this->gridDataLoader->applyFilterQuery($queryBuilder, $filters); + $paginator = $this->paginator->paginateQuery($queryBuilder); + } elseif ($gridData = $this->gridDataLoader->getGridData($filters)) { + $dataCollection = $gridData instanceof Collection ? $gridData : collect($gridData); + $dataCollection = $this->gridDataLoader->applyFilterData($dataCollection, $filters); + $paginator = $this->paginator->paginate($dataCollection); + $paginator->withPath(Request::url()); + } else { + ($paginator = $this->gridDataLoader->getGridCursorPaginator($filters)) + || ($paginator = $this->gridDataLoader->getGridPaginator($filters)); + } + $this->gridData = $paginator ? $paginator->withQueryString() : $this->paginator->paginate(collect([])); + } + return $this->gridData; + } + + public function getActions(): ActionCollection + { + return $this->actions->getCrudBoardActions(); + } + + public function getRowActions(): ActionCollection + { + return $this->actions->getRowActions(); + } + + public function getActionLevel(): string + { + return $this->config['actionHeader']; + } + + public function getRowCssClass(mixed $row): ?string + { + $cssClass = null; + $this->config['rowCssClassCallable'] && + is_callable($this->config['rowCssClassCallable']) && + $cssClass = call_user_func($this->config['rowCssClassCallable'], $row); + return $cssClass; + } + + public function getRowCss(mixed $row): ?string + { + $css = null; + $this->config['rowCssCallable'] && + is_callable($this->config['rowCssCallable']) && + $css = call_user_func($this->config['rowCssCallable'], $row); + return $css; + } + + public function getHeaderRowCssClass(): ?string + { + return $this->config['headerRowCssClass']; + } + + public function getTableCssClass(): ?string + { + return $this->config['tableCssClass']; + } + + public function isDisableSerialColumn(): ?bool + { + return $this->disableSerialColumn; + } + + public function disableSerialColumn(bool $disable = true): void + { + $this->disableSerialColumn = $disable; + } + + public function enableIdCheckBox(FieldDto $fieldDto = null): void + { + $this->disableSerialColumn(); + + $field = Field::init('id')->setLabel(" +
+ + +
+ ")->setComponent('aim-admin::field.id') + ->getDto(); + + // Use the provided FieldDto if given, otherwise use the one created above + $fieldDto = $fieldDto ?? $field; + + $this->fields->prepend($fieldDto); + } + +} diff --git a/src/Services/CrudBoard/CrudShow.php b/src/Services/CrudBoard/CrudShow.php new file mode 100644 index 0000000..1bc6678 --- /dev/null +++ b/src/Services/CrudBoard/CrudShow.php @@ -0,0 +1,85 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace CodeCoz\AimAdmin\Services\CrudBoard; + +use CodeCoz\AimAdmin\Collection\ActionCollection; +use CodeCoz\AimAdmin\Contracts\Service\CrudBoard\CrudShowInterface; +use CodeCoz\AimAdmin\Collection\FieldCollection; + +class CrudShow implements CrudShowInterface +{ + private FieldCollection $fields; + private ActionCollection $actions; + private ?string $title = null; + + private function __construct( array $fields, private \ArrayAccess $record) + { + $this->addFields($fields) + ->prepareRecord($record); + + } + + public static function init(array $fields, \ArrayAccess $record) + { + return new self($fields,$record); + } + + public function addFields($fields) + { + $this->fields = FieldCollection::init($fields); + return $this; + } + + public function addActions(array $actions=[]) + { + $dtos = []; + foreach($actions as $name => $action){ + $dtos[$name] = $action->getDto(); + } + $this->actions = ActionCollection::init($dtos); + return $this; + } + + public function getActions() : ActionCollection + { + return $this->actions; + } + + public function getFields(): FieldCollection + { + return $this->fields; + } + + private function prepareRecord($record) + { + foreach($this->fields as $name=>&$field) + { + $field->setValue($record[$name]); + } + } + + public function getRecord() : \ArrayAccess + { + return $this->record; + } + + public function setTitle(string $title) : self + { + $this->title = $title; + return $this; + } + + public function getTitle() : ?string + { + return $this->title; + } +} diff --git a/src/Services/CrudBoard/GridFilter.php b/src/Services/CrudBoard/GridFilter.php new file mode 100644 index 0000000..3296c4c --- /dev/null +++ b/src/Services/CrudBoard/GridFilter.php @@ -0,0 +1,76 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace CodeCoz\AimAdmin\Services\CrudBoard; + +use CodeCoz\AimAdmin\Form\AbstractForm; +use CodeCoz\AimAdmin\Collection\ActionCollection; +use Illuminate\Support\Facades\Request; +use CodeCoz\AimAdmin\Field\ButtonField; + + /** + * This is an abstract form class for crud board + * + * @author Muhammad Abdullah Ibne Masud + */ + + class GridFilter extends AbstractForm + { + const CONTAINER_NAME = 'filters'; + + public function getActions() : ActionCollection + { + return $this->actions; + } + + public function assignQueryData() : self + { + $queryData = Request::get(self::CONTAINER_NAME,[]); + // dd($queryData); + $fields = $this->getFields(); + foreach($queryData as $field=>$value){ + if($fields->offsetExists($field)) { + $value && $fields->get($field)->setValue($value); + } + } + return $this; + } + + public function getData(): array + { + $data = Request::get(self::CONTAINER_NAME,[]); + return array_filter($data); + } + + public function setActions(array $actions) : static + { + $dtos = []; + foreach($actions as $action) { + $dto = $action->getDto(); + $dto->isFilterAction() && $dtos[] = $dto; + } + if(empty($dtos)){ + $actions = [ + ButtonField::init('Search','Search')->createAsFilterSubmitAction() + ->setIcon('fa-search'), + ButtonField::init('reset','Reset')->createAsFilterAction() + ->displayAsButton()->setHtmlAttributes(['type'=>'reset']), + ]; + foreach($actions as $action) { + $dto = $action->getDto(); + $dto->isFilterAction() && $dtos[] = $dto; + } + } + $this->actions = ActionCollection::init($dtos); + return $this; + } + + + } diff --git a/src/Services/CrudBoard/GridPaginator.php b/src/Services/CrudBoard/GridPaginator.php new file mode 100644 index 0000000..1067bed --- /dev/null +++ b/src/Services/CrudBoard/GridPaginator.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace CodeCoz\AimAdmin\Services\CrudBoard; + +use Illuminate\Pagination\Paginator; +use Illuminate\Pagination\LengthAwarePaginator; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Support\Collection; + +/** + * This is an abstract form class for crud board + * + * @author Muhammad Abdullah Ibne Masud + */ +class GridPaginator +{ + + public function __construct(private readonly int $recordPerPage = 3) + { + } + + public function paginate(Collection $data, $options = []): LengthAwarePaginator + { + $page = Paginator::resolveCurrentPage(); + return new LengthAwarePaginator($data->forPage($page, $this->recordPerPage), $data->count(), $this->recordPerPage, $page, $options); + } + + public function paginateQuery(Builder $queryBuilder): LengthAwarePaginator + { + return $queryBuilder->paginate($this->recordPerPage); + } + +} diff --git a/src/Support/Facades/CrudBoardFacade.php b/src/Support/Facades/CrudBoardFacade.php new file mode 100644 index 0000000..43d4d17 --- /dev/null +++ b/src/Support/Facades/CrudBoardFacade.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace CodeCoz\AimAdmin\Support\Facades; + +use Illuminate\Support\Facades\Facade; +use CodeCoz\AimAdmin\Contracts\Service\CrudBoard\CrudBoardInterface; + +/** + * This is a facade class for crud grid + * + * @author CodeCoz + */ +class CrudBoardFacade extends Facade +{ + /** + * Get the registered name of the component. + * + * @return string + */ + protected static function getFacadeAccessor(): string + { + return CrudBoardInterface::class; + } +} diff --git a/src/Support/OptionValueStore.php b/src/Support/OptionValueStore.php new file mode 100644 index 0000000..f6e97c7 --- /dev/null +++ b/src/Support/OptionValueStore.php @@ -0,0 +1,178 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace CodeCoz\AimAdmin\Support; + +use Countable; + +final class OptionValueStore implements Countable +{ + private array $container = []; + + protected string $delimiter = '.'; + + private function __construct(array $options, $delimiter) + { + $this->container = $options; + $this->delimiter = $delimiter; + } + + public static function init(array $options = [], $delimiter = '.'): self + { + return new self($options, $delimiter); + } + + public function all(): array + { + return $this->container; + } + + public function offsetSet($offset, $value): void + { + if (is_null($offset)) { + $this->container[] = $value; + } else { + $this->container[$offset] = $value; + } + } + + protected function exists($array, $key): bool + { + return \array_key_exists($key, $array); + } + + public function count(): int + { + return \count($this->all()); + } + + public function isEmpty(): bool + { + return 0 === $this->count(); + } + + public function has($keys): bool + { + $keys = (array)$keys; + + if (!$this->container || $keys === []) { + return false; + } + + foreach ($keys as $key) { + $items = $this->container; + + if ($this->exists($items, $key)) { + continue; + } + + foreach (explode($this->delimiter, $key) as $segment) { + if (!is_array($items) || !$this->exists($items, $segment)) { + return false; + } + + $items = $items[$segment]; + } + } + + return true; + } + + public function get($key = null, $default = null) + { + if (\is_null($key)) { + return $this->container; + } + + if ($this->exists($this->container, $key)) { + return $this->container[$key]; + } + + if (!\str_contains($key, $this->delimiter)) { + return $default; + } + + $items = $this->container; + + foreach (\explode($this->delimiter, $key) as $segment) { + if (!\is_array($items) || !$this->exists($items, $segment)) { + return $default; + } + + $items = &$items[$segment]; + } + + return $items; + } + + public function set($keys, $value = null): void + { + if (\is_array($keys)) { + foreach ($keys as $key => $value) { + $this->set($key, $value); + } + + return; + } + + $items = &$this->container; + + foreach (\explode($this->delimiter, $keys) as $key) { + if (!isset($items[$key]) || !\is_array($items[$key])) { + $items[$key] = []; + } + + $items = &$items[$key]; + } + + $items = $value; + } + + public function setIfNotSet(string $key, mixed $value): void + { + if (!$this->has($key)) { + $this->set($key, $value); + } + } + + public function add($keys, $value = null) + { + if (is_array($keys)) { + foreach ($keys as $key => $value) { + $this->add($key, $value); + } + } elseif ($this->get($keys) === null) { + $this->set($keys, $value); + } + + return $this; + } + + public function keyValueExists($key, $value): bool + { + $keyValues = $this->get($key); + if ($keyValues) { + return is_array($keyValues) ? \in_array($value, $keyValues) : ($keyValues == $value); + } + return false; + } + + public function renderAsHtmlAttributes(): string + { + $html = ""; + foreach ($this->all() as $key => $value) { + $html .= "$key='$value'"; + } + return $html; + } + + +} diff --git a/src/View/Components/AbstractAimAdminComponent.php b/src/View/Components/AbstractAimAdminComponent.php new file mode 100644 index 0000000..3559fa4 --- /dev/null +++ b/src/View/Components/AbstractAimAdminComponent.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace CodeCoz\AimAdmin\View\Components; + +use Illuminate\View\Component; +use CodeCoz\AimAdmin\Contracts\Service\CrudBoard\CrudBoardInterface; + + +/** + * this is a base component class of AimAdmin platform. + * + * @author CodeCoz + */ +abstract class AbstractAimAdminComponent extends Component +{ + + public function __construct(private CrudBoardInterface $crudBoard) + { + } + + public function getCrudBoard(): CrudBoardInterface + { + return $this->crudBoard; + } + +} diff --git a/src/View/Components/CrudForm.php b/src/View/Components/CrudForm.php new file mode 100644 index 0000000..5281db9 --- /dev/null +++ b/src/View/Components/CrudForm.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace CodeCoz\AimAdmin\View\Components; + +use CodeCoz\AimAdmin\Form\CrudForm as Form; + +/** + * this is a component class responsible for Crud Grid view + * + * @author CodeCoz + */ +class CrudForm extends AbstractAimAdminComponent +{ + public Form $form; + + /** + * Get the view / contents that represent the component. + * + * @return \Illuminate\View\View|\Closure|string + */ + public function render() + { + $this->form = $this->getCrudBoard()->getForm(); + return view('aim-admin::crudboard.form'); + } +} diff --git a/src/View/Components/CrudGrid.php b/src/View/Components/CrudGrid.php new file mode 100644 index 0000000..d0c8d94 --- /dev/null +++ b/src/View/Components/CrudGrid.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace CodeCoz\AimAdmin\View\Components; + +use CodeCoz\AimAdmin\Contracts\Service\CrudBoard\CrudGridInterface; + +/** + * this is a component class responsible for Crud Grid view + * + * @author CodeCoz + */ +class CrudGrid extends AbstractAimAdminComponent +{ + + public CrudGridInterface $grid; + + /** + * Get the view / contents that represent the component. + * + * @return \Illuminate\View\View|\Closure|string + */ + public function render() + { + $this->grid = $this->getCrudBoard()->getGrid(); + return view('aim-admin::crudboard.grid'); + } +} diff --git a/src/View/Components/CrudShow.php b/src/View/Components/CrudShow.php new file mode 100644 index 0000000..71a812a --- /dev/null +++ b/src/View/Components/CrudShow.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace CodeCoz\AimAdmin\View\Components; + +use CodeCoz\AimAdmin\Services\CrudBoard\CrudShow as Show; + +/** + * this is a component class responsible for Crud Grid view + * + * @author CodeCoz + */ +class CrudShow extends AbstractAimAdminComponent +{ + + public Show $show; + + /** + * Get the view / contents that represent the component. + * + * @return \Illuminate\View\View|\Closure|string + */ + public function render() + { + $this->show = $this->getCrudBoard()->getCrudShow(); + return view('aim-admin::crudboard.show'); + } +} diff --git a/src/View/Components/GridFilter.php b/src/View/Components/GridFilter.php new file mode 100644 index 0000000..ab9557b --- /dev/null +++ b/src/View/Components/GridFilter.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace CodeCoz\AimAdmin\View\Components; + +use CodeCoz\AimAdmin\Services\CrudBoard\GridFilter as Filter; + +/** + * this is a component class responsible for Crud Grid view + * + * @author CodeCoz + */ +class GridFilter extends AbstractAimAdminComponent +{ + + public Filter $filter; + + /** + * Get the view / contents that represent the component. + * + * @return \Illuminate\View\View|\Closure|string + */ + public function render() + { + $this->prepareFields(); + return view('aim-admin::crudboard.filter'); + } + + private function prepareFields(): void + { + $filter = $this->getCrudBoard() + ->getGrid()->getFilter(); + $fields = $filter->getFields(); + foreach ($fields as $name => $field) { + $field->setName(Filter::CONTAINER_NAME . "[$name]"); + } + $this->filter = $filter; + } +} diff --git a/src/View/Components/MainLayout.php b/src/View/Components/MainLayout.php new file mode 100644 index 0000000..0d58999 --- /dev/null +++ b/src/View/Components/MainLayout.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace CodeCoz\AimAdmin\View\Components; + +use Illuminate\View\Component; +use Illuminate\View\View; + +/** + * this is a component class responsible for Crud Grid view + * + * @author CodeCoz + */ +class Toast extends Component +{ + /** + * Create the component instance. + */ + public function __construct( + public string $type, + public string $message, + public int $timer = 2000, + ) + { + } + + /** + * Get the view / contents that represent the component. + */ + public function render(): View + { + return view('aim-admin::crudboard.toast'); + } + +} diff --git a/src/stubs/app/Models/User.stub b/src/stubs/app/Models/User.stub new file mode 100644 index 0000000..485248e --- /dev/null +++ b/src/stubs/app/Models/User.stub @@ -0,0 +1,11 @@ + + + Aim :: Admin Dashboard + +
+
+

Admin Dashboard

+
+
+
+
+
+
Empty card
+
+
+
+
+
+
+ diff --git a/src/stubs/route/web.php.stub b/src/stubs/route/web.php.stub new file mode 100644 index 0000000..4e77542 --- /dev/null +++ b/src/stubs/route/web.php.stub @@ -0,0 +1,5 @@ +Route::group(['middleware' => ['web', 'auth']], function () { + Route::get('/dashboard', function () { + return view('dashboard'); + })->name('dashboard'); +}); diff --git a/src/stubs/vite.config.js b/src/stubs/vite.config.js new file mode 100644 index 0000000..3600312 --- /dev/null +++ b/src/stubs/vite.config.js @@ -0,0 +1,46 @@ +import {defineConfig} from 'vite'; +import laravel from 'laravel-vite-plugin'; +import {viteStaticCopy} from 'vite-plugin-static-copy' + +export default defineConfig({ + plugins: [ + laravel({ + input: [ + 'resources/js/app.js' + ], + refresh: true, + }), + viteStaticCopy({ + targets: [ + { + src: 'vendor/codecoz/aim-admin/resources/img', + dest: '../' + }, + { + src: 'node_modules/admin-lte/dist/img', + dest: '../dist' + } + ] + }) + ], + build: { + rollupOptions: { + output: { + manualChunks(id) { + if (id.includes('node_modules')) { + return 'vendor'; // split vendor modules into a separate chunk + } + // more conditions for other chunks + } + } + }, + chunkSizeWarningLimit: 500, // Adjust the chunk size limit + }, + server: { + hmr: { + host: 'localhost', + protocol: 'ws', + port: 3000 + } + }, +}); diff --git a/tests/ComponentTestCase.php b/tests/ComponentTestCase.php new file mode 100644 index 0000000..d73ae68 --- /dev/null +++ b/tests/ComponentTestCase.php @@ -0,0 +1,55 @@ +artisan('view:clear'); + } + + protected function flashOld(array $input): void + { + session()->flashInput($input); + $sessionStore = session()->driver(); + request()->setLaravelSession($sessionStore); + } + + + protected function getPackageProviders($app): array + { + return [AdminServiceProvider::class]; + } + + /** + * @throws RuntimeException + * @throws InvalidArgumentException|\Throwable + */ + public function assertComponentRenders(string $expected, string $template, array $data = []): void + { + $indenter = new Indenter(); + $indenter->setElementType('h1', Indenter::ELEMENT_TYPE_INLINE); + $indenter->setElementType('del', Indenter::ELEMENT_TYPE_INLINE); + + $blade = (string)$this->blade($template, $data); + $indented = $indenter->indent($blade); + $cleaned = str_replace( + [' >', "\n/>", '
', '> ', "\n>"], + ['>', ' />', "\n", ">\n ", '>'], + $indented, + ); + + $this->assertSame($expected, $cleaned); + } +} diff --git a/tests/Components/ButtonFieldTest.php b/tests/Components/ButtonFieldTest.php new file mode 100644 index 0000000..67ea899 --- /dev/null +++ b/tests/Components/ButtonFieldTest.php @@ -0,0 +1,90 @@ +assertInstanceOf(ButtonField::class, $component); + } + + #[Test] public function it_sets_the_html_element_as_anchor() + { + $component = ButtonField::init('name', 'Create'); + $this->assertEquals('a', $component->getDto()->getHtmlElement()); + } + + #[Test] public function it_sets_the_button_label_correctly() + { + $component = ButtonField::init('name', 'Create'); + $this->assertEquals('Create', $component->getDto()->getLabel()); + } + + + #[Test] public function it_sets_the_title_attribute_correctly() + { + $component = ButtonField::init('name', 'Create'); + $this->assertEquals('Create', $component->getDto()->getHtmlAttributes()->get('title')); + } + + + #[Test] public function it_sets_the_button_value_correctly() + { + $component = ButtonField::init('name', 'Create'); + $this->assertEquals('name', $component->getDto()->getValue()); + } + + #[Test] public function it_sets_the_css_class_correctly() + { + $component = ButtonField::init('name', 'Create'); + $this->assertEquals('btn btn-block btn-sm btn-primary', $component->getDto()->getCssClass()); + } + + + #[Test] public function it_sets_the_button_type_correctly() + { + $component = ButtonField::init('name', 'Create'); + $this->assertEquals('row', $component->getDto()->getType()); + } + + #[Test] public function it_adds_custom_html_attributes() + { + $component = ButtonField::init('name', 'Create') + ->setHtmlAttributes(['data-custom' => 'custom-value']); + $this->assertEquals('custom-value', $component->getDto()->getHtmlAttributes()->get('data-custom')); + } + + + #[Test] public function it_can_render_with_custom_url() + { + $component = ButtonField::init('name', 'Create') + ->createAsFormAction() + ->linkToUrl('/custom-url'); + $this->assertEquals('/custom-url', $component->getDto()->getUrl()); + } + + + #[Test] public function it_can_render_with_custom_route() + { + $component = ButtonField::init('name', 'Create') + ->createAsFormAction() + ->linkToRoute('custom.route', ['param' => 'value']); + $this->assertEquals('custom.route', $component->getDto()->getRouteName()); + $this->assertEquals(['param' => 'value'], $component->getDto()->getRouteParameters()); + } + + #[Test] public function it_sets_the_component_template_correctly() + { + $component = ButtonField::init('name', 'Create'); + $this->assertEquals('aim-admin::crudboard.actions.grid-button', $component->getDto()->getComponent()); + } + +} diff --git a/tests/Components/CheckBoxFieldTest.php b/tests/Components/CheckBoxFieldTest.php new file mode 100644 index 0000000..5181407 --- /dev/null +++ b/tests/Components/CheckBoxFieldTest.php @@ -0,0 +1,57 @@ +component = CheckBoxField::init('name', 'CheckBox'); + } + + #[Test] public function the_checkbox_component_can_be_rendered() + { + $this->assertInstanceOf(CheckBoxField::class, $this->component); + } + + #[Test] public function it_sets_the_html_element_as_anchor() + { + $this->assertEquals('input', $this->component->getDto()->getHtmlElement()); + } + + #[Test] public function it_sets_the_checkbox_label_correctly() + { + $this->assertEquals('CheckBox', $this->component->getDto()->getLabel()); + } + + #[Test] public function it_sets_the_checkbox_value_correctly() + { + $this->assertEquals(null, $this->component->getDto()->getValue()); + } + + #[Test] public function it_sets_the_checkbox_type_correctly() + { + $this->assertEquals('checkbox', $this->component->getDto()->getInputType()); + } + + #[Test] public function it_adds_custom_html_attributes() + { + $component = CheckBoxField::init('name', 'CheckBox') + ->setHtmlAttributes(['data-custom' => 'custom-value']); + $this->assertEquals('custom-value', $component->getDto()->getHtmlAttributes()->get('data-custom')); + } + + #[Test] public function it_sets_the_component_template_correctly() + { + $this->assertEquals('aim-admin::crudboard.fields.checkbox', $this->component->getDto()->getComponent()); + } + +} diff --git a/tests/Components/DateTimeFieldTest.php b/tests/Components/DateTimeFieldTest.php new file mode 100644 index 0000000..db081cb --- /dev/null +++ b/tests/Components/DateTimeFieldTest.php @@ -0,0 +1,34 @@ +component = DateTimeField::init('date', 'Date'); + } + + #[Test] + public function it_initializes_with_correct_attributes() + { + + $this->assertEquals('date', $this->component->getDto()->getName()); + $this->assertEquals('Date', $this->component->getDto()->getLabel()); + } + + #[Test] + public function it_sets_the_component_template_correctly() + { + $this->assertEquals('aim-admin::crudboard.fields.datetime', $this->component->getDto()->getComponent()); + } +} diff --git a/tests/Components/DropDownFieldTest.php b/tests/Components/DropDownFieldTest.php new file mode 100644 index 0000000..f9a02fe --- /dev/null +++ b/tests/Components/DropDownFieldTest.php @@ -0,0 +1,81 @@ +options = [ + 1 => 'One', + 2 => 'Two', + ]; + $this->component = ChoiceField::init('type', 'Type', choiceType: 'select', choiceList: $this->options); + } + + #[Test] public function the_choice_field_component_can_be_rendered() + { + $this->assertInstanceOf(ChoiceField::class, $this->component); + } + + #[Test] public function it_sets_the_required_html_attribute_correctly() + { + $component = $this->component->setHtmlAttributes(['required' => true]); + $this->assertTrue($component->getDto()->getHtmlAttributes()->get('required')); + } + + #[Test] public function it_sets_the_choice_list_correctly() + { + $this->assertEquals($this->options, $this->component->getDto()->getCustomOption('choiceList')); + } + + #[Test] public function it_sets_the_input_type_correctly() + { + $this->assertEquals('select', $this->component->getDto()->getInputType()); + } + + #[Test] public function it_sets_the_default_value_correctly() + { + $component = $this->component->setDefaultValue('1'); + $this->assertEquals('1', $component->getDto()->getValue()); + } + + #[Test] public function it_handles_custom_options_correctly() + { + $component = $this->component->setCustomOption('empty', 'Select an option'); + $this->assertEquals('Select an option', $component->getDto()->getCustomOption('empty')); + } + + #[Test] public function it_sets_the_name_and_label_correctly() + { + $this->assertEquals('type', $this->component->getDto()->getName()); + $this->assertEquals('Type', $this->component->getDto()->getLabel()); + } + + #[Test] public function it_defaults_css_class_to_empty_string() + { + $this->assertEquals('', $this->component->getDto()->getCssClass()); + } + + #[Test] public function it_sets_the_layout_class_correctly() + { + $this->assertEquals('col-lg-6', $this->component->getDto()->getLayoutClass()); + } + + #[Test] public function it_sets_the_component_template_correctly() + { + $this->assertEquals('aim-admin::crudboard.fields.choice', $this->component->getDto()->getComponent()); + } + + +} diff --git a/tests/Components/FieldGroupTest.php b/tests/Components/FieldGroupTest.php new file mode 100644 index 0000000..fe1baa7 --- /dev/null +++ b/tests/Components/FieldGroupTest.php @@ -0,0 +1,54 @@ +component = FieldGroup::init([ + TextField::init('name', 'Product Name'), + TextField::init('detail', 'Product Detail'), + TextField::init('source', 'Product Source'), + ], 'Group') + ->setLayoutClass('col-12'); + } + + #[Test] public function the_field_group_component_can_be_rendered() + { + $this->assertInstanceOf(FieldGroup::class, $this->component); + } + + #[Test] public function it_sets_the_field_group_label_correctly() + { + $this->assertEquals('Group', $this->component->getDto()->getLabel()); + } + + #[Test] public function it_sets_the_field_group_value_correctly() + { + $this->assertEquals(null, $this->component->getDto()->getValue()); + } + + #[Test] public function it_adds_custom_html_attributes() + { + $component = $this->component + ->setHtmlAttributes(['data-custom' => 'custom-value']); + $this->assertEquals('custom-value', $component->getDto()->getHtmlAttributes()->get('data-custom')); + } + + #[Test] public function it_sets_the_component_template_correctly() + { + $this->assertEquals('aim-admin::crudboard.fields.group', $this->component->getDto()->getComponent()); + } + +} diff --git a/tests/Components/FileFieldTest.php b/tests/Components/FileFieldTest.php new file mode 100644 index 0000000..2640c7a --- /dev/null +++ b/tests/Components/FileFieldTest.php @@ -0,0 +1,94 @@ +component = FileField::init('name', 'file-name'); + } + + #[Test] public function it_initializes_with_correct_attributes() + { + $this->assertEquals('name', $this->component->getDto()->getName()); + $this->assertEquals('File-name', $this->component->getDto()->getLabel()); + $this->assertEquals('file', $this->component->getDto()->getInputType()); + $this->assertEquals('input', $this->component->getDto()->getHtmlElement()); + $this->assertEquals(null, $this->component->getDto()->getHtmlAttributes()->get('id')); + } + + #[Test] public function the_text_filed_component_can_be_rendered() + { + $this->assertInstanceOf(FileField::class, $this->component); + } + + #[Test] public function it_sets_the_html_element_as_input() + { + $this->assertEquals('input', $this->component->getDto()->getHtmlElement()); + } + + #[Test] public function it_sets_the_placeholder_attribute() + { + $this->assertEquals('file-name', $this->component->getDto()->getHtmlAttributes()->get('placeholder')); + } + + #[Test] public function it_sets_the_id_attribute() + { + $this->assertEquals(null, $this->component->getDto()->getHtmlAttributes()->get('id')); + } + + #[Test] public function it_assigns_the_label_correctly() + { + $this->assertEquals('File-name', $this->component->getDto()->getLabel()); + } + + #[Test] public function it_defaults_to_not_required() + { + $this->assertFalse($this->component->getDto()->isRequired()); + } + + + #[Test] public function it_handles_custom_options() + { + $component = $this->component->setCustomOption('data-custom', 'value'); + $this->assertEquals('value', $component->getDto()->getCustomOption('data-custom')); + } + + + #[Test] public function it_defaults_css_class_to_empty_string() + { + $this->assertEquals('', $this->component->getDto()->getCssClass()); + } + + + #[Test] public function it_stores_html_attributes_correctly() + { + $component = $this->component->setHtmlAttributes(['data-test' => 'test-value']); + $this->assertEquals('test-value', $component->getDto()->getHtmlAttributes()->get('data-test')); + } + + + #[Test] public function it_sets_the_layout_class_correctly() + { + $this->assertEquals('col-lg-6', $this->component->getDto()->getLayoutClass()); + } + + + #[Test] public function it_sets_the_component_template_correctly() + { + $this->assertEquals('aim-admin::crudboard.fields.file', $this->component->getDto()->getComponent()); + } + +} diff --git a/tests/Components/HiddenFieldTest.php b/tests/Components/HiddenFieldTest.php new file mode 100644 index 0000000..afc2d24 --- /dev/null +++ b/tests/Components/HiddenFieldTest.php @@ -0,0 +1,86 @@ +component = HiddenField::init('name', 'input-name'); + } + + #[Test] public function it_initializes_with_correct_attributes() + { + $this->assertEquals('name', $this->component->getDto()->getName()); + $this->assertEquals(null, $this->component->getDto()->getLabel()); + } + + #[Test] public function the_text_filed_component_can_be_rendered() + { + $this->assertInstanceOf(HiddenField::class, $this->component); + } + + + #[Test] public function it_sets_the_placeholder_attribute() + { + $component = $this->component->setPlaceholder('Placeholder'); + $this->assertEquals('Placeholder', $component->getDto()->getHtmlAttributes()->get('placeholder')); + } + + + #[Test] public function it_assigns_the_label_correctly() + { + $this->assertEquals(null, $this->component->getDto()->getLabel()); + } + + + #[Test] public function it_defaults_to_not_required() + { + $this->assertFalse($this->component->getDto()->isRequired()); + } + + + #[Test] public function it_handles_custom_options() + { + $component = $this->component->setCustomOption('data-custom', 'value'); + $this->assertEquals('value', $component->getDto()->getCustomOption('data-custom')); + } + + + #[Test] public function it_defaults_css_class_to_empty_string() + { + $this->assertEquals('', $this->component->getDto()->getCssClass()); + } + + + #[Test] public function it_stores_html_attributes_correctly() + { + $component = $this->component->setHtmlAttributes(['data-test' => 'test-value']); + $this->assertEquals('test-value', $component->getDto()->getHtmlAttributes()->get('data-test')); + } + + + #[Test] public function it_sets_the_layout_class_correctly() + { + $this->assertEquals('col-lg-6', $this->component->getDto()->getLayoutClass()); + } + + + #[Test] public function it_sets_the_component_template_correctly() + { + $this->assertEquals('aim-admin::crudboard.fields.hidden', $this->component->getDto()->getComponent()); + } + + +} diff --git a/tests/Components/TextAreaFieldTest.php b/tests/Components/TextAreaFieldTest.php new file mode 100644 index 0000000..76258cd --- /dev/null +++ b/tests/Components/TextAreaFieldTest.php @@ -0,0 +1,91 @@ +component = TextareaField::init('name', 'textarea-name'); + } + + #[Test] public function it_initializes_with_correct_attributes() + { + $this->assertEquals('name', $this->component->getDto()->getName()); + $this->assertEquals('Textarea-name', $this->component->getDto()->getLabel()); + $this->assertEquals('name', $this->component->getDto()->getHtmlAttributes()->get('id')); + } + + #[Test] public function the_text_filed_component_can_be_rendered() + { + $this->assertInstanceOf(TextareaField::class, $this->component); + } + + + #[Test] public function it_sets_the_placeholder_attribute() + { + $component = $this->component->setPlaceholder('Placeholder'); + $this->assertEquals('Placeholder', $component->getDto()->getHtmlAttributes()->get('placeholder')); + } + + #[Test] public function it_sets_the_id_attribute() + { + $this->assertEquals('name', $this->component->getDto()->getHtmlAttributes()->get('id')); + } + + #[Test] public function it_assigns_the_label_correctly() + { + $this->assertEquals('Textarea-name', $this->component->getDto()->getLabel()); + } + + + #[Test] public function it_defaults_to_not_required() + { + $this->assertFalse($this->component->getDto()->isRequired()); + } + + + #[Test] public function it_handles_custom_options() + { + $component = $this->component->setCustomOption('data-custom', 'value'); + $this->assertEquals('value', $component->getDto()->getCustomOption('data-custom')); + } + + + #[Test] public function it_defaults_css_class_to_empty_string() + { + $this->assertEquals('', $this->component->getDto()->getCssClass()); + } + + + #[Test] public function it_stores_html_attributes_correctly() + { + $component = $this->component->setHtmlAttributes(['data-test' => 'test-value']); + $this->assertEquals('test-value', $component->getDto()->getHtmlAttributes()->get('data-test')); + } + + + #[Test] public function it_sets_the_layout_class_correctly() + { + $this->assertEquals('col-lg-6', $this->component->getDto()->getLayoutClass()); + } + + + #[Test] public function it_sets_the_component_template_correctly() + { + $this->assertEquals('aim-admin::crudboard.fields.textarea', $this->component->getDto()->getComponent()); + } + + +} diff --git a/tests/Components/TextFieldTest.php b/tests/Components/TextFieldTest.php new file mode 100644 index 0000000..8c4f568 --- /dev/null +++ b/tests/Components/TextFieldTest.php @@ -0,0 +1,102 @@ +component = TextField::init('name', 'input-name'); + } + + #[Test] public function it_initializes_with_correct_attributes() + { + $this->assertEquals('name', $this->component->getDto()->getName()); + $this->assertEquals('Input-name', $this->component->getDto()->getLabel()); + $this->assertEquals('text', $this->component->getDto()->getInputType()); + $this->assertEquals('input', $this->component->getDto()->getHtmlElement()); + $this->assertEquals('name', $this->component->getDto()->getHtmlAttributes()->get('id')); + } + + #[Test] public function the_text_filed_component_can_be_rendered() + { + $this->assertInstanceOf(TextField::class, $this->component); + } + + + #[Test] public function it_sets_the_html_element_as_input() + { + $this->assertEquals('input', $this->component->getDto()->getHtmlElement()); + } + + + #[Test] public function it_sets_the_placeholder_attribute() + { + $component = $this->component->setPlaceholder('Placeholder'); + $this->assertEquals('Placeholder', $component->getDto()->getHtmlAttributes()->get('placeholder')); + } + + + #[Test] public function it_sets_the_id_attribute() + { + $this->assertEquals('name', $this->component->getDto()->getHtmlAttributes()->get('id')); + } + + + #[Test] public function it_assigns_the_label_correctly() + { + $this->assertEquals('Input-name', $this->component->getDto()->getLabel()); + } + + + #[Test] public function it_defaults_to_not_required() + { + $this->assertFalse($this->component->getDto()->isRequired()); + } + + + #[Test] public function it_handles_custom_options() + { + $component = $this->component->setCustomOption('data-custom', 'value'); + $this->assertEquals('value', $component->getDto()->getCustomOption('data-custom')); + } + + + #[Test] public function it_defaults_css_class_to_empty_string() + { + $this->assertEquals('', $this->component->getDto()->getCssClass()); + } + + + #[Test] public function it_stores_html_attributes_correctly() + { + $component = $this->component->setHtmlAttributes(['data-test' => 'test-value']); + $this->assertEquals('test-value', $component->getDto()->getHtmlAttributes()->get('data-test')); + } + + + #[Test] public function it_sets_the_layout_class_correctly() + { + $this->assertEquals('col-lg-6', $this->component->getDto()->getLayoutClass()); + } + + + #[Test] public function it_sets_the_component_template_correctly() + { + $this->assertEquals('aim-admin::crudboard.fields.input', $this->component->getDto()->getComponent()); + } + + +} diff --git a/tests/Service/CrudBoardTest.php b/tests/Service/CrudBoardTest.php new file mode 100644 index 0000000..1d8661f --- /dev/null +++ b/tests/Service/CrudBoardTest.php @@ -0,0 +1,97 @@ +crudBoard = new CrudBoard(); + } + + public function testCanCreateCrudForm() + { + $fields = ['name']; + $form = $this->crudBoard->createForm($fields); + $this->assertInstanceOf(CrudForm::class,$form); + $this->assertInstanceOf(CrudForm::class,$this->crudBoard->getForm()); + } + + public function testCrudBoardParameter() + { + $this->crudBoard->setParam('test1'); + $this->crudBoard->setParam('test2'); + $this->crudBoard->setParam('test3'); + $this->assertSame(['test1','test2','test3'], $this->crudBoard->getParams()); + } + + /** + * @throws \Exception + */ + public function testCrudBoardService() + { + $stubRepo = new class() implements AimAdminRepositoryInterface + { + public function getModelFqcn(): string + { + return "User"; + } + public function crudShow(int|string $id): ?ArrayAccess + { + return $this->findById($id); + } + private function findById(int $id) : ArrayAccess{ + $row = ['id'=>$id, 'title'=>'foo']; + + return $stubRow = new class($row) implements ArrayAccess { + private array $container = []; + public function __construct(array $row) + { + $this->container = $row; + } + public function offsetSet($offset, $value): void { + if (is_null($offset)) { + $this->container[] = $value; + } else { + $this->container[$offset] = $value; + } + } + + public function offsetExists($offset): bool { + return isset($this->container[$offset]); + } + + public function offsetUnset($offset): void { + unset($this->container[$offset]); + } + + public function offsetGet($offset): mixed { + return $this->container[$offset] ?? null; + } + }; + } + }; + + $this->assertInstanceOf(CrudBoard::class,$this->crudBoard->setRepository($stubRepo)); + $this->assertInstanceOf(AimAdminRepositoryInterface::class,$this->crudBoard->getRepository()); + $crudShow = $this->crudBoard->createShow(1,['name']); + $this->assertInstanceOf(CrudShowInterface::class,$crudShow); + + } + + +} + + diff --git a/tests/Service/CrudFormTest.php b/tests/Service/CrudFormTest.php new file mode 100644 index 0000000..64d26b3 --- /dev/null +++ b/tests/Service/CrudFormTest.php @@ -0,0 +1,38 @@ +form = new CrudForm(); + } + + public function testFormField() + { + $this->form->addFields(['foo','bar']); + $this->assertInstanceOf(FormFieldCollection::class,$this->form->getFields()); + } + + public function testSetCssClass() + { + $this->form->setCssClass('foo') + ->setCssClass('bar'); + $this->assertEquals('foo bar',trim($this->form->getCssClass())); + } + + public function testAddIdField() + { + $this->form->addFields(['foo','bar']); + $this->form->addIdField(); + $this->assertEquals(3,$this->form->getFields()->count()); + } +} diff --git a/tests/Service/CrudGridTest.php b/tests/Service/CrudGridTest.php new file mode 100644 index 0000000..82f614e --- /dev/null +++ b/tests/Service/CrudGridTest.php @@ -0,0 +1,44 @@ +createMock(CrudGridLoaderInterface::class); + $this->crudGrid = CrudGrid::init($crudGridLoader,[]); + } + + public function testTitle() + { + $this->crudGrid->setTitle('foo'); + $this->assertEquals('foo',$this->crudGrid->getTitle(),'Should return foo'); + } + + public function testAddColumn() + { + $this->crudGrid->addColumns(['foo','bar']); + $this->assertInstanceOf(FieldCollection::class,$this->crudGrid->getColumns()); + } + + public function testDataLoader() + { + $this->assertInstanceOf(CrudGridLoaderInterface::class,$this->crudGrid->getGridDataLoader()); + } + + +} diff --git a/tests/Service/GridFilterTest.php b/tests/Service/GridFilterTest.php new file mode 100644 index 0000000..82f5fad --- /dev/null +++ b/tests/Service/GridFilterTest.php @@ -0,0 +1,44 @@ +filter = new GridFilter(); + } + + public function testFilterField() + { + $this->filter->addFields(['foo','bar']); + $this->assertInstanceOf(FormFieldCollection::class,$this->filter->getFields()); + } + + public function testSetCssClass() + { + $this->filter->setCssClass('foo') + ->setCssClass('bar'); + $this->assertEquals('foo bar',trim($this->filter->getCssClass())); + } + + public function testGetData() + { + $this->assertEquals([],$this->filter->getData()); + + } + + public function testAssignQueryData() + { + $this->filter->addFields(['foo','bar']); + $obj = $this->filter->assignQueryData(); + $this->assertInstanceOf(GridFilter::class,$obj); + } +} diff --git a/tests/TestView.php b/tests/TestView.php new file mode 100644 index 0000000..e4101a4 --- /dev/null +++ b/tests/TestView.php @@ -0,0 +1,144 @@ +view = $view; + $this->rendered = $view->render(); + } + + /** + * Assert that the given string is contained within the view. + * + * @param string $value + * @param bool $escaped + * @return $this + */ + public function assertSee($value, $escaped = true) + { + $value = $escaped ? e($value) : $value; + + PHPUnit::assertStringContainsString((string)$value, $this->rendered); + + return $this; + } + + /** + * Assert that the given strings are contained in order within the view. + * + * @param array $values + * @param bool $escape + * @return $this + */ + public function assertSeeInOrder(array $values, $escape = true) + { + $values = $escape ? array_map('e', ($values)) : $values; + + PHPUnit::assertThat($values, new SeeInOrder($this->rendered)); + + return $this; + } + + /** + * Assert that the given string is contained within the view text. + * + * @param string $value + * @param bool $escape + * @return $this + */ + public function assertSeeText($value, $escape = true) + { + $value = $escape ? e($value) : $value; + + PHPUnit::assertStringContainsString((string)$value, strip_tags($this->rendered)); + + return $this; + } + + /** + * Assert that the given strings are contained in order within the view text. + * + * @param array $values + * @param bool $escape + * @return $this + */ + public function assertSeeTextInOrder(array $values, $escape = true) + { + $values = $escape ? array_map('e', ($values)) : $values; + + PHPUnit::assertThat($values, new SeeInOrder(strip_tags($this->rendered))); + + return $this; + } + + /** + * Assert that the given string is not contained within the view. + * + * @param string $value + * @param bool $escape + * @return $this + */ + public function assertDontSee($value, $escape = true) + { + $value = $escape ? e($value) : $value; + + PHPUnit::assertStringNotContainsString((string)$value, $this->rendered); + + return $this; + } + + /** + * Assert that the given string is not contained within the view text. + * + * @param string $value + * @param bool $escape + * @return $this + */ + public function assertDontSeeText($value, $escape = true) + { + $value = $escape ? e($value) : $value; + + PHPUnit::assertStringNotContainsString((string)$value, strip_tags($this->rendered)); + + return $this; + } + + /** + * Get the string contents of the rendered view. + * + * @return string + */ + public function __toString() + { + return $this->rendered; + } +} diff --git a/tests/Traits/InteractsWithViews.php b/tests/Traits/InteractsWithViews.php new file mode 100644 index 0000000..dfecb02 --- /dev/null +++ b/tests/Traits/InteractsWithViews.php @@ -0,0 +1,79 @@ +getPaths())) { + ViewFacade::addLocation(sys_get_temp_dir()); + } + + $tempFile = tempnam($tempDirectory, 'laravel-blade') . '.blade.php'; + + file_put_contents($tempFile, $template); + + return new TestView(view(Str::before(basename($tempFile), '.blade.php'), $data)); + } + + /** + * Render the given view component. + * + * @param string $componentClass + * @param array $data + * @return TestView + * @throws BindingResolutionException + */ + protected function component(string $componentClass, array $data = []) + { + $component = $this->app->make($componentClass, $data); + + $view = $component->resolveView(); + + return $view instanceof View + ? new TestView($view->with($component->data())) + : new TestView(view($view, $component->data())); + } + + /** + * Populate the shared view error bag with the given errors. + * + * @param array $errors + * @param string $key + * @return void + */ + protected function withViewErrors(array $errors, $key = 'default') + { + ViewFacade::share('errors', (new ViewErrorBag())->put($key, new MessageBag($errors))); + } +}