├── resources
├── views
│ └── .gitkeep
└── lang
│ └── en
│ └── validation.php
├── pint.json
├── .prettierignore
├── CHANGELOG.md
├── stubs
├── stack-configs
│ ├── postcss.config.mjs
│ ├── jsconfig.json
│ ├── tailwind.config.mjs
│ ├── .prettierrc.json
│ ├── .vscode
│ │ └── settings.json
│ ├── vite.config.js
│ └── eslint.config.js
├── resources
│ ├── views
│ │ ├── components
│ │ │ └── translations.blade.php
│ │ └── app.blade.php
│ └── js
│ │ ├── Components
│ │ ├── Form
│ │ │ ├── AppLabel.vue
│ │ │ ├── TipTap
│ │ │ │ ├── TipTapDivider.vue
│ │ │ │ └── TipTapButton.vue
│ │ │ ├── AppFormErrors.vue
│ │ │ ├── AppInputText.vue
│ │ │ ├── AppInputDate.vue
│ │ │ ├── AppRadioButton.vue
│ │ │ ├── AppInputPassword.vue
│ │ │ ├── AppTextArea.vue
│ │ │ └── AppCheckbox.vue
│ │ ├── Misc
│ │ │ ├── AppLink.vue
│ │ │ ├── AppImageNotAvailable.vue
│ │ │ ├── AppCard.vue
│ │ │ ├── AppSectionHeader.vue
│ │ │ └── AppButton.vue
│ │ ├── DataTable
│ │ │ ├── AppDataTableRow.vue
│ │ │ ├── AppDataTableData.vue
│ │ │ ├── AppDataTableHead.vue
│ │ │ ├── AppDataTable.vue
│ │ │ ├── AppDataSearch.vue
│ │ │ └── AppPaginator.vue
│ │ ├── Auth
│ │ │ ├── AppAuthShell.vue
│ │ │ └── AppAuthLogo.vue
│ │ ├── Menu
│ │ │ ├── AppBreadCrumb.vue
│ │ │ ├── AppBreadCrumbItem.vue
│ │ │ ├── AppMenuItem.vue
│ │ │ └── AppMenuSection.vue
│ │ ├── Message
│ │ │ ├── AppAlert.vue
│ │ │ └── AppFlashMessage.vue
│ │ └── Overlay
│ │ │ ├── AppConfirmDialog.vue
│ │ │ └── AppModal.vue
│ │ ├── Utils
│ │ ├── truncate.js
│ │ ├── debounce.js
│ │ ├── chunk.js
│ │ └── slug.js
│ │ ├── Composables
│ │ ├── useIsMobile.js
│ │ ├── useFormContext.js
│ │ ├── useFormErrors.js
│ │ ├── useAuthCan.js
│ │ ├── useClickOutside.js
│ │ ├── useTitle.js
│ │ └── useDataSearch.js
│ │ ├── Layouts
│ │ ├── GuestLayout.vue
│ │ └── AuthenticatedLayout.vue
│ │ ├── Plugins
│ │ └── Translations.js
│ │ ├── Configs
│ │ └── menu.js
│ │ ├── app.js
│ │ ├── Resolvers
│ │ └── AppComponentsResolver.js
│ │ └── Pages
│ │ ├── Dashboard
│ │ ├── Components
│ │ │ └── DashboardCard.vue
│ │ └── DashboardIndex.vue
│ │ ├── AclRole
│ │ └── RoleForm.vue
│ │ ├── AclPermission
│ │ └── PermissionForm.vue
│ │ └── AdminAuth
│ │ └── ForgotPage.vue
├── site
│ ├── resources-site
│ │ ├── images
│ │ │ └── home-img.png
│ │ ├── js
│ │ │ ├── Components
│ │ │ │ └── IndexExampleComponent.vue
│ │ │ ├── index-app.js
│ │ │ └── create-vue-app.js
│ │ └── views
│ │ │ ├── site-layout.blade.php
│ │ │ └── pagination
│ │ │ └── simple-tailwind.blade.php
│ └── modules
│ │ ├── Index
│ │ ├── Models
│ │ │ └── Index.php
│ │ ├── routes
│ │ │ └── site.php
│ │ ├── Tests
│ │ │ └── IndexTest.php
│ │ ├── Http
│ │ │ ├── Controllers
│ │ │ │ └── IndexController.php
│ │ │ └── Requests
│ │ │ │ └── IndexValidate.php
│ │ ├── IndexServiceProvider.php
│ │ └── views
│ │ │ └── index.blade.php
│ │ └── Support
│ │ ├── SiteModel.php
│ │ └── SiteController.php
├── .prettierrc.json
├── module-stub
│ └── modules
│ │ ├── Services
│ │ └── Service.stub
│ │ ├── Tests
│ │ └── ModuleTest.stub
│ │ ├── Http
│ │ ├── Requests
│ │ │ └── ModuleValidate.stub
│ │ └── Controllers
│ │ │ └── ModuleController.stub
│ │ ├── ModuleServiceProvider.stub
│ │ ├── Database
│ │ ├── Seeders
│ │ │ └── ModuleSeeder.stub
│ │ ├── Factories
│ │ │ └── ModelFactory.stub
│ │ └── Migrations
│ │ │ └── create_table.stub
│ │ ├── Models
│ │ └── Model.stub
│ │ └── routes
│ │ └── app.stub
├── tests
│ ├── TestCase.php
│ ├── CreatesApplication.php
│ └── Pest.php
├── modules
│ ├── Dashboard
│ │ ├── routes
│ │ │ └── app.php
│ │ ├── DashboardServiceProvider.php
│ │ ├── Http
│ │ │ └── Controllers
│ │ │ │ └── DashboardController.php
│ │ └── Tests
│ │ │ └── DashboardTest.php
│ ├── Support
│ │ ├── Http
│ │ │ ├── Controllers
│ │ │ │ ├── BackendController.php
│ │ │ │ └── AppController.php
│ │ │ └── Requests
│ │ │ │ ├── Request.php
│ │ │ │ └── JsonRequest.php
│ │ ├── helpers.php
│ │ ├── Validators
│ │ │ ├── required_editor.php
│ │ │ └── recaptcha.php
│ │ ├── Models
│ │ │ └── BaseModel.php
│ │ ├── Traits
│ │ │ ├── ActivityLog.php
│ │ │ ├── UpdateOrder.php
│ │ │ ├── UploadFile.php
│ │ │ ├── FileNameGenerator.php
│ │ │ ├── Searchable.php
│ │ │ └── EditorImage.php
│ │ ├── Tests
│ │ │ ├── FileNameGeneratorTraitTest.php
│ │ │ ├── UploadFileTraitTest.php
│ │ │ ├── EditorImageTraitTest.php
│ │ │ ├── ActivityLogTraitTest.php
│ │ │ ├── SearchableTraitTest.php
│ │ │ └── UpdateOrderTraitTest.php
│ │ ├── SupportServiceProvider.php
│ │ └── BaseServiceProvider.php
│ ├── AdminAuth
│ │ ├── views
│ │ │ └── emails
│ │ │ │ └── reset-password.blade.php
│ │ ├── Http
│ │ │ ├── Middleware
│ │ │ │ └── UserAuth.php
│ │ │ ├── Controllers
│ │ │ │ ├── AuthenticatedSessionController.php
│ │ │ │ ├── PasswordResetLinkController.php
│ │ │ │ └── NewPasswordController.php
│ │ │ └── Requests
│ │ │ │ └── LoginRequest.php
│ │ ├── AdminAuthServiceProvider.php
│ │ ├── Notifications
│ │ │ └── ResetPassword.php
│ │ ├── Tests
│ │ │ ├── AuthenticationTest.php
│ │ │ └── PasswordResetTest.php
│ │ └── routes
│ │ │ └── site.php
│ ├── Acl
│ │ ├── Services
│ │ │ ├── ListUserPermissions.php
│ │ │ └── GetUserPermissions.php
│ │ ├── Database
│ │ │ └── Seeders
│ │ │ │ ├── AclRoleSeeder.php
│ │ │ │ └── AclPermissionSeeder.php
│ │ ├── config
│ │ │ └── config.php
│ │ ├── Http
│ │ │ ├── Requests
│ │ │ │ ├── RoleValidate.php
│ │ │ │ └── PermissionValidate.php
│ │ │ └── Controllers
│ │ │ │ ├── UserController.php
│ │ │ │ ├── UserRoleController.php
│ │ │ │ ├── UserPermissionController.php
│ │ │ │ ├── RolePermissionController.php
│ │ │ │ ├── RoleController.php
│ │ │ │ └── PermissionController.php
│ │ ├── AclServiceProvider.php
│ │ └── Tests
│ │ │ ├── Role
│ │ │ ├── UserRoleTest.php
│ │ │ └── RolePermissionTest.php
│ │ │ └── Permission
│ │ │ ├── UserPermissionTest.php
│ │ │ └── GetUserPermissionTest.php
│ └── User
│ │ ├── Observers
│ │ └── UserObserver.php
│ │ ├── routes
│ │ └── app.php
│ │ ├── Database
│ │ ├── Factories
│ │ │ └── UserFactory.php
│ │ └── Migrations
│ │ │ └── 2024_01_25_000000_add_custom_fields_to_users_table.php
│ │ ├── Http
│ │ ├── Requests
│ │ │ └── UserValidate.php
│ │ └── Controllers
│ │ │ └── UserController.php
│ │ ├── UserServiceProvider.php
│ │ └── Models
│ │ └── User.php
├── page-stub
│ ├── Composables
│ │ └── Composable.stub
│ ├── Components
│ │ └── Component.stub
│ └── Form.stub
├── database
│ └── seeders
│ │ └── DatabaseSeeder.php
├── routes
│ └── web.php
├── lang
│ ├── pt_BR
│ │ ├── pagination.php
│ │ ├── auth.php
│ │ └── passwords.php
│ └── bn
│ │ ├── auth.php
│ │ ├── pagination.php
│ │ └── passwords.php
└── app
│ └── Http
│ └── Middleware
│ └── HandleInertiaRequests.php
├── config
└── modular.php
├── .prettierrc.json
├── .vscode
└── settings.json
├── SECURITY.md
├── package.json
├── src
├── Console
│ ├── InstallerTraits
│ │ ├── ModuleExists.php
│ │ └── BackendPackages.php
│ ├── SiteTraits
│ │ ├── PublishConfigFile.php
│ │ ├── ConfigureViews.php
│ │ └── CopySiteFiles.php
│ ├── PublishSiteFilesCommand.php
│ ├── PublishLaravelTranslationsCommand.php
│ ├── MakeMigrationCommand.php
│ ├── MakeTestCommand.php
│ ├── InstallCommand.php
│ ├── MakeValidateCommand.php
│ ├── MakeModelCommand.php
│ ├── MakeServiceCommand.php
│ ├── MakeSeederCommand.php
│ ├── MakeFactoryCommand.php
│ ├── MakeComponentCommand.php
│ ├── MakeRouteCommand.php
│ ├── RegisterServiceProviderCommand.php
│ └── MakeComposableCommand.php
├── Components
│ └── Translations.php
└── ModularServiceProvider.php
├── rector.php
├── LICENSE.md
├── eslint.config.js
└── composer.json
/resources/views/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/pint.json:
--------------------------------------------------------------------------------
1 | {
2 | "preset": "laravel"
3 | }
4 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | stubs/stack-configs/*
2 | stubs/site/stack-configs/*
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to `modular` will be documented in this file.
4 |
--------------------------------------------------------------------------------
/resources/lang/en/validation.php:
--------------------------------------------------------------------------------
1 | 'The :attribute field is required.',
5 | ];
6 |
--------------------------------------------------------------------------------
/stubs/stack-configs/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | '@tailwindcss/postcss': {},
4 | },
5 | };
--------------------------------------------------------------------------------
/config/modular.php:
--------------------------------------------------------------------------------
1 | '/',
5 | 'default-logged-route' => 'dashboard.index',
6 | ];
7 |
--------------------------------------------------------------------------------
/stubs/resources/views/components/translations.blade.php:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "semi": false,
4 | "trailingComma": "none",
5 | "arrowParens": "always"
6 | }
7 |
--------------------------------------------------------------------------------
/stubs/site/resources-site/images/home-img.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ModularThink/modular/HEAD/stubs/site/resources-site/images/home-img.png
--------------------------------------------------------------------------------
/stubs/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "semi": false,
4 | "trailingComma": "none",
5 | "arrowParens": "always"
6 | }
7 |
--------------------------------------------------------------------------------
/stubs/resources/js/Components/Form/AppLabel.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/stubs/site/modules/Index/Models/Index.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/stubs/resources/js/Components/DataTable/AppDataTableRow.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/stubs/resources/js/Components/Auth/AppAuthShell.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
--------------------------------------------------------------------------------
/stubs/site/modules/Support/SiteController.php:
--------------------------------------------------------------------------------
1 |
2 |
3 | From ExampleComponent.vue: Let's do it!
4 |
5 |
6 |
--------------------------------------------------------------------------------
/stubs/module-stub/modules/Services/Service.stub:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 | |
7 |
8 |
--------------------------------------------------------------------------------
/stubs/resources/js/Components/Auth/AppAuthLogo.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/stubs/resources/js/Components/Form/TipTap/TipTapDivider.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
--------------------------------------------------------------------------------
/stubs/site/modules/Index/routes/site.php:
--------------------------------------------------------------------------------
1 | name('index.index');
9 |
--------------------------------------------------------------------------------
/stubs/site/resources-site/js/index-app.js:
--------------------------------------------------------------------------------
1 | import { createVueApp } from './create-vue-app.js'
2 | import IndexExampleComponent from './Components/IndexExampleComponent.vue'
3 |
4 | createVueApp({
5 | IndexExampleComponent
6 | }).mount('#app')
7 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | If you discover a security vulnerability within Modular, please send an e-mail to Daniel Cintra via [modular@visualcom.com.br](mailto:modular@visualcom.com.br). All security vulnerabilities will be promptly addressed.
4 |
--------------------------------------------------------------------------------
/stubs/modules/Dashboard/routes/app.php:
--------------------------------------------------------------------------------
1 | name('dashboard.index');
9 |
--------------------------------------------------------------------------------
/stubs/resources/js/Utils/truncate.js:
--------------------------------------------------------------------------------
1 | const truncate = (value, length, ending = '...') => {
2 | if (!value || value.length < length) {
3 | return value
4 | }
5 |
6 | return value.substring(0, length) + ending
7 | }
8 |
9 | export default truncate
10 |
--------------------------------------------------------------------------------
/stubs/stack-configs/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "paths": {
5 | "@/*": ["resources/js/*"]
6 | },
7 | "jsx": "preserve"
8 | },
9 | "exclude": ["node_modules", "public"]
10 | }
11 |
--------------------------------------------------------------------------------
/stubs/modules/Support/Http/Controllers/BackendController.php:
--------------------------------------------------------------------------------
1 | middleware('auth.user');
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/stubs/resources/js/Components/Misc/AppImageNotAvailable.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 | N/A
6 |
7 |
8 |
--------------------------------------------------------------------------------
/stubs/modules/Support/helpers.php:
--------------------------------------------------------------------------------
1 | assertTrue(true);
10 | // });
11 |
--------------------------------------------------------------------------------
/stubs/page-stub/Composables/Composable.stub:
--------------------------------------------------------------------------------
1 | import { ref } from 'vue'
2 |
3 | export default function use{{ ComposableName }}() {
4 | const title = ref('foo')
5 |
6 | const {{ composableName }} = () => {
7 | return title.value
8 | }
9 |
10 | return { {{ composableName }} }
11 | }
12 |
--------------------------------------------------------------------------------
/stubs/site/modules/Index/Tests/IndexTest.php:
--------------------------------------------------------------------------------
1 | withoutVite();
9 | $response = $this->get('/');
10 | $response->assertStatus(200);
11 | });
12 |
--------------------------------------------------------------------------------
/stubs/modules/AdminAuth/views/emails/reset-password.blade.php:
--------------------------------------------------------------------------------
1 | @component('mail::message')
2 | # Forgot your password?
3 |
4 | Here is your password reset link.
5 |
6 | @component('mail::button', ['url' => $url])
7 | Reset Password
8 | @endcomponent
9 |
10 | Thanks,
11 | {{ config('app.name') }}
12 | @endcomponent
--------------------------------------------------------------------------------
/stubs/resources/js/Composables/useIsMobile.js:
--------------------------------------------------------------------------------
1 | import { ref } from 'vue'
2 |
3 | export default function useIsMobile() {
4 | const isMobile = ref(false)
5 | const width = window.innerWidth
6 |
7 | if (width <= 1024) {
8 | isMobile.value = true
9 | }
10 |
11 | return { isMobile }
12 | }
13 |
--------------------------------------------------------------------------------
/stubs/resources/js/Utils/debounce.js:
--------------------------------------------------------------------------------
1 | function debounce(func, timeout = 300) {
2 | let timer
3 | return (...args) => {
4 | window.clearTimeout(timer)
5 | timer = window.setTimeout(() => {
6 | func.apply(this, args)
7 | }, timeout)
8 | }
9 | }
10 |
11 | export default debounce
12 |
--------------------------------------------------------------------------------
/stubs/resources/js/Layouts/GuestLayout.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
14 |
--------------------------------------------------------------------------------
/stubs/site/modules/Index/Http/Controllers/IndexController.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
New Component
4 |
5 |
6 |
7 |
17 |
--------------------------------------------------------------------------------
/stubs/resources/js/Composables/useFormContext.js:
--------------------------------------------------------------------------------
1 | import { ref } from 'vue'
2 |
3 | export default function useFormContext() {
4 | const isCreate = ref()
5 | const isEdit = ref()
6 |
7 | isCreate.value = route().current().includes('.create')
8 | isEdit.value = route().current().includes('.edit')
9 |
10 | return { isCreate, isEdit }
11 | }
12 |
--------------------------------------------------------------------------------
/stubs/site/modules/Index/Http/Requests/IndexValidate.php:
--------------------------------------------------------------------------------
1 | 'required',
13 | ];
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/stubs/resources/js/Composables/useFormErrors.js:
--------------------------------------------------------------------------------
1 | import { computed } from 'vue'
2 | import { usePage } from '@inertiajs/vue3'
3 |
4 | export default function useFormErrors() {
5 | const errors = computed(() => usePage().props.errors)
6 |
7 | const errorsFields = computed(() => Object.keys(errors.value))
8 |
9 | return { errors, errorsFields }
10 | }
11 |
--------------------------------------------------------------------------------
/stubs/resources/js/Utils/chunk.js:
--------------------------------------------------------------------------------
1 | const chunk = (array, size = 1) => {
2 | const length = array?.length || 0
3 | if (!length || size < 1) return []
4 | const result = []
5 | for (let index = 0; index < length; index += size) {
6 | result.push(array.slice(index, index + size))
7 | }
8 | return result
9 | }
10 |
11 | export default chunk
12 |
--------------------------------------------------------------------------------
/stubs/resources/js/Utils/slug.js:
--------------------------------------------------------------------------------
1 | function slug(string) {
2 | return string
3 | .toLowerCase()
4 | .normalize('NFD') // Normalizes to decomposed form (NFD)
5 | .replace(/[\u0300-\u036f]/g, '') // Removes diacritics
6 | .replace(/ /g, '-')
7 | .replace(/[^\w-]+/g, '') // Existing replacements
8 | }
9 |
10 | export default slug
11 |
--------------------------------------------------------------------------------
/stubs/module-stub/modules/Http/Requests/ModuleValidate.stub:
--------------------------------------------------------------------------------
1 | 'required',
13 | ];
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/stubs/modules/Acl/Services/ListUserPermissions.php:
--------------------------------------------------------------------------------
1 | run($userId);
12 |
13 | return Arr::pluck($userPermissions, 'name');
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/stubs/modules/Support/Validators/required_editor.php:
--------------------------------------------------------------------------------
1 | ') {
7 | return true;
8 | }
9 |
10 | return false;
11 | }, trans('modular::validation.required_editor'));
12 |
--------------------------------------------------------------------------------
/stubs/module-stub/modules/ModuleServiceProvider.stub:
--------------------------------------------------------------------------------
1 | loadMigrationsFrom(__DIR__.'/Database/Migrations');
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/stubs/modules/Support/Models/BaseModel.php:
--------------------------------------------------------------------------------
1 | {
7 | if (auth && auth.isRootUser) {
8 | return true
9 | }
10 |
11 | return auth && auth.permissions.includes(permission)
12 | }
13 |
14 | return { can }
15 | }
16 |
--------------------------------------------------------------------------------
/stubs/module-stub/modules/Database/Seeders/ModuleSeeder.stub:
--------------------------------------------------------------------------------
1 | loadViewsFrom(__DIR__.'/views', 'index');
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "type": "module",
4 | "scripts": {
5 | "lint": "eslint \"**/*.{js,vue}\"",
6 | "format": "prettier --write ."
7 | },
8 | "devDependencies": {
9 | "eslint": "^9.5.0",
10 | "eslint-config-prettier": "^10.0.2",
11 | "eslint-plugin-vue": "^9.32.0",
12 | "prettier": "^3.4.2",
13 | "vue-eslint-parser": "^9.4.3"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/Console/InstallerTraits/ModuleExists.php:
--------------------------------------------------------------------------------
1 | moduleName}"))) {
10 | $this->components->error("Module {$this->moduleName} does not exist.");
11 |
12 | return false;
13 | }
14 |
15 | return true;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/stubs/module-stub/modules/Models/Model.stub:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
19 |
--------------------------------------------------------------------------------
/stubs/stack-configs/tailwind.config.mjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | content: [
3 | './vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php',
4 | './storage/framework/views/*.php',
5 | './resources/views/**/*.blade.php',
6 | './resources/js/**/*.vue',
7 | './resources-site/views/**/*.blade.php',
8 | './resources-site/js/**/*.vue',
9 | './modules/**/views/**/*.blade.php'
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/stubs/database/seeders/DatabaseSeeder.php:
--------------------------------------------------------------------------------
1 | call([
14 | AclRoleSeeder::class,
15 | AclPermissionSeeder::class,
16 | ]);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/stubs/modules/Support/Http/Controllers/AppController.php:
--------------------------------------------------------------------------------
1 | {
5 | const app = createApp({
6 | components: {
7 | //commonComponent,
8 | ...additionalComponents
9 | }
10 | })
11 |
12 | import.meta.glob(['../images/**'])
13 |
14 | return app
15 | }
16 |
--------------------------------------------------------------------------------
/stubs/resources/js/Components/Menu/AppBreadCrumbItem.vue:
--------------------------------------------------------------------------------
1 |
2 | {{ __(item.label) }}
3 |
4 |
5 | {{ __(item.label) }}
6 |
7 |
8 | >
9 |
10 |
11 |
19 |
--------------------------------------------------------------------------------
/stubs/stack-configs/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "semi": false,
4 | "trailingComma": "none",
5 | "arrowParens": "always",
6 |
7 | "tailwindConfig": "tailwind.config.mjs",
8 | "plugins": ["prettier-plugin-blade", "prettier-plugin-tailwindcss"],
9 |
10 | "overrides": [
11 | {
12 | "files": ["*.blade.php"],
13 | "options": {
14 | "parser": "blade"
15 | }
16 | }
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/stubs/tests/CreatesApplication.php:
--------------------------------------------------------------------------------
1 | make(Kernel::class)->bootstrap();
18 |
19 | return $app;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/Console/SiteTraits/PublishConfigFile.php:
--------------------------------------------------------------------------------
1 | info('The Modular config file already exists. Moving on...');
11 | } else {
12 | $this->call('vendor:publish', [
13 | '--tag' => 'modular-config',
14 | ]);
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/stubs/resources/js/Components/DataTable/AppDataTableHead.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | |
9 | {{ header }}
10 | |
11 |
12 |
13 |
14 |
15 |
23 |
--------------------------------------------------------------------------------
/stubs/modules/Support/Traits/ActivityLog.php:
--------------------------------------------------------------------------------
1 | logAll()
18 | ->logOnlyDirty()
19 | ->useLogName('system');
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/stubs/modules/Support/Traits/UpdateOrder.php:
--------------------------------------------------------------------------------
1 | $item) {
15 | if (is_array($item)) {
16 | $this->where('id', $item['id'])
17 | ->update([$this->getOrderFieldName() => $index]);
18 | }
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/stubs/modules/Support/Validators/recaptcha.php:
--------------------------------------------------------------------------------
1 | post('https://www.google.com/recaptcha/api/siteverify', [
9 | 'secret' => config('services.recaptcha.secret_key'),
10 | 'response' => $value,
11 | 'remoteip' => request()->ip(),
12 | ]);
13 |
14 | $body = $response->json();
15 |
16 | return $body['success'];
17 | });
18 |
--------------------------------------------------------------------------------
/stubs/modules/Acl/Database/Seeders/AclRoleSeeder.php:
--------------------------------------------------------------------------------
1 | 'root',
19 | 'guard_name' => 'user',
20 | ]);
21 |
22 | Schema::enableForeignKeyConstraints();
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/stubs/stack-configs/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "css.customData": [".vscode/tailwind.json"],
3 | "tailwindCSS.files.exclude": ["**/node_modules/**", "**/vendor/**"],
4 | "editor.formatOnSave": true,
5 | "editor.defaultFormatter": "esbenp.prettier-vscode",
6 | "[vue]": {
7 | "editor.defaultFormatter": "esbenp.prettier-vscode"
8 | },
9 | "[php]": {
10 | "editor.defaultFormatter": "open-southeners.laravel-pint"
11 | },
12 | "[blade]": {
13 | "editor.defaultFormatter": "esbenp.prettier-vscode"
14 | },
15 | "prettier.documentSelectors": ["**/*.blade.php"]
16 | }
17 |
--------------------------------------------------------------------------------
/stubs/modules/Support/Tests/FileNameGeneratorTraitTest.php:
--------------------------------------------------------------------------------
1 | image('A nice file.jpg');
12 |
13 | $readableName = $this->getReadableName($file);
14 | $fileName = $this->getFileName($file, $readableName, 'original');
15 |
16 | expect($fileName)->toBe('a-nice-file.jpg');
17 | });
18 |
--------------------------------------------------------------------------------
/stubs/resources/js/Plugins/Translations.js:
--------------------------------------------------------------------------------
1 | export default {
2 | install: (app) => {
3 | const __ = (key, replacements = {}) => {
4 | let translation = window._translations[key] || key
5 |
6 | Object.keys(replacements).forEach((replacement) => {
7 | translation = translation.replace(
8 | `:${replacement}`,
9 | replacements[replacement]
10 | )
11 | })
12 |
13 | return translation
14 | }
15 |
16 | app.config.globalProperties.__ = __
17 |
18 | app.provide('translate', __)
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/stubs/modules/Acl/config/config.php:
--------------------------------------------------------------------------------
1 | [
6 | // Menu Principal: Dashboard
7 | '1' => 'Menu Principal: Dashboard',
8 |
9 | // Menu Principal: Controles de Acesso
10 | '2' => 'Menu Principal: Controles de Acesso',
11 | '3' => 'Menu Principal: Controles de Acesso: Usuários - Listar',
12 | '4' => 'Menu Principal: Controles de Acesso: Permissões de Acesso - Listar',
13 | '5' => 'Menu Principal: Controles de Acesso: Perfis de Acesso - Listar',
14 | ],
15 |
16 | 'adminPermissions' => [
17 | '1', '2', '3', '4', '5',
18 | ],
19 |
20 | ];
21 |
--------------------------------------------------------------------------------
/stubs/resources/js/Components/Misc/AppCard.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/stubs/modules/Support/Http/Requests/JsonRequest.php:
--------------------------------------------------------------------------------
1 | json([
19 | 'errors' => $validator->errors(),
20 | ], 422));
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/stubs/modules/User/Observers/UserObserver.php:
--------------------------------------------------------------------------------
1 | filled('password')) {
13 | $user->password = Hash::make(request('password'));
14 | }
15 | }
16 |
17 | public function created(User $user)
18 | {
19 | if (! $user->profile_type) {
20 | $user->profile_type = 'user';
21 | $user->profile_id = $user->id;
22 | $user->save();
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/stubs/modules/Dashboard/DashboardServiceProvider.php:
--------------------------------------------------------------------------------
1 | 'Hello World!',
20 | // ]);
21 | // });
22 |
--------------------------------------------------------------------------------
/stubs/resources/js/Components/Form/AppFormErrors.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ __('Whoops! Something went wrong...') }}
5 |
6 |
7 |
10 |
11 |
12 |
13 |
21 |
--------------------------------------------------------------------------------
/stubs/resources/js/Composables/useClickOutside.js:
--------------------------------------------------------------------------------
1 | import { onBeforeUnmount, onMounted, ref } from 'vue'
2 |
3 | export default function useClickOutside(elementRef) {
4 | const isClickOutside = ref(false)
5 |
6 | const handler = (e) => {
7 | if (elementRef.value && !elementRef.value.contains(e.target)) {
8 | isClickOutside.value = true
9 | } else {
10 | isClickOutside.value = false
11 | }
12 | }
13 |
14 | onMounted(() => {
15 | document.addEventListener('click', handler)
16 | })
17 |
18 | onBeforeUnmount(() => {
19 | document.removeEventListener('click', handler)
20 | })
21 |
22 | return { isClickOutside }
23 | }
24 |
--------------------------------------------------------------------------------
/stubs/modules/Acl/Http/Requests/RoleValidate.php:
--------------------------------------------------------------------------------
1 | [
21 | 'required',
22 | 'string',
23 | 'min:2',
24 | 'max:255',
25 | Rule::unique(Role::class)->ignore($this->id),
26 | ],
27 |
28 | ];
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/stubs/modules/AdminAuth/Http/Middleware/UserAuth.php:
--------------------------------------------------------------------------------
1 | guest()) {
20 | return redirect()->route('adminAuth.loginForm')
21 | ->withErrors(['email' => 'Session expired, please login again.']);
22 | }
23 |
24 | return $next($request);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/stubs/lang/pt_BR/pagination.php:
--------------------------------------------------------------------------------
1 | '« Anterior',
17 | 'next' => 'Próxima »',
18 |
19 | 'Showing' => 'Mostrando',
20 | 'to' => 'até',
21 | 'of' => 'de',
22 | 'results' => 'resultados',
23 |
24 | ];
25 |
--------------------------------------------------------------------------------
/stubs/lang/bn/auth.php:
--------------------------------------------------------------------------------
1 | 'এই পরিচয়পত্র আমাদের রেকর্ডের সাথে মেলে না।',
17 | 'password' => 'পাসওয়ার্ড ভুল।',
18 | 'throttle' => 'লগইন করার জন্য অনেকবার চেষ্টা করেছেন, :seconds সেকেন্ড পরে পুনরায় চেষ্টা করুন।',
19 |
20 | ];
21 |
--------------------------------------------------------------------------------
/stubs/lang/bn/pagination.php:
--------------------------------------------------------------------------------
1 | '« পূর্বে',
17 | 'next' => 'পরবর্তি »',
18 |
19 | 'Showing' => 'দেখাচ্ছে',
20 | 'to' => 'পর্যন্ত',
21 | 'of' => 'এর মধ্যে',
22 | 'results' => 'ফলাফলসমুহ',
23 |
24 | ];
25 |
--------------------------------------------------------------------------------
/stubs/modules/AdminAuth/AdminAuthServiceProvider.php:
--------------------------------------------------------------------------------
1 | loadViewsFrom(__DIR__.'/views', 'admin-auth');
26 | parent::boot();
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/stubs/lang/pt_BR/auth.php:
--------------------------------------------------------------------------------
1 | 'Credenciais incorretas.',
17 | 'password' => 'A senha informada está incorreta.',
18 | 'throttle' => 'Tentativas de login excedidas. Por favor tente novamente em :seconds segundos.',
19 |
20 | ];
21 |
--------------------------------------------------------------------------------
/stubs/modules/Acl/Http/Requests/PermissionValidate.php:
--------------------------------------------------------------------------------
1 | [
21 | 'required',
22 | 'string',
23 | 'min:3',
24 | 'max:255',
25 | Rule::unique(Permission::class)->ignore($this->id),
26 | ],
27 |
28 | ];
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/stubs/modules/User/routes/app.php:
--------------------------------------------------------------------------------
1 | name('user.index');
9 |
10 | Route::get('user/create', [
11 | UserController::class, 'create',
12 | ])->name('user.create');
13 |
14 | Route::get('user/{id}/edit', [
15 | UserController::class, 'edit',
16 | ])->name('user.edit');
17 |
18 | Route::post('user', [
19 | UserController::class, 'store',
20 | ])->name('user.store');
21 |
22 | Route::put('user/{id}', [
23 | UserController::class, 'update',
24 | ])->name('user.update');
25 |
26 | Route::delete('user/{id}', [
27 | UserController::class, 'destroy',
28 | ])->name('user.destroy');
29 |
--------------------------------------------------------------------------------
/stubs/module-stub/modules/Database/Factories/ModelFactory.stub:
--------------------------------------------------------------------------------
1 | faker->unique()->sentence(4);
15 |
16 | return [
17 | 'name' => $name,
18 |
19 | 'created_at' => $this->faker->dateTimeBetween('-1 year', '-6 month'),
20 | 'updated_at' => $this->faker->dateTimeBetween('-5 month', 'now'),
21 | ];
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/stubs/resources/js/Composables/useTitle.js:
--------------------------------------------------------------------------------
1 | import useFormContext from '@/Composables/useFormContext'
2 | import { computed } from 'vue'
3 | import { inject } from 'vue'
4 |
5 | export default function useTitle(sectionName) {
6 | const translate = inject('translate')
7 |
8 | const { isCreate, isEdit } = useFormContext()
9 |
10 | const title = computed(() => {
11 | let prefix = ''
12 |
13 | if (isCreate.value) {
14 | prefix = 'Create'
15 | }
16 |
17 | if (isEdit.value) {
18 | prefix = 'Edit'
19 | }
20 |
21 | // let prefix = isCreate.value ? 'Create' : 'Edit'
22 | prefix = translate(prefix)
23 | return prefix + ' ' + translate(sectionName)
24 | })
25 |
26 | return { title }
27 | }
28 |
--------------------------------------------------------------------------------
/stubs/resources/js/Components/Form/TipTap/TipTapButton.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 |
25 |
--------------------------------------------------------------------------------
/stubs/modules/Acl/Http/Controllers/UserController.php:
--------------------------------------------------------------------------------
1 | orderBy('name')->get();
14 |
15 | return compact('users');
16 | }
17 |
18 | public function getUserRolesAndPermissions(Request $request)
19 | {
20 | $user = auth()->guard('user')->user();
21 |
22 | return [
23 | 'userRoles' => $user->getRoleNames(),
24 | 'userPermissions' => $user->getPermissionsViaRoles()->pluck('name'),
25 | ];
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/stubs/modules/Support/SupportServiceProvider.php:
--------------------------------------------------------------------------------
1 | id();
16 | $table->string('name');
17 | $table->timestamps();
18 | $table->softDeletes();
19 | });
20 | }
21 |
22 | /**
23 | * Reverse the migrations.
24 | */
25 | public function down(): void
26 | {
27 | Schema::dropIfExists('${{ resourceName }}');
28 | }
29 | };
30 |
--------------------------------------------------------------------------------
/stubs/modules/Dashboard/Http/Controllers/DashboardController.php:
--------------------------------------------------------------------------------
1 | User::count(),
22 | 'permissions' => Permission::count(),
23 | 'roles' => Role::count(),
24 | ];
25 |
26 | return Inertia::render('Dashboard/DashboardIndex', [
27 | 'count' => $count,
28 | ]);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/stubs/modules/Support/Traits/UploadFile.php:
--------------------------------------------------------------------------------
1 | file($inputFileName);
14 |
15 | if (! $file or ! $file instanceof UploadedFile) {
16 | return [];
17 | }
18 |
19 | $readableName = $this->getReadableName($file);
20 | $fileName = $this->getFileName($file, $readableName, $nameStrategy);
21 |
22 | $file->storeAs(
23 | $path,
24 | $fileName,
25 | $disc,
26 | );
27 |
28 | return [$inputFileName => $fileName];
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/stubs/resources/js/Components/Form/AppInputText.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
32 |
--------------------------------------------------------------------------------
/stubs/lang/pt_BR/passwords.php:
--------------------------------------------------------------------------------
1 | 'Sua senha foi redefinida!',
17 | 'sent' => 'Enviamos um email para redefinição da sua senha!',
18 | 'throttled' => 'Por favor aguarde para tentar novamente.',
19 | 'token' => 'O token para redefinição da senha é inválido.',
20 | 'user' => 'Nós não encontramos um usuário com este email definido.',
21 |
22 | ];
23 |
--------------------------------------------------------------------------------
/stubs/resources/js/Components/DataTable/AppDataTable.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
29 |
--------------------------------------------------------------------------------
/stubs/modules/Support/Tests/UploadFileTraitTest.php:
--------------------------------------------------------------------------------
1 | deleteDirectory(storage_path('user-files'));
12 | });
13 |
14 | afterAll(function () {
15 | (new Filesystem)->deleteDirectory(storage_path('user-files'));
16 | });
17 |
18 | it('can handle requests without uploads', function () {
19 | $result = $this->uploadFile('');
20 |
21 | expect($result)->toBe([]);
22 | });
23 |
24 | it('can handle requests with wrong inputs', function () {
25 | request('file', 'not an uploaded file');
26 |
27 | $result = $this->uploadFile('file');
28 |
29 | expect($result)->toBe([]);
30 | });
31 |
--------------------------------------------------------------------------------
/rector.php:
--------------------------------------------------------------------------------
1 | paths([
11 | // __DIR__.'/config',
12 | // __DIR__.'/resources',
13 | // __DIR__.'/src',
14 | // __DIR__.'/tests',
15 | // __DIR__.'/stubs/app',
16 | // __DIR__.'/stubs/config',
17 | // __DIR__.'/stubs/database',
18 | // __DIR__.'/stubs/lang',
19 | // __DIR__.'/stubs/routes',
20 | ]);
21 |
22 | // register a single rule
23 | // $rectorConfig->rule(InlineConstructorDefaultToPropertyRector::class);
24 |
25 | // define sets of rules
26 | $rectorConfig->sets([
27 | LevelSetList::UP_TO_PHP_82,
28 | ]);
29 | };
30 |
--------------------------------------------------------------------------------
/stubs/resources/js/Components/Misc/AppSectionHeader.vue:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 |
31 |
--------------------------------------------------------------------------------
/stubs/lang/bn/passwords.php:
--------------------------------------------------------------------------------
1 | 'আপনার পাসওয়ার্ড পুনরায় সেট করা হয়েছে!',
17 | 'sent' => 'আমরা আপনার পাসওয়ার্ড পুনরায় সেট করার লিঙ্ক ই-মেইল করেছি!',
18 | 'throttle' => 'লগইন করার জন্য অনেকবার চেষ্টা করেছেন, :seconds সেকেন্ড পরে পুনরায় চেষ্টা করুন।',
19 | 'token' => 'এই পাসওয়ার্ড রিসেট টোকেনটি সঠিক নয়।',
20 | 'user' => 'এই ই-মেইল দিয়ে কোন ব্যবহারকারী খুঁজে পাওয়া যাচ্ছে না',
21 |
22 | ];
23 |
--------------------------------------------------------------------------------
/stubs/modules/Support/Tests/EditorImageTraitTest.php:
--------------------------------------------------------------------------------
1 | file = UploadedFile::fake()->image('A nice file.jpg');
15 | });
16 |
17 | it('can upload an image to the default editor media file path', function () {
18 | $result = $this->uploadImage($this->file, 'original');
19 |
20 | expect($result)->toHaveKeys(['fileName', 'filePath', 'fileUrl', 'readableName']);
21 | expect($result['readableName'])->toBe('A nice file');
22 | assertFileExists($result['filePath']);
23 |
24 | (new Filesystem)->delete($result['filePath']);
25 | });
26 |
--------------------------------------------------------------------------------
/src/Console/PublishSiteFilesCommand.php:
--------------------------------------------------------------------------------
1 | publishModularConfigFile();
21 |
22 | $this->copySupportModuleFiles();
23 | $this->copyIndexModuleDir();
24 |
25 | $this->copyResourcesSiteDir();
26 |
27 | $this->configureViews();
28 |
29 | return self::SUCCESS;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/stubs/modules/Acl/AclServiceProvider.php:
--------------------------------------------------------------------------------
1 | mergeConfigFrom(__DIR__.'/config/config.php', 'acl');
25 |
26 | Gate::before(function ($user, $ability) {
27 | return $user->hasRole('root') ? true : null; // Must be null, not false. (From Spatie Permission Package documentation.)
28 | });
29 |
30 | parent::boot();
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/stubs/modules/Support/Traits/FileNameGenerator.php:
--------------------------------------------------------------------------------
1 | getClientOriginalExtension();
14 | }
15 |
16 | if ($nameStrategy == 'originalUUID') {
17 | return Str::slug($readableName).'-'.Str::uuid().'.'.$file->getClientOriginalExtension();
18 | }
19 |
20 | if ($nameStrategy == 'hash') {
21 | return $file->hashName();
22 | }
23 | }
24 |
25 | private function getReadableName(UploadedFile $file): string
26 | {
27 | return str_replace('.'.$file->getClientOriginalExtension(), '', $file->getClientOriginalName());
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/stubs/modules/Acl/Services/GetUserPermissions.php:
--------------------------------------------------------------------------------
1 | function ($query) {
13 | $query->get(['id', 'name']);
14 | }])->findOrFail($userId);
15 |
16 | // if has direct permissions use it
17 | if ($user->permissions->count()) {
18 | return $this->mapPermissions($user->permissions);
19 | }
20 |
21 | // get the permissions via roles
22 | return $this->mapPermissions($user->getAllPermissions());
23 | }
24 |
25 | private function mapPermissions(Collection $permissions): array
26 | {
27 | return $permissions->map(fn ($permission) => [
28 | 'id' => $permission->id,
29 | 'name' => $permission->name,
30 | ])->toArray();
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/stubs/stack-configs/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import tailwindcss from '@tailwindcss/vite'
3 | import laravel from 'laravel-vite-plugin'
4 | import vue from '@vitejs/plugin-vue'
5 |
6 | import Components from 'unplugin-vue-components/vite'
7 | import AppComponentsResolver from './resources/js/Resolvers/AppComponentsResolver.js'
8 |
9 | export default defineConfig({
10 | plugins: [
11 | tailwindcss(),
12 | laravel({
13 | input: ['resources/js/app.js'],
14 | refresh: ['resources/**/*']
15 | }),
16 | vue({
17 | template: {
18 | transformAssetUrls: {
19 | base: null,
20 | includeAbsolute: false
21 | }
22 | }
23 | }),
24 | Components({
25 | resolvers: [AppComponentsResolver]
26 | })
27 | ],
28 | resolve: {
29 | alias: {
30 | '@resources': '/resources'
31 | }
32 | }
33 | })
34 |
--------------------------------------------------------------------------------
/stubs/modules/User/Database/Factories/UserFactory.php:
--------------------------------------------------------------------------------
1 | fake()->name(),
17 | 'email' => fake()->unique()->safeEmail(),
18 | 'email_verified_at' => now(),
19 | 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
20 | 'remember_token' => Str::random(10),
21 | ];
22 | }
23 |
24 | /**
25 | * Indicate that the model's email address should be unverified.
26 | */
27 | public function unverified(): static
28 | {
29 | return $this->state(fn (array $attributes) => [
30 | 'email_verified_at' => null,
31 | ]);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/stubs/modules/User/Database/Migrations/2024_01_25_000000_add_custom_fields_to_users_table.php:
--------------------------------------------------------------------------------
1 | string('profile_type')->after('remember_token')->nullable();
15 | $table->unsignedBigInteger('profile_id')->after('profile_type')->nullable();
16 | $table->timestamp('deleted_at')->nullable()->after('updated_at');
17 | });
18 | }
19 |
20 | public function down(): void
21 | {
22 | Schema::table('users', function (Blueprint $table) {
23 | $table->dropColumn('profile_type');
24 | $table->dropColumn('profile_id');
25 | $table->dropColumn('deleted_at');
26 | });
27 | }
28 | };
29 |
--------------------------------------------------------------------------------
/stubs/resources/js/Components/Menu/AppMenuItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 | {{ __(menuItem.label) }}
10 |
11 |
12 |
16 | {{ __(menuItem.label) }}
17 |
18 |
19 |
20 |
21 |
22 |
23 |
35 |
--------------------------------------------------------------------------------
/src/Console/SiteTraits/ConfigureViews.php:
--------------------------------------------------------------------------------
1 | call('config:publish', [
13 | 'name' => 'view',
14 | ]);
15 | }
16 |
17 | $this->info('Setting config/view.php...');
18 |
19 | $viewConfig = file_get_contents($configViewFilePath);
20 |
21 | $strSearch = "resource_path('views'),";
22 | $strReplace = "resource_path('views'),".PHP_EOL." base_path('resources-site/views'),";
23 |
24 | $updatedViewConfig = str_replace(
25 | $strSearch,
26 | $strReplace,
27 | $viewConfig
28 | );
29 |
30 | file_put_contents($configViewFilePath, $updatedViewConfig);
31 |
32 | $this->info('The config/view.php file was updated.');
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/stubs/module-stub/modules/routes/app.stub:
--------------------------------------------------------------------------------
1 | name('{{ resourceName }}.index');
9 |
10 | Route::get('{{ resource-name }}/create', [
11 | {{ ResourceName }}Controller::class, 'create',
12 | ])->name('{{ resourceName }}.create');
13 |
14 | Route::post('{{ resource-name }}', [
15 | {{ ResourceName }}Controller::class, 'store',
16 | ])->name('{{ resourceName }}.store');
17 |
18 | Route::get('{{ resource-name }}/{id}/edit', [
19 | {{ ResourceName }}Controller::class, 'edit',
20 | ])->name('{{ resourceName }}.edit');
21 |
22 | Route::put('{{ resource-name }}/{id}', [
23 | {{ ResourceName }}Controller::class, 'update',
24 | ])->name('{{ resourceName }}.update');
25 |
26 | Route::delete('{{ resource-name }}/{id}', [
27 | {{ ResourceName }}Controller::class, 'destroy',
28 | ])->name('{{ resourceName }}.destroy');
29 |
--------------------------------------------------------------------------------
/stubs/site/modules/Index/views/index.blade.php:
--------------------------------------------------------------------------------
1 | @extends('site-layout')
2 |
3 | @section('meta-title', 'Modular: Ready to build')
4 |
5 | @section('meta-description', 'Your amazing site')
6 |
7 | @section('bodyEndScripts')
8 | @vite('resources-site/js/index-app.js')
9 | @endsection
10 |
11 | @section('content')
12 |
13 |
14 |
15 |
16 |
17 |
18 |
 }})
19 |
20 |
21 |
Modular: Ready to build
22 |
23 |
Your amazing site
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | @endsection
32 |
--------------------------------------------------------------------------------
/stubs/modules/User/Http/Requests/UserValidate.php:
--------------------------------------------------------------------------------
1 | 'required',
19 | 'email' => 'required|email',
20 | 'password' => $this->passwordRules(),
21 | 'profile_type' => 'string',
22 | 'profile_id' => 'integer|numeric',
23 | ];
24 | }
25 |
26 | private function passwordRules()
27 | {
28 | $rules = [Password::min(8)];
29 |
30 | if (request()->isMethod('post')) {
31 | array_unshift($rules, ['required']);
32 | }
33 |
34 | if (request()->isMethod('put') and request()->isEmptyString('password')) {
35 | return [];
36 | }
37 |
38 | return $rules;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/stubs/site/resources-site/views/site-layout.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | @yield('meta-title', config('app.name', 'Modular'))
9 |
10 |
14 |
15 |
16 | @if (app()->environment('production'))
17 |
21 | @endif
22 |
23 |
24 |
25 |
26 |
27 | @vite(['resources-site/css/site.css'])
28 | @yield('headEndScripts')
29 |
30 |
31 |
32 |
33 | @yield('content')
34 |
35 |
36 | @yield('bodyEndScripts')
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/stubs/modules/Acl/Http/Controllers/UserRoleController.php:
--------------------------------------------------------------------------------
1 | function ($q) {
14 | $q->get(['id', 'name']);
15 | }])->findOrFail($id);
16 |
17 | $user->roles->map(function ($role) {
18 | unset($role->pivot);
19 |
20 | return $role;
21 | });
22 |
23 | $roles = Role::orderBy('name')->get(['id', 'name']);
24 |
25 | return inertia('AclUserRole/UserRoleForm', [
26 | 'user' => $user,
27 | 'roles' => $roles,
28 | ]);
29 | }
30 |
31 | public function update($id)
32 | {
33 | $user = User::findOrFail($id);
34 |
35 | $user->syncRoles(request('userRoles'));
36 |
37 | return redirect()->route('user.index')
38 | ->with('success', 'User roles updated');
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/stubs/modules/Acl/Http/Controllers/UserPermissionController.php:
--------------------------------------------------------------------------------
1 | run($id);
16 |
17 | $permissions = Permission::orderBy('name')->get(['id', 'name']);
18 |
19 | return inertia('AclUserPermission/UserPermissionForm', [
20 | 'user' => $user,
21 | 'userPermissions' => $userPermissions,
22 | 'permissions' => $permissions,
23 | ]);
24 | }
25 |
26 | public function update($id)
27 | {
28 | $user = User::findOrFail($id);
29 |
30 | $user->syncPermissions(request('userPermissions'));
31 |
32 | return redirect()->route('user.index')
33 | ->with('success', 'User permissions updated');
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/stubs/modules/Support/Tests/ActivityLogTraitTest.php:
--------------------------------------------------------------------------------
1 | id();
15 | $table->string('name')->nullable();
16 | $table->timestamps();
17 | });
18 | });
19 |
20 | it('logs the activity for the model', function () {
21 | $model = new class extends Model
22 | {
23 | use ActivityLog;
24 |
25 | protected $table = 'items';
26 |
27 | protected $guarded = [];
28 | };
29 |
30 | $item1 = $model->create(['name' => 'Item 1']);
31 |
32 | $item1->delete();
33 |
34 | $this->assertDatabaseHas('activity_log', [
35 | 'subject_id' => $item1->id,
36 | 'subject_type' => get_class($item1),
37 | 'description' => 'deleted',
38 | ]);
39 | });
40 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) Daniel Coimbra Cintra
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/stubs/modules/User/UserServiceProvider.php:
--------------------------------------------------------------------------------
1 | loadMigrationsFrom(__DIR__.'/Database/Migrations');
30 |
31 | Relation::morphMap([
32 | 'user' => User::class,
33 | ]);
34 |
35 | User::observe(UserObserver::class);
36 |
37 | $this->commands([
38 | CreateUserCommand::class,
39 | ]);
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/stubs/resources/js/Components/Form/AppInputDate.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
15 |
16 |
17 |
39 |
40 |
45 |
--------------------------------------------------------------------------------
/stubs/resources/views/app.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | {{-- If developing using SSL/HTTPS (uncomment the line below): Enforces loading all resources over HTTPS, upgrading requests from HTTP to HTTPS for enhanced security --}}
9 | {{-- --}}
10 |
11 | {{ config('app.name', 'Laravel') }}
12 |
13 | {{-- used by Tiptap Editor --}}
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | @routes
23 | @vite('resources/js/app.js')
24 | @inertiaHead
25 |
26 |
27 |
28 | @inertia
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/stubs/resources/js/Configs/menu.js:
--------------------------------------------------------------------------------
1 | export default {
2 | // main navigation - side menu
3 | items: [
4 | {
5 | label: 'Dashboard',
6 | permission: 'Dashboard',
7 | icon: 'ri-dashboard-line',
8 | link: route('dashboard.index')
9 | },
10 |
11 | {
12 | label: 'Access Control List',
13 | permission: 'Acl',
14 | children: [
15 | {
16 | label: 'Users',
17 | permission: 'Acl: User - List',
18 | icon: 'ri-user-line',
19 | link: route('user.index')
20 | },
21 | {
22 | label: 'Permissions',
23 | permission: 'Acl: Permission - List',
24 | icon: 'ri-shield-keyhole-line',
25 | link: route('aclPermission.index')
26 | },
27 | {
28 | label: 'Roles',
29 | permission: 'Acl: Role - List',
30 | icon: 'ri-account-box-line',
31 | link: route('aclRole.index')
32 | }
33 | ]
34 | }
35 | ]
36 | }
37 |
--------------------------------------------------------------------------------
/stubs/resources/js/Components/Form/AppRadioButton.vue:
--------------------------------------------------------------------------------
1 |
2 |
18 |
19 |
20 |
48 |
--------------------------------------------------------------------------------
/stubs/modules/Acl/Http/Controllers/RolePermissionController.php:
--------------------------------------------------------------------------------
1 | function ($q) {
14 | $q->get(['id', 'name']);
15 | }])->findOrFail($id);
16 |
17 | $role->permissions->map(function ($permission) {
18 | unset($permission->pivot);
19 |
20 | return $permission;
21 | });
22 |
23 | $permissions = Permission::orderBy('name')->get(['id', 'name']);
24 |
25 | return inertia('AclRolePermission/RolePermissionForm', [
26 | 'role' => $role,
27 | 'permissions' => $permissions,
28 | ]);
29 | }
30 |
31 | public function update($id)
32 | {
33 | $role = Role::findOrFail($id);
34 |
35 | $role->syncPermissions(request('rolePermissions'));
36 |
37 | return redirect()->route('aclRole.index')
38 | ->with('success', 'Role permissions updated');
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/stubs/modules/Support/Tests/SearchableTraitTest.php:
--------------------------------------------------------------------------------
1 | user = User::factory()->create([
12 | 'name' => 'John Doe',
13 | 'email' => 'doe@gmail.com',
14 | 'password' => 'secret',
15 | ]);
16 | });
17 |
18 | it('returns empty collection if no result is found', function () {
19 | $result = User::search('name,email', 'Jane')->get();
20 |
21 | expect($result)->toHaveCount(0);
22 | });
23 |
24 | it('can search a single database column', function () {
25 | $stringToSearch = 'John';
26 |
27 | $result = User::search('name', $stringToSearch)->get();
28 |
29 | expect($result)->toHaveCount(1);
30 | expect($result->first()->name)->toBe('John Doe');
31 | });
32 |
33 | it('can search multiple database columns', function () {
34 | $stringToSearch = 'doe';
35 |
36 | $result = User::search('name,email', $stringToSearch)->get();
37 |
38 | expect($result)->toHaveCount(1);
39 | expect($result->first()->name)->toBe('John Doe');
40 | });
41 |
--------------------------------------------------------------------------------
/stubs/modules/AdminAuth/Notifications/ResetPassword.php:
--------------------------------------------------------------------------------
1 | $this->token,
36 | 'email' => $notifiable->getEmailForPasswordReset(),
37 | ];
38 |
39 | return (new MailMessage)->markdown(
40 | 'admin-auth::emails.reset-password',
41 | [
42 | 'url' => url(route('adminAuth.resetPasswordForm', $params, false)),
43 | ]
44 | );
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/stubs/resources/js/Components/Message/AppAlert.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
30 |
31 |
54 |
--------------------------------------------------------------------------------
/src/Console/PublishLaravelTranslationsCommand.php:
--------------------------------------------------------------------------------
1 | option('lang');
19 |
20 | $availableLangs = ['pt_BR'];
21 |
22 | if (! in_array($lang, $availableLangs)) {
23 | $this->error("The language '{$lang}' is not available. Available languages: ".implode(', ', $availableLangs));
24 |
25 | return self::FAILURE;
26 | }
27 |
28 | $this->publishLang($lang);
29 |
30 | return self::SUCCESS;
31 | }
32 |
33 | private function publishLang(string $lang): void
34 | {
35 | $filesystem = new Filesystem;
36 |
37 | $filesystem->copyDirectory(
38 | __DIR__."/../../stubs/lang/{$lang}",
39 | lang_path("{$lang}")
40 | );
41 |
42 | $this->info("The language files for '{$lang}' were published successfully.");
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/stubs/modules/Support/Traits/Searchable.php:
--------------------------------------------------------------------------------
1 | when($searchTerm, function ($query) use ($columns, $searchTerm) {
24 | $query->where(function ($query) use ($columns, $searchTerm) {
25 | foreach ($columns as $index => $column) {
26 | // Use where for the first column to start the group, then orWhere for subsequent columns
27 | if ($index === 0) {
28 | $query->where($column, 'like', "%{$searchTerm}%");
29 | } else {
30 | $query->orWhere($column, 'like', "%{$searchTerm}%");
31 | }
32 | }
33 | });
34 | });
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/stubs/resources/js/Components/Misc/AppButton.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
10 |
11 |
46 |
--------------------------------------------------------------------------------
/stubs/modules/Support/Tests/UpdateOrderTraitTest.php:
--------------------------------------------------------------------------------
1 | id();
15 | $table->string('name')->nullable();
16 | $table->unsignedTinyInteger('order')->nullable();
17 | $table->timestamps();
18 | });
19 | });
20 |
21 | it('can update the order of the model items', function () {
22 | $model = new class extends Model
23 | {
24 | use UpdateOrder;
25 |
26 | protected $table = 'items';
27 |
28 | protected $guarded = [];
29 | };
30 |
31 | $item1 = $model->create(['name' => 'Item 1', 'order' => 0]);
32 | $item2 = $model->create(['name' => 'Item 2', 'order' => 1]);
33 |
34 | $newOrder = [
35 | ['id' => $item2->id],
36 | ['id' => $item1->id],
37 | ];
38 |
39 | $model->updateOrder($newOrder);
40 |
41 | $this->assertEquals(0, $model->find($item2->id)->order);
42 | $this->assertEquals(1, $model->find($item1->id)->order);
43 | });
44 |
--------------------------------------------------------------------------------
/stubs/modules/Dashboard/Tests/DashboardTest.php:
--------------------------------------------------------------------------------
1 | user = User::factory()->create();
12 | $this->loggedRequest = $this->actingAs($this->user);
13 | });
14 |
15 | it('can render the dashboard page', function () {
16 | $dashboardUrl = route(config('modular.default-logged-route'));
17 | $response = $this->loggedRequest->get($dashboardUrl);
18 |
19 | $response->assertStatus(200);
20 |
21 | $response->assertInertia(
22 | fn (Assert $page) => $page
23 | ->component('Dashboard/DashboardIndex')
24 | ->has(
25 | 'auth.user',
26 | fn (Assert $page) => $page
27 | ->where('id', $this->user->id)
28 | ->where('name', $this->user->name)
29 | ->etc()
30 | )
31 | ->has(
32 | 'auth.permissions',
33 | )
34 | ->has(
35 | 'errors',
36 | )
37 | ->has(
38 | 'flash',
39 | 2,
40 | )
41 | ->has(
42 | 'ziggy.routes',
43 | )
44 | );
45 | });
46 |
--------------------------------------------------------------------------------
/stubs/resources/js/app.js:
--------------------------------------------------------------------------------
1 | import '../css/app.css'
2 | import 'remixicon/fonts/remixicon.css'
3 |
4 | import { createApp, h } from 'vue'
5 | import { createInertiaApp } from '@inertiajs/vue3'
6 | import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers'
7 | import { ZiggyVue } from '../../vendor/tightenco/ziggy/dist/index.js'
8 |
9 | const appName = import.meta.env.VITE_APP_NAME || 'Laravel'
10 |
11 | // global components
12 | import { Link } from '@inertiajs/vue3'
13 | import Layout from './Layouts/AuthenticatedLayout.vue'
14 |
15 | import Translations from '@/Plugins/Translations'
16 |
17 | createInertiaApp({
18 | title: (title) => `${title} - ${appName}`,
19 | resolve: (name) => {
20 | const page = resolvePageComponent(
21 | `./Pages/${name}.vue`,
22 | import.meta.glob('./Pages/**/*.vue')
23 | )
24 |
25 | page.then((module) => {
26 | module.default.layout = module.default.layout || Layout
27 | })
28 |
29 | return page
30 | },
31 | setup({ el, App, props, plugin }) {
32 | return createApp({ render: () => h(App, props) })
33 | .use(plugin)
34 | .use(ZiggyVue, Ziggy) // eslint-disable-line no-undef
35 | .use(Translations)
36 | .component('Link', Link)
37 | .mount(el)
38 | },
39 | progress: {
40 | color: '#3e63dd'
41 | }
42 | })
43 |
--------------------------------------------------------------------------------
/stubs/stack-configs/eslint.config.js:
--------------------------------------------------------------------------------
1 | import js from '@eslint/js'
2 | import vue from 'eslint-plugin-vue'
3 | import prettierConfig from 'eslint-config-prettier'
4 | import globals from 'globals'
5 |
6 | export default [
7 | // Base ESLint recommended rules
8 | js.configs.recommended,
9 |
10 | // Vue recommended rules (uses flat config)
11 | ...vue.configs['flat/recommended'],
12 |
13 | // Prettier configuration to disable conflicting rules
14 | {
15 | rules: {
16 | ...prettierConfig.rules,
17 | 'unicode-bom': 'off'
18 | }
19 | },
20 |
21 | // Vue specific rule overrides
22 | {
23 | files: ['**/*.vue'],
24 | rules: {
25 | '@stylistic/js/indent': 'off',
26 | '@stylistic/js/quotes': 'off',
27 |
28 | // Disable specific Vue rules
29 | 'vue/no-v-html': 'off',
30 | 'vue/comment-directive': 'off',
31 | 'vue/no-v-text-v-html-on-component': 'off'
32 | }
33 | },
34 |
35 | // Global variables
36 | {
37 | languageOptions: {
38 | globals: {
39 | ...globals.browser,
40 | route: 'readonly',
41 | grecaptcha: 'readonly'
42 | }
43 | }
44 | },
45 |
46 | // Ignore patterns
47 | {
48 | ignores: ['node_modules/*', 'vendor/*', 'public/*']
49 | }
50 | ]
51 |
--------------------------------------------------------------------------------
/stubs/modules/AdminAuth/Tests/AuthenticationTest.php:
--------------------------------------------------------------------------------
1 | get($loginRoute);
13 |
14 | $response->assertStatus(200);
15 | });
16 |
17 | test('users can authenticate using the login screen', function () {
18 | $user = User::factory()->create();
19 |
20 | $response = $this->post('/admin-auth/login', [
21 | 'email' => $user->email,
22 | 'password' => 'password',
23 | ]);
24 |
25 | $this->assertAuthenticated();
26 | $response->assertRedirect('/admin/dashboard');
27 | });
28 |
29 | test('users can not authenticate with invalid password', function () {
30 | $user = User::factory()->create();
31 |
32 | $this->post('/login', [
33 | 'email' => $user->email,
34 | 'password' => 'wrong-password',
35 | ]);
36 |
37 | $this->assertGuest();
38 | });
39 |
40 | test('users can logout', function () {
41 | $loginRoute = config('modular.login-url');
42 | $user = User::factory()->create();
43 |
44 | $response = $this->actingAs($user)->get('/admin-auth/logout');
45 |
46 | $this->assertGuest();
47 |
48 | $response->assertRedirect($loginRoute);
49 | });
50 |
--------------------------------------------------------------------------------
/stubs/modules/AdminAuth/routes/site.php:
--------------------------------------------------------------------------------
1 | name('adminAuth.loginForm');
11 |
12 | Route::post('/admin-auth/login', [
13 | AuthenticatedSessionController::class, 'login',
14 | ])->name('adminAuth.login');
15 |
16 | Route::get('/admin-auth/logout', [
17 | AuthenticatedSessionController::class, 'logout',
18 | ])->name('adminAuth.logout');
19 |
20 | // form to receive the email that contains the link to reset password
21 | Route::get('/admin-auth/forgot-password', [
22 | PasswordResetLinkController::class, 'forgotPasswordForm',
23 | ])->name('adminAuth.forgotPassword');
24 |
25 | Route::post('/admin-auth/send-reset-link-email', [
26 | PasswordResetLinkController::class, 'sendResetLinkEmail',
27 | ])->name('adminAuth.sendResetLinkEmail');
28 |
29 | // password reset form
30 | Route::get('/admin-auth/reset-password/{token}', [
31 | NewPasswordController::class, 'resetPasswordForm',
32 | ])->name('adminAuth.resetPasswordForm');
33 |
34 | Route::post('/admin-auth/reset-password', [
35 | NewPasswordController::class, 'store',
36 | ])->name('adminAuth.resetPassword');
37 |
--------------------------------------------------------------------------------
/src/Console/MakeMigrationCommand.php:
--------------------------------------------------------------------------------
1 | moduleName = Str::studly($this->argument('moduleName'));
25 | $this->migrationName = $this->argument('migrationName');
26 |
27 | if (! $this->moduleExists()) {
28 | return self::FAILURE;
29 | }
30 |
31 | $this->comment('Module '.$this->moduleName.' found, creating migration file...');
32 | $this->createMigrationFile();
33 |
34 | return self::SUCCESS;
35 | }
36 |
37 | private function createMigrationFile(): void
38 | {
39 | (new Filesystem)->ensureDirectoryExists(base_path("modules/{$this->moduleName}/Database/Migrations"));
40 |
41 | $this->call('make:migration', [
42 | 'name' => $this->migrationName,
43 | '--path' => "modules/{$this->moduleName}/Database/Migrations",
44 | ]);
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/stubs/resources/js/Components/Form/AppInputPassword.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
11 |
14 |
19 |
20 |
21 |
22 |
23 |
48 |
--------------------------------------------------------------------------------
/src/Console/SiteTraits/CopySiteFiles.php:
--------------------------------------------------------------------------------
1 | info('Copying Support Module files...');
12 |
13 | copy(__DIR__.'/../../../stubs/site/modules/Support/SiteController.php', base_path('modules/Support/Http/Controllers/SiteController.php'));
14 | copy(__DIR__.'/../../../stubs/site/modules/Support/SiteModel.php', base_path('modules/Support/Models/SiteModel.php'));
15 |
16 | $this->info('Support Module files copied.');
17 | }
18 |
19 | private function copyResourcesSiteDir(): void
20 | {
21 | $this->info('Copying resources-site directory...');
22 |
23 | (new Filesystem)->ensureDirectoryExists(base_path('resources-site'));
24 | (new Filesystem)->copyDirectory(__DIR__.'/../../../stubs/site/resources-site', base_path('resources-site'));
25 |
26 | $this->info('The resources-site directory was copied.');
27 | }
28 |
29 | private function copyIndexModuleDir(): void
30 | {
31 | $this->info('Copying Index Module directory...');
32 |
33 | (new Filesystem)->ensureDirectoryExists(base_path('modules/Index'));
34 | (new Filesystem)->copyDirectory(__DIR__.'/../../../stubs/site/modules/Index', base_path('modules/Index'));
35 |
36 | $this->info('The Index Module directory was copied.');
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/Console/MakeTestCommand.php:
--------------------------------------------------------------------------------
1 | moduleName = Str::studly($this->argument('moduleName'));
25 | $this->testFileName = $this->argument('resourceName');
26 |
27 | if (! $this->moduleExists()) {
28 | return self::FAILURE;
29 | }
30 |
31 | $this->comment('Module '.$this->moduleName.' found, creating test file...');
32 | $this->createTestFile();
33 |
34 | return self::SUCCESS;
35 | }
36 |
37 | private function createTestFile(): void
38 | {
39 | (new Filesystem)->ensureDirectoryExists(base_path("modules/{$this->moduleName}/Tests/"));
40 |
41 | $stub = file_get_contents(__DIR__.'/../../stubs/module-stub/modules/Tests/ModuleTest.stub');
42 |
43 | $path = base_path("modules/{$this->moduleName}/Tests/{$this->testFileName}Test.php");
44 |
45 | file_put_contents($path, $stub);
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/Console/InstallCommand.php:
--------------------------------------------------------------------------------
1 | components->info('Installing required stacks...');
23 |
24 | $this->setupBackendPackages();
25 |
26 | $this->installFrontendPackages();
27 |
28 | $this->components->info('Required stacks installed!');
29 |
30 | $this->configureCoreModules();
31 |
32 | $this->setupPestTests();
33 |
34 | $this->info('🎉 Modular successfully installed!');
35 |
36 | $this->info('🚀 To create your first user, please run: php artisan modular:create-user');
37 |
38 | return self::SUCCESS;
39 | }
40 |
41 | protected function phpBinary(): string
42 | {
43 | return (new PhpExecutableFinder)->find(false) ?: 'php';
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/stubs/resources/js/Resolvers/AppComponentsResolver.js:
--------------------------------------------------------------------------------
1 | const componentGroups = {
2 | Auth: ['AppAuthLogo', 'AppAuthShell'],
3 | DataTable: [
4 | 'AppDataSearch',
5 | 'AppDataTable',
6 | 'AppDataTableData',
7 | 'AppDataTableHead',
8 | 'AppDataTableRow',
9 | 'AppPaginator'
10 | ],
11 | Form: [
12 | 'AppCheckbox',
13 | 'AppCombobox',
14 | 'AppDataSearch',
15 | 'AppFormErrors',
16 | 'AppInputDate',
17 | 'AppInputFile',
18 | 'AppInputPassword',
19 | 'AppInputText',
20 | 'AppLabel',
21 | 'AppMultiCombobox',
22 | 'AppRadioButton',
23 | 'AppTextArea',
24 | 'AppTipTapEditor'
25 | ],
26 | Menu: [
27 | 'AppBreadCrumb',
28 | 'AppBreadCrumbItem',
29 | 'AppMenu',
30 | 'AppMenuItem',
31 | 'AppMenuSection'
32 | ],
33 | Message: ['AppAlert', 'AppFlashMessage', 'AppToast', 'AppTooltip'],
34 | Misc: ['AppButton', 'AppCard', 'AppLink', 'AppSectionHeader', 'AppTopBar'],
35 | Overlay: ['AppConfirmDialog', 'AppModal', 'AppSideBar']
36 | }
37 |
38 | export default (componentName) => {
39 | if (componentName.startsWith('App')) {
40 | for (const [group, components] of Object.entries(componentGroups)) {
41 | if (components.includes(componentName)) {
42 | return {
43 | from: `@/Components/${group}/${componentName}.vue`
44 | }
45 | }
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/stubs/modules/AdminAuth/Http/Controllers/AuthenticatedSessionController.php:
--------------------------------------------------------------------------------
1 | authenticate();
32 |
33 | $request->session()->regenerate();
34 |
35 | return redirect()->intended(route(config('modular.default-logged-route')));
36 | }
37 |
38 | /**
39 | * Destroy an authenticated session.
40 | *
41 | * @return \Illuminate\Http\RedirectResponse
42 | */
43 | public function logout(Request $request)
44 | {
45 | Auth::guard('user')->logout();
46 |
47 | $request->session()->invalidate();
48 |
49 | $request->session()->regenerateToken();
50 |
51 | return Inertia::location(config('modular.login-url'));
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/stubs/resources/js/Components/Menu/AppMenuSection.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
13 |
32 |
33 |
34 |
35 |
36 |
44 |
--------------------------------------------------------------------------------
/src/Console/MakeValidateCommand.php:
--------------------------------------------------------------------------------
1 | moduleName = Str::studly($this->argument('moduleName'));
24 | $this->resourceName = Str::studly($this->argument('resourceName'));
25 |
26 | if (! $this->moduleExists()) {
27 | return self::FAILURE;
28 | }
29 |
30 | $this->comment('Module '.$this->moduleName.' found, creating HTTP Request Validate...');
31 | $this->createValidateFile();
32 |
33 | return self::SUCCESS;
34 | }
35 |
36 | private function createValidateFile(): void
37 | {
38 | $stub = file_get_contents(__DIR__.'/../../stubs/module-stub/modules/Http/Requests/ModuleValidate.stub');
39 |
40 | $stub = str_replace('{{ ModuleName }}', $this->moduleName, $stub);
41 | $stub = str_replace('{{ ResourceName }}', $this->resourceName, $stub);
42 |
43 | $path = base_path("modules/{$this->moduleName}/Http/Requests/{$this->resourceName}Validate.php");
44 |
45 | file_put_contents($path, $stub);
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/stubs/app/Http/Middleware/HandleInertiaRequests.php:
--------------------------------------------------------------------------------
1 | user();
37 |
38 | return array_merge(parent::share($request), [
39 | 'auth' => [
40 | 'user' => $user,
41 | 'permissions' => $user ? (new ListUserPermissions)->run($user->id) : [],
42 | 'isRootUser' => $user ? ($user->hasRole('root') ? true : false) : false,
43 | ],
44 | 'ziggy' => fn () => array_merge((new Ziggy)->toArray(), [
45 | 'location' => $request->url(),
46 | ]),
47 | 'flash' => fn () => [
48 | 'success' => $request->session()->get('success'),
49 | 'error' => $request->session()->get('error'),
50 | ],
51 | ]);
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/Console/MakeModelCommand.php:
--------------------------------------------------------------------------------
1 | moduleName = Str::studly($this->argument('moduleName'));
24 | $this->resourceName = Str::studly($this->argument('resourceName'));
25 |
26 | if (! $this->moduleExists()) {
27 | return self::FAILURE;
28 | }
29 |
30 | $this->comment('Module '.$this->moduleName.' found, creating Model...');
31 | $this->createModuleModel();
32 |
33 | return self::SUCCESS;
34 | }
35 |
36 | private function createModuleModel(): void
37 | {
38 | $stub = file_get_contents(__DIR__.'/../../stubs/module-stub/modules/Models/Model.stub');
39 |
40 | $stub = str_replace('{{ ModuleName }}', $this->moduleName, $stub);
41 | $stub = str_replace('{{ ResourceName }}', $this->resourceName, $stub);
42 | $stub = str_replace('{{ resourceName }}', Str::camel(Str::plural($this->resourceName)), $stub);
43 |
44 | $path = base_path("modules/{$this->moduleName}/Models/{$this->resourceName}.php");
45 |
46 | file_put_contents($path, $stub);
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/stubs/modules/AdminAuth/Http/Controllers/PasswordResetLinkController.php:
--------------------------------------------------------------------------------
1 | validate([
31 | 'email' => 'required|email',
32 | ]);
33 |
34 | // We will send the password reset link to this user. Once we have attempted
35 | // to send the link, we will examine the response then see the message we
36 | // need to show to the user. Finally, we'll send out a proper response.
37 | $status = Password::broker('usersModule')->sendResetLink(
38 | $request->only('email')
39 | );
40 |
41 | return $status == Password::broker('usersModule')::RESET_LINK_SENT
42 | ? back()->with('success', __($status))
43 | : back()->withInput($request->only('email'))
44 | ->withErrors(['email' => __($status)]);
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/stubs/resources/js/Composables/useDataSearch.js:
--------------------------------------------------------------------------------
1 | import { ref, watch, onMounted } from 'vue'
2 | import { router } from '@inertiajs/vue3'
3 | import debounce from '@/Utils/debounce'
4 |
5 | export default function useDataSearch(
6 | routePath,
7 | columnsToSearch,
8 | aditionalParams = {}
9 | ) {
10 | const searchTerm = ref('')
11 |
12 | const debouncedSearch = debounce((value) => {
13 | const params = {
14 | page: 1,
15 | searchContext: columnsToSearch,
16 | searchTerm: value
17 | }
18 |
19 | Object.assign(params, aditionalParams)
20 |
21 | fetchData(params)
22 | }, 500)
23 |
24 | watch(searchTerm, (value, oldValue) => {
25 | //prevent new search request on paginated results and back button navigation combined
26 | if (oldValue === '' && value.length > 1) {
27 | return
28 | }
29 |
30 | debouncedSearch(value)
31 | })
32 |
33 | const fetchData = (params) => {
34 | router.visit(routePath, {
35 | data: params,
36 | replace: true,
37 | preserveState: true
38 | })
39 | }
40 |
41 | const clearSearch = () => {
42 | searchTerm.value = ''
43 | const params = { page: 1 }
44 |
45 | Object.assign(params, aditionalParams)
46 | fetchData(params)
47 | }
48 |
49 | // handle back button navigation
50 | onMounted(() => {
51 | const urlParams = new URLSearchParams(window.location.search)
52 | searchTerm.value = urlParams.get('searchTerm') || ''
53 | })
54 |
55 | return { searchTerm, clearSearch }
56 | }
57 |
--------------------------------------------------------------------------------
/stubs/resources/js/Pages/Dashboard/Components/DashboardCard.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 |
10 |
11 | {{ label }}
12 |
13 |
14 | {{ count }}
15 |
16 |
17 |
18 |
19 |
20 |
47 |
48 |
67 |
--------------------------------------------------------------------------------
/stubs/resources/js/Components/DataTable/AppDataSearch.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
15 |
16 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
55 |
--------------------------------------------------------------------------------
/src/Console/InstallerTraits/BackendPackages.php:
--------------------------------------------------------------------------------
1 | components->info('Publishing vendor files...');
12 | $this->publishVendorFiles();
13 | }
14 |
15 | protected function publishVendorFiles(): void
16 | {
17 | (new Process([$this->phpBinary(), 'artisan', 'vendor:publish', '--provider=Spatie\Permission\PermissionServiceProvider'], base_path()))
18 | ->setTimeout(null)
19 | ->run(function ($type, $output) {
20 | $this->output->write($output);
21 | });
22 |
23 | (new Process([$this->phpBinary(), 'artisan', 'config:clear'], base_path()))
24 | ->setTimeout(null)
25 | ->run(function ($type, $output) {
26 | $this->output->write($output);
27 | });
28 |
29 | (new Process([$this->phpBinary(), 'artisan', 'vendor:publish', '--provider=Spatie\Activitylog\ActivitylogServiceProvider', '--tag=activitylog-migrations'], base_path()))
30 | ->setTimeout(null)
31 | ->run(function ($type, $output) {
32 | $this->output->write($output);
33 | });
34 |
35 | (new Process([$this->phpBinary(), 'artisan', 'vendor:publish', '--provider=Spatie\Activitylog\ActivitylogServiceProvider', '--tag=activitylog-config'], base_path()))
36 | ->setTimeout(null)
37 | ->run(function ($type, $output) {
38 | $this->output->write($output);
39 | });
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Console/MakeServiceCommand.php:
--------------------------------------------------------------------------------
1 | moduleName = Str::studly($this->argument('moduleName'));
25 | $this->serviceName = Str::studly($this->argument('serviceName'));
26 |
27 | if (! $this->moduleExists()) {
28 | return self::FAILURE;
29 | }
30 |
31 | $this->comment('Module '.$this->moduleName.' found, creating Service...');
32 | $this->createModuleService();
33 |
34 | return self::SUCCESS;
35 | }
36 |
37 | private function createModuleService(): void
38 | {
39 | (new Filesystem)->ensureDirectoryExists(base_path("modules/{$this->moduleName}/Services/"));
40 |
41 | $stub = file_get_contents(__DIR__.'/../../stubs/module-stub/modules/Services/Service.stub');
42 |
43 | $stub = str_replace('{{ ModuleName }}', $this->moduleName, $stub);
44 | $stub = str_replace('{{ ServiceName }}', $this->serviceName, $stub);
45 |
46 | $path = base_path("modules/{$this->moduleName}/Services/{$this->serviceName}.php");
47 |
48 | file_put_contents($path, $stub);
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/Console/MakeSeederCommand.php:
--------------------------------------------------------------------------------
1 | moduleName = Str::studly($this->argument('moduleName'));
25 | $this->resourceName = Str::studly($this->argument('resourceName'));
26 |
27 | if (! $this->moduleExists()) {
28 | return self::FAILURE;
29 | }
30 |
31 | $this->comment('Module '.$this->moduleName.' found, creating seeder file...');
32 | $this->createModuleSeeder();
33 |
34 | return self::SUCCESS;
35 | }
36 |
37 | private function createModuleSeeder(): void
38 | {
39 | (new Filesystem)->ensureDirectoryExists(base_path("modules/{$this->moduleName}/Database/Seeders"));
40 |
41 | $stub = file_get_contents(__DIR__.'/../../stubs/module-stub/modules/Database/Seeders/ModuleSeeder.stub');
42 |
43 | $stub = str_replace('{{ ModuleName }}', $this->moduleName, $stub);
44 | $stub = str_replace('{{ ResourceName }}', $this->resourceName, $stub);
45 |
46 | $path = base_path("modules/{$this->moduleName}/Database/Seeders/{$this->resourceName}.php");
47 |
48 | file_put_contents($path, $stub);
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/stubs/tests/Pest.php:
--------------------------------------------------------------------------------
1 | in('Feature');
18 |
19 | /*
20 | |--------------------------------------------------------------------------
21 | | Expectations
22 | |--------------------------------------------------------------------------
23 | |
24 | | When you're writing tests, you often need to check that values meet certain conditions. The
25 | | "expect()" function gives you access to a set of "expectations" methods that you can use
26 | | to assert different things. Of course, you may extend the Expectation API at any time.
27 | |
28 | */
29 |
30 | expect()->extend('toBeOne', function () {
31 | return $this->toBe(1);
32 | });
33 |
34 | /*
35 | |--------------------------------------------------------------------------
36 | | Functions
37 | |--------------------------------------------------------------------------
38 | |
39 | | While Pest is very powerful out-of-the-box, you may have some testing code specific to your
40 | | project that you don't want to repeat in every file. Here you can also expose helpers as
41 | | global functions to help you to reduce the number of lines of code in your test files.
42 | |
43 | */
44 |
45 | function something()
46 | {
47 | // ..
48 | }
49 |
--------------------------------------------------------------------------------
/stubs/resources/js/Components/DataTable/AppPaginator.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Showing {{ from }} to {{ to }} of
5 | {{ total }} results
6 |
7 |
27 |
28 |
29 |
30 |
54 |
--------------------------------------------------------------------------------
/src/Console/MakeFactoryCommand.php:
--------------------------------------------------------------------------------
1 | moduleName = Str::studly($this->argument('moduleName'));
25 | $this->resourceName = Str::studly($this->argument('resourceName'));
26 |
27 | if (! $this->moduleExists()) {
28 | return self::FAILURE;
29 | }
30 |
31 | $this->comment('Module '.$this->moduleName.' found, creating Factory...');
32 | $this->createModuleFactory();
33 |
34 | return self::SUCCESS;
35 | }
36 |
37 | private function createModuleFactory(): void
38 | {
39 | (new Filesystem)->ensureDirectoryExists(base_path("modules/{$this->moduleName}/Database/Factories"));
40 |
41 | $stub = file_get_contents(__DIR__.'/../../stubs/module-stub/modules/Database/Factories/ModelFactory.stub');
42 |
43 | $stub = str_replace('{{ ModuleName }}', $this->moduleName, $stub);
44 | $stub = str_replace('{{ ResourceName }}', $this->resourceName, $stub);
45 | $stub = str_replace('{{ resourceName }}', Str::camel($this->resourceName), $stub);
46 |
47 | $path = base_path("modules/{$this->moduleName}/Database/Factories/{$this->resourceName}Factory.php");
48 |
49 | file_put_contents($path, $stub);
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/stubs/modules/Acl/Database/Seeders/AclPermissionSeeder.php:
--------------------------------------------------------------------------------
1 | getPermissions();
18 |
19 | foreach ($permissions as $permission) {
20 | Permission::create([
21 | 'name' => $permission,
22 | 'guard_name' => 'user',
23 | ]);
24 | }
25 |
26 | Schema::enableForeignKeyConstraints();
27 | }
28 |
29 | private function getPermissions(): array
30 | {
31 | return [
32 | // Main Menu
33 | 'Dashboard',
34 |
35 | // Acl: Access Control List
36 | 'Acl',
37 | 'Acl: User - List',
38 | 'Acl: Permission - List',
39 | 'Acl: Role - List',
40 |
41 | // User/UserIndex.vue
42 | 'Acl: User: Role - Edit',
43 | 'Acl: User: Permission - Edit',
44 | 'Acl: User - Create',
45 | 'Acl: User - Edit',
46 | 'Acl: User - Delete',
47 |
48 | // AclPermission/PermissionIndex.vue
49 | 'Acl: Permission - Create',
50 | 'Acl: Permission - Edit',
51 | 'Acl: Permission - Delete',
52 |
53 | // AclRole/RoleIndex.vue
54 | 'Acl: Role - Create',
55 | 'Acl: Role - Edit',
56 | 'Acl: Role - Delete',
57 |
58 | // AclRolePermission/RolePermissionForm.vue
59 | 'Acl: Role: Permission - Edit',
60 | ];
61 |
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/stubs/resources/js/Pages/Dashboard/DashboardIndex.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 | Welcome
9 | {{ $page.props.auth.user.name }} !
10 |
11 |
12 |
13 |
14 |
15 |
23 |
24 |
25 |
33 |
34 |
35 |
43 |
44 |
45 |
58 |
--------------------------------------------------------------------------------
/stubs/modules/User/Models/User.php:
--------------------------------------------------------------------------------
1 | 'datetime',
56 | ];
57 |
58 | /**
59 | * Overwrites the method from Authenticatable/CanResetPasswordContract.
60 | *
61 | * @param string $token
62 | */
63 | public function sendPasswordResetNotification($token)
64 | {
65 | $this->notify(new ResetPassword($token));
66 | }
67 |
68 | public function profile()
69 | {
70 | return $this->morphTo();
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/Console/MakeComponentCommand.php:
--------------------------------------------------------------------------------
1 | moduleName = Str::studly($this->argument('moduleName'));
25 | $this->componentName = Str::studly($this->argument('componentName'));
26 |
27 | if (! $this->moduleExists()) {
28 | return self::FAILURE;
29 | }
30 |
31 | $this->comment('Module '.$this->moduleName.' found, creating Component...');
32 | $this->createComponent();
33 |
34 | $this->generateComments();
35 |
36 | return self::SUCCESS;
37 | }
38 |
39 | private function createComponent(): void
40 | {
41 | $stub = file_get_contents(__DIR__.'/../../stubs/page-stub/Components/Component.stub');
42 |
43 | $stub = str_replace('{{ componentName }}', Str::camel($this->componentName), $stub);
44 |
45 | (new Filesystem)->ensureDirectoryExists(resource_path("js/Pages/{$this->moduleName}/Components/"));
46 |
47 | $path = resource_path("js/Pages/{$this->moduleName}/Components/{$this->componentName}.vue");
48 |
49 | file_put_contents($path, $stub);
50 | }
51 |
52 | private function generateComments(): void
53 | {
54 | $this->comment('In your Vue Component, import the new created Component as:');
55 | $this->info("import {$this->componentName} from './Components/{$this->componentName}.vue'");
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/Console/MakeRouteCommand.php:
--------------------------------------------------------------------------------
1 | moduleName = Str::studly($this->argument('moduleName'));
24 | $this->resourceName = Str::studly($this->argument('resourceName'));
25 |
26 | if (! $this->moduleExists()) {
27 | return self::FAILURE;
28 | }
29 |
30 | $this->comment('Module '.$this->moduleName.' found, creating Route...');
31 | $this->createModuleRoute();
32 |
33 | return self::SUCCESS;
34 | }
35 |
36 | private function createModuleRoute(): void
37 | {
38 | $stub = file_get_contents(__DIR__.'/../../stubs/module-stub/modules/routes/app.stub');
39 |
40 | $stub = str_replace('{{ resource-name }}', Str::snake($this->resourceName, '-'), $stub);
41 | $stub = str_replace('{{ ResourceName }}', $this->resourceName, $stub);
42 | $stub = str_replace('{{ resourceName }}', Str::camel($this->resourceName), $stub);
43 |
44 | $path = base_path("modules/{$this->moduleName}/routes/app.php");
45 |
46 | if (! file_exists($path)) {
47 | file_put_contents($path, $stub);
48 | } else {
49 | $stub = str_replace('resourceName).' routes', $stub);
51 |
52 | file_put_contents($path, $stub, FILE_APPEND);
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/Components/Translations.php:
--------------------------------------------------------------------------------
1 | getLocale();
16 |
17 | if (app()->environment('production')) {
18 | $translations = cache()->rememberForever("translations_$locale", fn () => $this->getTranslations($locale));
19 | } else {
20 | $translations = $this->getTranslations($locale);
21 | }
22 |
23 | return view('components.translations', [
24 | 'translations' => $translations,
25 | ]);
26 | }
27 |
28 | private function getTranslations(string $locale): array
29 | {
30 | $appPHPTranslations = $this->getPHPTranslations(lang_path($locale));
31 |
32 | $appJsonTranslations = $this->getJsonTranslations(lang_path("$locale.json"));
33 | $modularJsonTranslations = $this->getJsonTranslations(lang_path("vendor/modular/$locale/$locale.json"));
34 |
35 | return array_merge($appPHPTranslations, $appJsonTranslations, $modularJsonTranslations);
36 | }
37 |
38 | private function getPHPTranslations(string $directory): array
39 | {
40 | if (! is_dir($directory)) {
41 | return [];
42 | }
43 |
44 | return collect(File::allFiles($directory))
45 | ->filter(fn ($file) => $file->getExtension() === 'php')->flatMap(fn ($file) => Arr::dot(File::getRequire($file->getRealPath())))->toArray();
46 | }
47 |
48 | private function getJsonTranslations(string $filePath): array
49 | {
50 | if (File::exists($filePath)) {
51 | return json_decode(File::get($filePath), true, 512, JSON_THROW_ON_ERROR);
52 | } else {
53 | return [];
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | import js from '@eslint/js'
2 | import vue from 'eslint-plugin-vue'
3 | import vueParser from 'vue-eslint-parser'
4 | import prettierConfig from 'eslint-config-prettier'
5 | import globals from 'globals'
6 |
7 | export default [
8 | // Base ESLint recommended rules
9 | js.configs.recommended,
10 |
11 | // Prettier configuration to disable conflicting rules
12 | {
13 | rules: {
14 | ...prettierConfig.rules,
15 | 'unicode-bom': 'off'
16 | }
17 | },
18 |
19 | // Vue plugin configuration
20 | {
21 | files: ['**/*.vue'],
22 | languageOptions: {
23 | parser: vueParser,
24 | parserOptions: {
25 | ecmaVersion: 'latest',
26 | sourceType: 'module'
27 | }
28 | },
29 | plugins: {
30 | vue
31 | },
32 | rules: {
33 | // Combine base and recommended Vue rules
34 | ...vue.configs.base.rules,
35 | ...vue.configs['vue3-recommended'].rules,
36 |
37 | '@stylistic/js/indent': 'off',
38 | '@stylistic/js/quotes': 'off',
39 |
40 | // Disable specific Vue rules
41 | 'vue/no-v-html': 'off',
42 | 'vue/comment-directive': 'off'
43 |
44 | // You can add other Vue-specific rules here
45 | }
46 | },
47 |
48 | // General JavaScript rules (for .js and .vue files)
49 | {
50 | files: ['**/*.{js,vue}'],
51 | rules: {
52 | // Disable general ESLint rules
53 | // 'no-undef': 'off'
54 | }
55 | },
56 |
57 | // Custom rules (if any)
58 | {
59 | languageOptions: {
60 | globals: {
61 | ...globals.browser,
62 | route: 'readonly'
63 | }
64 | },
65 | rules: {
66 | // Add your custom rules here
67 | }
68 | },
69 |
70 | // Ignore patterns
71 | {
72 | ignores: ['node_modules/*', 'vendor/*']
73 | }
74 | ]
75 |
--------------------------------------------------------------------------------
/stubs/resources/js/Components/Form/AppTextArea.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 |
49 |
50 |
76 |
--------------------------------------------------------------------------------
/src/Console/RegisterServiceProviderCommand.php:
--------------------------------------------------------------------------------
1 | argument('name');
16 |
17 | if ($this->registerServiceProvider($moduleName)) {
18 | $this->info("Service provider for module {$moduleName} registered successfully.");
19 |
20 | return self::SUCCESS;
21 | }
22 |
23 | $this->error("Service provider for module {$moduleName} is already registered.");
24 |
25 | return self::FAILURE;
26 | }
27 |
28 | public function registerServiceProvider(string $moduleName): bool
29 | {
30 | $providerPath = base_path('bootstrap/providers.php');
31 | $content = file_get_contents($providerPath);
32 |
33 | $providerClass = "Modules\\{$moduleName}\\{$moduleName}ServiceProvider::class";
34 |
35 | // Check if provider already exists
36 | if (strpos($content, $providerClass) !== false) {
37 | return false;
38 | }
39 |
40 | // Split content into lines
41 | $lines = explode("\n", $content);
42 |
43 | // Find the position of the closing bracket
44 | $returnArrayIndex = -1;
45 | foreach ($lines as $index => $line) {
46 | if (trim($line) === '];') {
47 | $returnArrayIndex = $index;
48 | break;
49 | }
50 | }
51 |
52 | if ($returnArrayIndex === -1) {
53 | return false;
54 | }
55 |
56 | // Insert the new provider before the closing bracket
57 | array_splice($lines, $returnArrayIndex, 0, " {$providerClass},");
58 |
59 | // Join the lines back together
60 | $updatedContent = implode("\n", $lines);
61 |
62 | return (bool) file_put_contents($providerPath, $updatedContent);
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/stubs/modules/AdminAuth/Tests/PasswordResetTest.php:
--------------------------------------------------------------------------------
1 | get('/admin-auth/forgot-password');
13 |
14 | $response->assertStatus(200);
15 | });
16 |
17 | test('reset password link can be requested', function () {
18 | Notification::fake();
19 |
20 | $user = User::factory()->create();
21 |
22 | $this->post('/admin-auth/send-reset-link-email', ['email' => $user->email]);
23 |
24 | Notification::assertSentTo($user, AdminAuthResetPassword::class);
25 | });
26 |
27 | test('reset password screen can be rendered', function () {
28 | Notification::fake();
29 |
30 | $user = User::factory()->create();
31 |
32 | $this->post('/admin-auth/send-reset-link-email', ['email' => $user->email]);
33 |
34 | Notification::assertSentTo($user, AdminAuthResetPassword::class, function ($notification) {
35 | $response = $this->get('/admin-auth/reset-password/'.$notification->token);
36 |
37 | $response->assertStatus(200);
38 |
39 | return true;
40 | });
41 | });
42 |
43 | test('password can be reset with valid token', function () {
44 | Notification::fake();
45 |
46 | $user = User::factory()->create();
47 |
48 | $this->post('/admin-auth/send-reset-link-email', ['email' => $user->email]);
49 |
50 | Notification::assertSentTo($user, AdminAuthResetPassword::class, function ($notification) use ($user) {
51 | $response = $this->post('/admin-auth/reset-password/', [
52 | 'token' => $notification->token,
53 | 'email' => $user->email,
54 | 'password' => 'password',
55 | 'password_confirmation' => 'password',
56 | ]);
57 |
58 | $response->assertSessionHasNoErrors();
59 |
60 | return true;
61 | });
62 | });
63 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "daniel-cintra/modular",
3 | "description": "A fast way to develop web apps using Laravel, Vue and Inertia.",
4 | "keywords": [
5 | "modular",
6 | "laravel",
7 | "modular"
8 | ],
9 | "homepage": "https://ismodular.com/",
10 | "license": "MIT",
11 | "authors": [
12 | {
13 | "name": "Daniel Coimbra Cintra",
14 | "email": "danic10@gmail.com",
15 | "role": "Developer"
16 | }
17 | ],
18 | "require": {
19 | "php": "^8.2",
20 | "illuminate/contracts": "^12.0",
21 | "inertiajs/inertia-laravel": "^2.0",
22 | "laravel/prompts": "^0.3.5",
23 | "spatie/laravel-activitylog": "^4.10",
24 | "spatie/laravel-package-tools": "^1.19",
25 | "spatie/laravel-permission": "^6.15",
26 | "tightenco/ziggy": "^2.5"
27 | },
28 | "require-dev": {
29 | "laravel/pint": "^1.24",
30 | "nunomaduro/collision": "^8.6",
31 | "orchestra/testbench": "^10.0",
32 | "pestphp/pest": "^4.1",
33 | "pestphp/pest-plugin-laravel": "^4.0"
34 | },
35 | "autoload": {
36 | "psr-4": {
37 | "Modular\\Modular\\": "src",
38 | "Modular\\Modular\\Database\\Factories\\": "database/factories"
39 | }
40 | },
41 | "autoload-dev": {
42 | "psr-4": {
43 | "Modular\\Modular\\Tests\\": "tests"
44 | }
45 | },
46 | "scripts": {
47 | "analyse": "vendor/bin/phpstan analyse",
48 | "test": "vendor/bin/pest",
49 | "test-coverage": "vendor/bin/pest --coverage",
50 | "format": "vendor/bin/pint"
51 | },
52 | "config": {
53 | "sort-packages": true,
54 | "allow-plugins": {
55 | "pestphp/pest-plugin": true,
56 | "phpstan/extension-installer": true
57 | }
58 | },
59 | "extra": {
60 | "laravel": {
61 | "providers": [
62 | "Modular\\Modular\\ModularServiceProvider"
63 | ]
64 | }
65 | },
66 | "minimum-stability": "dev",
67 | "prefer-stable": true
68 | }
69 |
--------------------------------------------------------------------------------
/stubs/modules/Support/Traits/EditorImage.php:
--------------------------------------------------------------------------------
1 | all(), [
16 | 'file' => $this->getUploadImageValidationRules(),
17 | ]);
18 |
19 | if ($validator->fails()) {
20 | return response()->json(['errors' => $validator->errors()->all()], 422);
21 | }
22 |
23 | $fileAttributes = $this->uploadImage(request()->file('file'));
24 |
25 | return response()->json($fileAttributes);
26 | }
27 |
28 | protected function uploadImage(UploadedFile $file): array
29 | {
30 | $readableName = $this->getReadableName($file);
31 | $fileName = $this->getFileName($file, $readableName, $this->getUploadImageNameStrategy());
32 |
33 | $file->storeAs(
34 | $this->getUploadImagePath(),
35 | $fileName,
36 | 'public',
37 | );
38 |
39 | return [
40 | 'fileName' => $fileName,
41 | 'filePath' => storage_path("app/public/{$this->getUploadImagePath()}/{$fileName}"),
42 | 'fileUrl' => asset("storage/{$this->getUploadImagePath()}/{$fileName}"),
43 | 'readableName' => $readableName,
44 | ];
45 | }
46 |
47 | private function getUploadImageValidationRules(): string
48 | {
49 | return property_exists($this, 'uploadImageValidationRules') ? $this->uploadImageValidationRules : 'required|image|mimes:jpeg,png,jpg,gif,svg|max:2048';
50 | }
51 |
52 | private function getUploadImagePath(): string
53 | {
54 | return property_exists($this, 'uploadImagePath') ? $this->uploadImagePath : 'editor-files';
55 | }
56 |
57 | private function getUploadImageNameStrategy(): string
58 | {
59 | return property_exists($this, 'uploadImageNameStrategy') ? $this->uploadImageNameStrategy : 'originalUUID';
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/stubs/modules/Acl/Http/Controllers/RoleController.php:
--------------------------------------------------------------------------------
1 | where(request('searchContext'), 'like', "%{$searchTerm}%");
16 | })
17 | ->orderBy('name')
18 | ->paginate(request('rowsPerPage', 10))
19 | ->withQueryString()
20 | ->through(fn ($role) => [
21 | 'id' => $role->id,
22 | 'name' => $role->name,
23 | 'guard_name' => $role->guard_name,
24 | ]);
25 |
26 | return inertia('AclRole/RoleIndex', [
27 | 'roles' => $roles,
28 | ]);
29 | }
30 |
31 | public function create()
32 | {
33 | return inertia('AclRole/RoleForm');
34 | }
35 |
36 | public function store(RoleValidate $request)
37 | {
38 | $params = $request->validated();
39 | $params['guard_name'] = 'user';
40 | Role::create($params);
41 |
42 | return redirect()->route('aclRole.index')
43 | ->with('success', 'Role created');
44 | }
45 |
46 | public function edit($id)
47 | {
48 | $role = Role::find($id);
49 |
50 | return inertia('AclRole/RoleForm', [
51 | 'role' => $role,
52 | ]);
53 | }
54 |
55 | public function update(RoleValidate $request, $id)
56 | {
57 | $role = Role::findOrFail($id);
58 |
59 | $role->update($request->validated());
60 |
61 | return redirect()->route('aclRole.index')
62 | ->with('success', 'Role updated');
63 | }
64 |
65 | public function destroy($id)
66 | {
67 | Role::findOrFail($id)->delete();
68 |
69 | return redirect()->route('aclRole.index')
70 | ->with('success', 'Role deleted');
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/stubs/modules/Acl/Tests/Role/UserRoleTest.php:
--------------------------------------------------------------------------------
1 | user = User::factory()->create();
13 | $this->loggedRequest = $this->actingAs($this->user);
14 |
15 | $this->role = Role::create(['name' => 'root', 'guard_name' => 'user']);
16 | $this->role2 = Role::create(['name' => 'second', 'guard_name' => 'user']);
17 |
18 | $this->user->syncRoles([$this->role->id]);
19 | });
20 |
21 | test('user roles can be rendered', function () {
22 | $response = $this->loggedRequest->get('/admin/acl-user-role/'.$this->user->id.'/edit');
23 |
24 | $response->assertStatus(200);
25 |
26 | $response->assertInertia(
27 | fn (Assert $page) => $page
28 | ->component('AclUserRole/UserRoleForm')
29 | ->has(
30 | 'user',
31 | fn (Assert $page) => $page
32 | ->where('id', $this->user->id)
33 | ->etc()
34 | )
35 | ->has(
36 | 'user.roles',
37 | 1,
38 | fn (Assert $page) => $page
39 | ->where('id', $this->role->id)
40 | ->where('name', $this->role->name)
41 | )
42 | ->has(
43 | 'roles',
44 | 2
45 | )
46 | );
47 | });
48 |
49 | test('user roles can be updated', function () {
50 | $response = $this->loggedRequest->put('/admin/acl-user-role/'.$this->user->id, [
51 | 'userRoles' => [$this->role2->id],
52 | ]);
53 |
54 | $response->assertRedirect('/admin/user');
55 |
56 | $user = User::with(['roles' => function ($q) {
57 | $q->get(['id', 'name']);
58 | }])->findOrFail($this->user->id);
59 |
60 | $this->assertCount(1, $user->roles);
61 | $this->assertFalse($user->hasRole($this->role->name));
62 | $this->assertTrue($user->hasRole($this->role2->name));
63 | });
64 |
--------------------------------------------------------------------------------
/stubs/modules/User/Http/Controllers/UserController.php:
--------------------------------------------------------------------------------
1 | search(request('searchContext'), request('searchTerm'))
15 | ->paginate(request('rowsPerPage', 10))
16 | ->withQueryString()
17 | ->through(fn ($user) => [
18 | 'id' => $user->id,
19 | 'name' => $user->name,
20 | 'email' => $user->email,
21 | 'created_at' => $user->created_at->format('d/m/Y H:i').'h',
22 | ]);
23 |
24 | return inertia('User/UserIndex', [
25 | 'users' => $users,
26 | ]);
27 | }
28 |
29 | public function create()
30 | {
31 | return inertia('User/UserForm');
32 | }
33 |
34 | public function store(UserValidate $request)
35 | {
36 | User::create($request->validated());
37 |
38 | return redirect()->route('user.index')
39 | ->with('success', 'User created');
40 | }
41 |
42 | public function edit($id)
43 | {
44 | $user = User::select('id', 'name', 'email')->find($id);
45 |
46 | return inertia('User/UserForm', [
47 | 'user' => $user,
48 | ]);
49 | }
50 |
51 | public function update(UserValidate $request, $id)
52 | {
53 | $user = User::findOrFail($id);
54 |
55 | $params = $request->validated();
56 |
57 | if (empty($params['password'])) {
58 | unset($params['password']);
59 | }
60 |
61 | $user->update($params);
62 |
63 | return redirect()->route('user.index')
64 | ->with('success', 'User updated');
65 | }
66 |
67 | public function destroy($id)
68 | {
69 | User::findOrFail($id)->delete();
70 |
71 | return redirect()->route('user.index')
72 | ->with('success', 'User deleted');
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/stubs/site/resources-site/views/pagination/simple-tailwind.blade.php:
--------------------------------------------------------------------------------
1 | @if ($paginator->hasPages())
2 |
25 | @endif
26 |
--------------------------------------------------------------------------------
/stubs/resources/js/Components/Form/AppCheckbox.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 |
12 |
13 |
22 |
23 |
24 |
77 |
--------------------------------------------------------------------------------
/stubs/resources/js/Layouts/AuthenticatedLayout.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
16 |
17 |
18 |
19 |
20 |
21 |
25 |
26 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
68 |
69 |
81 |
--------------------------------------------------------------------------------
/stubs/resources/js/Pages/AclRole/RoleForm.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | {{ title }}
8 |
9 |
10 |
24 |
25 |
26 |
27 | {{ __('Save') }}
28 |
29 |
30 |
31 |
32 |
33 |
71 |
--------------------------------------------------------------------------------
/src/Console/MakeComposableCommand.php:
--------------------------------------------------------------------------------
1 | moduleName = Str::studly($this->argument('moduleName'));
25 | $this->composableName = Str::studly($this->argument('composableName'));
26 |
27 | if (! $this->moduleExists()) {
28 | return self::FAILURE;
29 | }
30 |
31 | $this->comment('Module '.$this->moduleName.' found, creating Composable...');
32 | $this->createComposable();
33 |
34 | $this->generateComments();
35 |
36 | return self::SUCCESS;
37 | }
38 |
39 | private function createComposable(): void
40 | {
41 | $stub = file_get_contents(__DIR__.'/../../stubs/page-stub/Composables/Composable.stub');
42 |
43 | $stub = str_replace('{{ ComposableName }}', $this->composableName, $stub);
44 | $stub = str_replace('{{ composableName }}', Str::camel($this->composableName), $stub);
45 |
46 | (new Filesystem)->ensureDirectoryExists(resource_path("js/Pages/{$this->moduleName}/Composables/"));
47 |
48 | $path = resource_path("js/Pages/{$this->moduleName}/Composables/use{$this->composableName}.js");
49 |
50 | file_put_contents($path, $stub);
51 | }
52 |
53 | private function generateComments(): void
54 | {
55 | $camelCaseComposableName = Str::camel($this->composableName);
56 |
57 | $this->comment('In your Vue Component, import the composable:');
58 | $this->info("import use{$this->composableName} from './Composables/use{$this->composableName}'");
59 |
60 | $this->comment('And use it like:');
61 | $this->info("const { {$camelCaseComposableName} } = use{$this->composableName}()");
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/stubs/page-stub/Form.stub:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | {{ title }}
8 |
9 |
10 |
23 |
24 |
25 |
26 | {{ __('Save') }}
27 |
28 |
29 |
30 |
31 |
32 |
72 |
--------------------------------------------------------------------------------
/stubs/resources/js/Pages/AclPermission/PermissionForm.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | {{ title }}
8 |
9 |
10 |
24 |
25 |
26 |
27 | {{ __('Save') }}
28 |
29 |
30 |
31 |
32 |
33 |
71 |
--------------------------------------------------------------------------------
/stubs/resources/js/Components/Overlay/AppConfirmDialog.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
9 | {{ __('Confirmation') }}
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | {{ __('Are you sure you want to proceed?') }}
22 |
23 |
24 |
25 |
26 |
27 |
28 |
31 |
32 | {{ __('No') }}
33 |
34 |
35 |
36 | {{ __('Yes') }}
37 |
38 |
39 |
40 |
41 |
42 |
43 |
74 |
--------------------------------------------------------------------------------
/stubs/modules/Acl/Tests/Role/RolePermissionTest.php:
--------------------------------------------------------------------------------
1 | role = Role::create(['name' => 'root']);
14 | $this->user = User::factory()->create();
15 |
16 | $this->permission = Permission::create(['name' => 'first']);
17 | $this->permission2 = Permission::create(['name' => 'second']);
18 |
19 | $this->role->syncPermissions([$this->permission->id]);
20 | $this->user->assignRole($this->role);
21 |
22 | $this->loggedRequest = $this->actingAs($this->user);
23 | });
24 |
25 | test('role permissions can be rendered', function () {
26 | $response = $this->loggedRequest->get('/admin/acl-role-permission/'.$this->role->id.'/edit');
27 |
28 | $response->assertStatus(200);
29 |
30 | $response->assertInertia(
31 | fn (Assert $page) => $page
32 | ->component('AclRolePermission/RolePermissionForm')
33 | ->has(
34 | 'role',
35 | fn (Assert $page) => $page
36 | ->where('id', $this->role->id)
37 | ->etc()
38 | )
39 | ->has(
40 | 'role.permissions',
41 | 1,
42 | fn (Assert $page) => $page
43 | ->where('id', $this->permission->id)
44 | ->where('name', $this->permission->name)
45 | )
46 | ->has(
47 | 'permissions',
48 | 2
49 | )
50 | );
51 | });
52 |
53 | test('role permissions can be updated', function () {
54 | $response = $this->loggedRequest->put('/admin/acl-role-permission/'.$this->role->id, [
55 | 'rolePermissions' => [$this->permission2->id],
56 | ]);
57 |
58 | $response->assertRedirect('/admin/acl-role');
59 |
60 | $role = Role::with(['permissions' => function ($q) {
61 | $q->get(['id', 'name']);
62 | }])->findOrFail($this->role->id);
63 |
64 | $this->assertCount(1, $role->permissions);
65 | $this->assertEquals($this->permission2->id, $role->permissions->first()->id);
66 | });
67 |
--------------------------------------------------------------------------------
/stubs/modules/Acl/Tests/Permission/UserPermissionTest.php:
--------------------------------------------------------------------------------
1 | 'root']);
15 | $this->user = User::factory()->create();
16 | $this->user->assignRole($role);
17 |
18 | $this->loggedRequest = $this->actingAs($this->user);
19 |
20 | $this->permission = Permission::create(['name' => 'first', 'guard_name' => 'user']);
21 | $this->permission2 = Permission::create(['name' => 'second', 'guard_name' => 'user']);
22 |
23 | $this->user->syncPermissions([$this->permission->id]);
24 | });
25 |
26 | test('user permissions can be rendered', function () {
27 | $response = $this->loggedRequest->get('/admin/acl-user-permission/'.$this->user->id.'/edit');
28 |
29 | $response->assertStatus(200);
30 |
31 | $response->assertInertia(
32 | fn (Assert $page) => $page
33 | ->component('AclUserPermission/UserPermissionForm')
34 | ->has(
35 | 'user',
36 | fn (Assert $page) => $page
37 | ->where('id', $this->user->id)
38 | ->etc()
39 | )
40 | ->has(
41 | 'userPermissions',
42 | 1,
43 | fn (Assert $page) => $page
44 | ->where('id', $this->permission->id)
45 | ->where('name', $this->permission->name)
46 | )
47 | ->has(
48 | 'permissions',
49 | 2
50 | )
51 | );
52 | });
53 |
54 | test('user permissions can be updated', function () {
55 | $response = $this->loggedRequest->put('/admin/acl-user-permission/'.$this->user->id, [
56 | 'userPermissions' => [$this->permission2->id],
57 | ]);
58 |
59 | $response->assertRedirect('/admin/user');
60 |
61 | $userPermissions = (new GetUserPermissions)->run($this->user->id);
62 |
63 | $this->assertCount(1, $userPermissions);
64 | $this->assertEquals($this->permission2->id, $userPermissions[0]['id']);
65 | });
66 |
--------------------------------------------------------------------------------
/stubs/modules/Acl/Http/Controllers/PermissionController.php:
--------------------------------------------------------------------------------
1 | where(request('searchContext'), 'like', "%{$searchTerm}%");
16 | })
17 | ->orderBy('guard_name')
18 | ->orderBy('name')
19 | ->paginate(request('rowsPerPage', 10))
20 | ->withQueryString()
21 | ->through(fn ($permission) => [
22 | 'id' => $permission->id,
23 | 'name' => $permission->name,
24 | 'guard' => $permission->guard,
25 |
26 | ]);
27 |
28 | return inertia('AclPermission/PermissionIndex', [
29 | 'permissions' => $permissions,
30 | ]);
31 | }
32 |
33 | public function create()
34 | {
35 | return inertia('AclPermission/PermissionForm');
36 | }
37 |
38 | public function store(PermissionValidate $request)
39 | {
40 | $params = $request->validated();
41 | $params['guard_name'] = 'user';
42 | Permission::create($params);
43 |
44 | return redirect()->route('aclPermission.index')
45 | ->with('success', 'Permission created');
46 | }
47 |
48 | public function edit($id)
49 | {
50 | $permission = Permission::find($id);
51 |
52 | return inertia('AclPermission/PermissionForm', [
53 | 'permission' => $permission,
54 | ]);
55 | }
56 |
57 | public function update(PermissionValidate $request, $id)
58 | {
59 | $permission = Permission::findOrFail($id);
60 |
61 | $permission->update($request->validated());
62 |
63 | return redirect()->route('aclPermission.index')
64 | ->with('success', 'Permission updated');
65 | }
66 |
67 | public function destroy($id)
68 | {
69 | Permission::findOrFail($id)->delete();
70 |
71 | return redirect()->route('aclPermission.index')
72 | ->with('success', 'Permission deleted');
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/stubs/resources/js/Components/Message/AppFlashMessage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {{ message }}
7 |
8 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
71 |
72 |
87 |
--------------------------------------------------------------------------------
/stubs/modules/Acl/Tests/Permission/GetUserPermissionTest.php:
--------------------------------------------------------------------------------
1 | user = User::factory()->create();
14 |
15 | $this->role = Role::create(['name' => 'role 1', 'guard_name' => 'user']);
16 | $this->role2 = Role::create(['name' => 'role 2', 'guard_name' => 'user']);
17 |
18 | $this->permission = Permission::create(['name' => 'permission 1', 'guard_name' => 'user']);
19 | $this->permission2 = Permission::create(['name' => 'permission 2', 'guard_name' => 'user']);
20 | });
21 |
22 | test('user permission service returns correct direct permissions for the user', function () {
23 | $this->user->syncPermissions([$this->permission->id]);
24 |
25 | $userPermissions = (new GetUserPermissions)->run($this->user->id);
26 |
27 | $this->assertCount(1, $userPermissions);
28 | $this->assertEquals($this->permission->id, $userPermissions[0]['id']);
29 | });
30 |
31 | test('user permission service returns correct empty permissions for the user', function () {
32 | $userPermissions = (new GetUserPermissions)->run($this->user->id);
33 |
34 | $this->assertTrue(is_array($userPermissions));
35 | $this->assertCount(0, $userPermissions);
36 | });
37 |
38 | test('user permission service returns correct role permissions for the user', function () {
39 | $this->role2->syncPermissions([$this->permission2->id]);
40 | $this->user->syncRoles([$this->role2->id]);
41 |
42 | $userPermissions = (new GetUserPermissions)->run($this->user->id);
43 |
44 | $this->assertCount(1, $userPermissions);
45 | $this->assertEquals($this->permission2->id, $userPermissions[0]['id']);
46 | });
47 |
48 | test('user permission service returns correct direct and role permissions for the user', function () {
49 | $this->user->syncPermissions([$this->permission->id]);
50 | $this->role2->syncPermissions([$this->permission2->id]);
51 | $this->user->syncRoles([$this->role2->id]);
52 |
53 | $userPermissions = (new GetUserPermissions)->run($this->user->id);
54 |
55 | // granular user permissions, will always override role permissions for the user
56 | $this->assertCount(1, $userPermissions);
57 | $this->assertEquals($this->permission->id, $userPermissions[0]['id']);
58 | });
59 |
--------------------------------------------------------------------------------
/src/ModularServiceProvider.php:
--------------------------------------------------------------------------------
1 | name('modular')
37 | ->hasConfigFile()
38 | ->hasTranslations()
39 | ->hasViewComponents('modular', Translations::class)
40 | ->hasCommand(InstallCommand::class)
41 | ->hasCommand(MakeModuleCommand::class)
42 | ->hasCommand(MakeControllerCommand::class)
43 | ->hasCommand(MakeValidateCommand::class)
44 | ->hasCommand(MakeModelCommand::class)
45 | ->hasCommand(MakeRouteCommand::class)
46 | ->hasCommand(MakeServiceCommand::class)
47 | ->hasCommand(MakePageCommand::class)
48 | ->hasCommand(MakeComposableCommand::class)
49 | ->hasCommand(MakeComponentCommand::class)
50 | ->hasCommand(MakeTestCommand::class)
51 | ->hasCommand(MakeMigrationCommand::class)
52 | ->hasCommand(MakeSeederCommand::class)
53 | ->hasCommand(PublishLaravelTranslationsCommand::class)
54 | ->hasCommand(PublishSiteFilesCommand::class)
55 | ->hasCommand(MakeFactoryCommand::class)
56 | ->hasCommand(RegisterServiceProviderCommand::class);
57 |
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/stubs/modules/Support/BaseServiceProvider.php:
--------------------------------------------------------------------------------
1 | mapAppRoutes();
25 |
26 | $this->mapSiteRoutes();
27 |
28 | $this->mapApiRoutes();
29 | }
30 |
31 | /**
32 | * Define the "web" routes, that must be authenticated for the application.
33 | * These routes all receive session state, CSRF protection, etc.
34 | */
35 | protected function mapAppRoutes()
36 | {
37 | Route::prefix('admin')
38 | ->middleware(['web', 'auth.user'])
39 | ->group(function ($router) {
40 | $routesPath = $this->getCurrentDir().'/routes/app.php';
41 |
42 | if (file_exists($routesPath)) {
43 | require $routesPath;
44 | }
45 | });
46 | }
47 |
48 | /**
49 | * Define the "web" routes for the application.
50 | * These routes all receive session state, CSRF protection, etc.
51 | */
52 | protected function mapSiteRoutes()
53 | {
54 | Route::middleware(['web'])
55 | ->group(function ($router) {
56 | $routesPath = $this->getCurrentDir().'/routes/site.php';
57 |
58 | if (file_exists($routesPath)) {
59 | require $routesPath;
60 | }
61 | });
62 | }
63 |
64 | /**
65 | * Define the "api" routes for the application.
66 | * These routes are typically stateless.
67 | */
68 | protected function mapApiRoutes()
69 | {
70 | Route::prefix('api')
71 | ->middleware(['auth:sanctum'])
72 | ->group(function ($router) {
73 | $routesPath = $this->getCurrentDir().'/routes/api.php';
74 |
75 | if (file_exists($routesPath)) {
76 | require $routesPath;
77 | }
78 | });
79 | }
80 |
81 | /**
82 | * Returns the module directory in context.
83 | */
84 | protected function getCurrentDir(): string
85 | {
86 | $classInfo = new ReflectionClass($this);
87 |
88 | return dirname($classInfo->getFileName());
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/stubs/module-stub/modules/Http/Controllers/ModuleController.stub:
--------------------------------------------------------------------------------
1 | search(request('searchContext'), request('searchTerm'))
17 | ->paginate(request('rowsPerPage', 10))
18 | ->withQueryString()
19 | ->through(fn (${{ resourceName }}) => [
20 | 'id' => ${{ resourceName }}->id,
21 | 'name' => ${{ resourceName }}->name,
22 | 'created_at' => ${{ resourceName }}->created_at->format('d/m/Y H:i') . 'h'
23 | ]);
24 |
25 | return inertia('{{ ModuleName }}/{{ ResourceName }}Index', [
26 | '{{ resourceNameCamelPlural }}' => ${{ resourceNameCamelPlural }}
27 | ]);
28 | }
29 |
30 | public function create(): Response
31 | {
32 | return inertia('{{ ModuleName }}/{{ ResourceName }}Form');
33 | }
34 |
35 | public function store({{ ResourceName }}Validate $request): RedirectResponse
36 | {
37 | {{ ResourceName }}::create($request->validated());
38 |
39 | return redirect()->route('{{ resourceName }}.index')
40 | ->with('success', '{{ ResourceName }} created.');
41 | }
42 |
43 | public function edit(int $id): Response
44 | {
45 | ${{ resourceName }} = {{ ResourceName }}::find($id);
46 |
47 | return inertia('{{ ModuleName }}/{{ ResourceName }}Form', [
48 | '{{ resourceName }}' => ${{ resourceName }}
49 | ]);
50 | }
51 |
52 | public function update({{ ResourceName }}Validate $request, int $id): RedirectResponse
53 | {
54 | ${{ resourceName }} = {{ ResourceName }}::findOrFail($id);
55 |
56 | ${{ resourceName }}->update($request->validated());
57 |
58 | return redirect()->route('{{ resourceName }}.index')
59 | ->with('success', '{{ ResourceName }} updated.');
60 | }
61 |
62 | public function destroy(int $id): RedirectResponse
63 | {
64 | {{ ResourceName }}::findOrFail($id)->delete();
65 |
66 | return redirect()->route('{{ resourceName }}.index')
67 | ->with('success', '{{ ResourceName }} deleted.');
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/stubs/modules/AdminAuth/Http/Controllers/NewPasswordController.php:
--------------------------------------------------------------------------------
1 | $request->email,
24 | 'token' => $request->route('token'),
25 | ]);
26 | }
27 |
28 | /**
29 | * Handle an incoming new password request.
30 | *
31 | * @return \Illuminate\Http\RedirectResponse
32 | *
33 | * @throws \Illuminate\Validation\ValidationException
34 | */
35 | public function store(Request $request)
36 | {
37 | $request->validate([
38 | 'token' => 'required',
39 | 'email' => 'required|email',
40 | 'password' => ['required', 'confirmed', Rules\Password::defaults()],
41 | ]);
42 |
43 | // Here we will attempt to reset the user's password. If it is successful we
44 | // will update the password on an actual user model and persist it to the
45 | // database. Otherwise we will parse the error and return the response.
46 | $status = Password::broker('usersModule')->reset(
47 | $request->only('email', 'password', 'password_confirmation', 'token'),
48 | function ($user) use ($request) {
49 | $user->forceFill([
50 | 'password' => Hash::make($request->password),
51 | 'remember_token' => Str::random(60),
52 | ])->save();
53 |
54 | event(new PasswordReset($user));
55 | }
56 | );
57 |
58 | // If the password was successfully reset, we will redirect the user back to
59 | // the application's home authenticated view. If there is an error we can
60 | // redirect them back to where they came from with their error message.
61 | return $status == Password::broker('usersModule')::PASSWORD_RESET
62 | ? redirect()->route('adminAuth.loginForm')->with('success', 'Password updated')
63 | : back()->withInput($request->only('email'))
64 | ->withErrors(['email' => __($status)]);
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/stubs/modules/AdminAuth/Http/Requests/LoginRequest.php:
--------------------------------------------------------------------------------
1 | ['required', 'string', 'email'],
33 | 'password' => ['required', 'string'],
34 | ];
35 | }
36 |
37 | /**
38 | * Attempt to authenticate the request's credentials.
39 | *
40 | * @return void
41 | *
42 | * @throws \Illuminate\Validation\ValidationException
43 | */
44 | public function authenticate()
45 | {
46 | $this->ensureIsNotRateLimited();
47 |
48 | if (! Auth::attempt($this->only('email', 'password'), $this->boolean('remember'))) {
49 | RateLimiter::hit($this->throttleKey());
50 |
51 | throw ValidationException::withMessages([
52 | 'email' => trans('auth.failed'),
53 | ]);
54 | }
55 |
56 | RateLimiter::clear($this->throttleKey());
57 | }
58 |
59 | /**
60 | * Ensure the login request is not rate limited.
61 | *
62 | * @return void
63 | *
64 | * @throws \Illuminate\Validation\ValidationException
65 | */
66 | public function ensureIsNotRateLimited()
67 | {
68 | if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
69 | return;
70 | }
71 |
72 | event(new Lockout($this));
73 |
74 | $seconds = RateLimiter::availableIn($this->throttleKey());
75 |
76 | throw ValidationException::withMessages([
77 | 'email' => trans('auth.throttle', [
78 | 'seconds' => $seconds,
79 | 'minutes' => ceil($seconds / 60),
80 | ]),
81 | ]);
82 | }
83 |
84 | /**
85 | * Get the rate limiting throttle key for the request.
86 | *
87 | * @return string
88 | */
89 | public function throttleKey()
90 | {
91 | return Str::transliterate(Str::lower($this->input('email')).'|'.$this->ip());
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/stubs/resources/js/Components/Overlay/AppModal.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
71 |
72 |
85 |
--------------------------------------------------------------------------------
/stubs/resources/js/Pages/AdminAuth/ForgotPage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | {{ __('Forgot your password?') }}
10 |
11 |
12 |
13 |
14 |
15 | {{ __('Enter your email to reset your password.') }}
16 |
17 |
18 |
19 |
20 |
21 |
22 |
36 |
37 |
38 |
39 |
40 |
44 | {{ __('Send Password Reset Link') }}
45 |
46 |
47 |
48 |
49 | {{ __('Back to Login') }}
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
65 |
66 |
81 |
--------------------------------------------------------------------------------