From c2ca786233c200b9f7dd9a3421f8af2a9d535f9a Mon Sep 17 00:00:00 2001 From: Fady Mondy Date: Wed, 27 Nov 2024 16:10:37 +0200 Subject: [PATCH] =?UTF-8?q?first=20commit=20=F0=9F=94=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/CONTRIBUTING.md | 55 ++++ .github/FUNDING.yml | 1 + .github/ISSUE_TEMPLATE/bug.yml | 66 +++++ .github/ISSUE_TEMPLATE/config.yaml | 11 + .github/SECURITY.md | 3 + .github/dependabot.yml | 12 + .github/workflows/dependabot-auto-merge.yml | 32 +++ .github/workflows/fix-php-code-styling.yml | 30 ++ .github/workflows/tests.yml | 68 +++++ .php-cs-fixer.dist.php | 37 +++ 3x1io-tomato-form-builder.md | 14 + CHANGELOG.md | 3 + CODE_OF_CONDUCT.md | 128 +++++++++ LICENSE.md | 21 ++ README.md | 69 +++++ SECURITY.md | 3 + composer.json | 68 +++++ config/.gitkeep | 0 config/filament-form-builder.php | 5 + .../2023_01_19_115146_create_forms_table.php | 50 ++++ ...01_19_115147_create_form_options_table.php | 79 +++++ ...1_19_115148_create_form_requests_table.php | 48 ++++ ...01_19_115148_update_form_options_table.php | 37 +++ ...115149_create_form_request_metas_table.php | 38 +++ module.json | 29 ++ phpunit.xml | 26 ++ pint.json | 14 + resources/lang/.gitkeep | 0 resources/lang/ar/messages.php | 73 +++++ resources/lang/en/messages.php | 73 +++++ src/Console/.gitkeep | 0 src/Console/FilamentFormBuilderInstall.php | 44 +++ src/Console/FilamentFormGenerator.php | 197 +++++++++++++ src/Filament/Resources/.gitkeep | 0 src/Filament/Resources/FormOptionResource.php | 138 +++++++++ .../Pages/CreateFormOption.php | 12 + .../Pages/EditFormOption.php | 19 ++ .../Pages/ListFormOptions.php | 19 ++ .../Resources/FormRequestMetaResource.php | 91 ++++++ .../Pages/CreateFormRequestMeta.php | 12 + .../Pages/EditFormRequestMeta.php | 19 ++ .../Pages/ListFormRequestMetas.php | 19 ++ .../Resources/FormRequestResource.php | 108 +++++++ .../Pages/CreateFormRequest.php | 12 + .../Pages/EditFormRequest.php | 19 ++ .../Pages/ListFormRequests.php | 19 ++ src/Filament/Resources/FormResource.php | 156 ++++++++++ .../FormResource/Pages/CreateForm.php | 12 + .../Resources/FormResource/Pages/EditForm.php | 22 ++ .../FormResource/Pages/ListForms.php | 22 ++ .../RelationManagers/FormFieldsRelation.php | 272 ++++++++++++++++++ .../RelationManagers/FormRequestsRelation.php | 173 +++++++++++ src/FilamentFormBuilderPlugin.php | 30 ++ src/FilamentFormBuilderServiceProvider.php | 171 +++++++++++ src/Models/.gitkeep | 0 src/Models/Form.php | 57 ++++ src/Models/FormOption.php | 71 +++++ src/Models/FormRequest.php | 82 ++++++ src/Models/FormRequestMeta.php | 36 +++ src/Services/Contracts/CmsFormFieldType.php | 102 +++++++ src/Services/Contracts/Form.php | 97 +++++++ src/Services/Contracts/FormInput.php | 238 +++++++++++++++ src/Services/Contracts/FormInputOption.php | 58 ++++ src/Services/FilamentCMSFormBuilder.php | 142 +++++++++ src/Services/FilamentCMSFormFields.php | 27 ++ src/Services/FilamentFormsServices.php | 30 ++ testbench.yaml | 24 ++ tests/Pest.php | 5 + tests/database/database.sqlite | Bin 0 -> 245760 bytes tests/database/factories/.gitkeep | 0 tests/database/factories/UserFactory.php | 23 ++ tests/src/AdminPanelProvider.php | 50 ++++ tests/src/DebugTest.php | 5 + tests/src/Models/.gitkeep | 0 tests/src/Models/User.php | 34 +++ tests/src/TestCase.php | 53 ++++ 76 files changed, 3813 insertions(+) create mode 100644 .github/CONTRIBUTING.md create mode 100644 .github/FUNDING.yml create mode 100644 .github/ISSUE_TEMPLATE/bug.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yaml create mode 100644 .github/SECURITY.md create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/dependabot-auto-merge.yml create mode 100644 .github/workflows/fix-php-code-styling.yml create mode 100644 .github/workflows/tests.yml create mode 100644 .php-cs-fixer.dist.php create mode 100644 3x1io-tomato-form-builder.md create mode 100644 CHANGELOG.md create mode 100644 CODE_OF_CONDUCT.md create mode 100644 LICENSE.md create mode 100644 README.md create mode 100644 SECURITY.md create mode 100644 composer.json create mode 100644 config/.gitkeep create mode 100644 config/filament-form-builder.php create mode 100644 database/migrations/2023_01_19_115146_create_forms_table.php create mode 100644 database/migrations/2023_01_19_115147_create_form_options_table.php create mode 100644 database/migrations/2023_01_19_115148_create_form_requests_table.php create mode 100644 database/migrations/2023_01_19_115148_update_form_options_table.php create mode 100644 database/migrations/2023_01_19_115149_create_form_request_metas_table.php create mode 100644 module.json create mode 100644 phpunit.xml create mode 100644 pint.json create mode 100644 resources/lang/.gitkeep create mode 100644 resources/lang/ar/messages.php create mode 100644 resources/lang/en/messages.php create mode 100644 src/Console/.gitkeep create mode 100644 src/Console/FilamentFormBuilderInstall.php create mode 100644 src/Console/FilamentFormGenerator.php create mode 100644 src/Filament/Resources/.gitkeep create mode 100644 src/Filament/Resources/FormOptionResource.php create mode 100644 src/Filament/Resources/FormOptionResource/Pages/CreateFormOption.php create mode 100644 src/Filament/Resources/FormOptionResource/Pages/EditFormOption.php create mode 100644 src/Filament/Resources/FormOptionResource/Pages/ListFormOptions.php create mode 100644 src/Filament/Resources/FormRequestMetaResource.php create mode 100644 src/Filament/Resources/FormRequestMetaResource/Pages/CreateFormRequestMeta.php create mode 100644 src/Filament/Resources/FormRequestMetaResource/Pages/EditFormRequestMeta.php create mode 100644 src/Filament/Resources/FormRequestMetaResource/Pages/ListFormRequestMetas.php create mode 100644 src/Filament/Resources/FormRequestResource.php create mode 100644 src/Filament/Resources/FormRequestResource/Pages/CreateFormRequest.php create mode 100644 src/Filament/Resources/FormRequestResource/Pages/EditFormRequest.php create mode 100644 src/Filament/Resources/FormRequestResource/Pages/ListFormRequests.php create mode 100644 src/Filament/Resources/FormResource.php create mode 100644 src/Filament/Resources/FormResource/Pages/CreateForm.php create mode 100644 src/Filament/Resources/FormResource/Pages/EditForm.php create mode 100644 src/Filament/Resources/FormResource/Pages/ListForms.php create mode 100644 src/Filament/Resources/FormResource/RelationManagers/FormFieldsRelation.php create mode 100644 src/Filament/Resources/FormResource/RelationManagers/FormRequestsRelation.php create mode 100644 src/FilamentFormBuilderPlugin.php create mode 100644 src/FilamentFormBuilderServiceProvider.php create mode 100644 src/Models/.gitkeep create mode 100644 src/Models/Form.php create mode 100644 src/Models/FormOption.php create mode 100644 src/Models/FormRequest.php create mode 100644 src/Models/FormRequestMeta.php create mode 100644 src/Services/Contracts/CmsFormFieldType.php create mode 100644 src/Services/Contracts/Form.php create mode 100644 src/Services/Contracts/FormInput.php create mode 100644 src/Services/Contracts/FormInputOption.php create mode 100644 src/Services/FilamentCMSFormBuilder.php create mode 100644 src/Services/FilamentCMSFormFields.php create mode 100644 src/Services/FilamentFormsServices.php create mode 100644 testbench.yaml create mode 100644 tests/Pest.php create mode 100644 tests/database/database.sqlite create mode 100644 tests/database/factories/.gitkeep create mode 100644 tests/database/factories/UserFactory.php create mode 100644 tests/src/AdminPanelProvider.php create mode 100644 tests/src/DebugTest.php create mode 100644 tests/src/Models/.gitkeep create mode 100644 tests/src/Models/User.php create mode 100644 tests/src/TestCase.php diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..b0ee5d8 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,55 @@ +# Contributing + +Contributions are **welcome** and will be fully **credited**. + +Please read and understand the contribution guide before creating an issue or pull request. + +## Etiquette + +This project is open source, and as such, the maintainers give their free time to build and maintain the source code +held within. They make the code freely available in the hope that it will be of use to other developers. It would be +extremely unfair for them to suffer abuse or anger for their hard work. + +Please be considerate towards maintainers when raising issues or presenting pull requests. Let's show the +world that developers are civilized and selfless people. + +It's the duty of the maintainer to ensure that all submissions to the project are of sufficient +quality to benefit the project. Many developers have different skills, strengths, and weaknesses. Respect the maintainer's decision, and do not be upset or abusive if your submission is not used. + +## Viability + +When requesting or submitting new features, first consider whether it might be useful to others. Open +source projects are used by many developers, who may have entirely different needs to your own. Think about +whether or not your feature is likely to be used by other users of the project. + +## Procedure + +Before filing an issue: + +- Attempt to replicate the problem, to ensure that it wasn't a coincidental incident. +- Check to make sure your feature suggestion isn't already present within the project. +- Check the pull requests tab to ensure that the bug doesn't have a fix in progress. +- Check the pull requests tab to ensure that the feature isn't already in progress. + +Before submitting a pull request: + +- Check the codebase to ensure that your feature doesn't already exist. +- Check the pull requests to ensure that another person hasn't already submitted the feature or fix. + +## Requirements + +If the project maintainer has any additional requirements, you will find them listed here. + +- **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](https://pear.php.net/package/PHP_CodeSniffer). + +- **Add tests!** - Your patch won't be accepted if it doesn't have tests. + +- **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. + +- **Consider our release cycle** - We try to follow [SemVer v2.0.0](https://semver.org/). Randomly breaking public APIs is not an option. + +- **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. + +- **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](https://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. + +**Happy coding**! diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..892ba05 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: [3x1io] diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 0000000..8fa85ce --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,66 @@ +name: Bug Report +description: Report an Issue or Bug with the Package +title: "[Bug]: " +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + We're sorry to hear you have a problem. Can you help us solve it by providing the following details. + - type: textarea + id: what-happened + attributes: + label: What happened? + description: What did you expect to happen? + placeholder: I cannot currently do X thing because when I do, it breaks X thing. + validations: + required: true + - type: textarea + id: how-to-reproduce + attributes: + label: How to reproduce the bug + description: How did this occur, please add any config values used and provide a set of reliable steps if possible. + placeholder: When I do X I see Y. + validations: + required: true + - type: input + id: package-version + attributes: + label: Package Version + description: What version of our Package are you running? Please be as specific as possible + placeholder: 2.0.0 + validations: + required: true + - type: input + id: php-version + attributes: + label: PHP Version + description: What version of PHP are you running? Please be as specific as possible + placeholder: 8.2.0 + validations: + required: true + - type: input + id: laravel-version + attributes: + label: Laravel Version + description: What version of Laravel are you running? Please be as specific as possible + placeholder: 9.0.0 + validations: + required: true + - type: dropdown + id: operating-systems + attributes: + label: Which operating systems does with happen with? + description: You may select more than one. + multiple: true + options: + - macOS + - Windows + - Linux + - type: textarea + id: notes + attributes: + label: Notes + description: Use this field to provide any other notes that you feel might be relevant to the issue. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/config.yaml b/.github/ISSUE_TEMPLATE/config.yaml new file mode 100644 index 0000000..cb37ffd --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yaml @@ -0,0 +1,11 @@ +blank_issues_enabled: false +contact_links: + - name: Ask a question + url: https://github.com/tomatophp/filament-form-builder/discussions/new?category=q-a + about: Ask the community for help + - name: Request a feature + url: https://github.com/tomatophp/filament-form-builder/discussions/new?category=ideas + about: Share ideas for new features + - name: Report a security issue + url: https://github.com/tomatophp/filament-form-builder/security/policy + about: Learn how to notify us for sensitive bugs diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 0000000..b2490a9 --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,3 @@ +# Security Policy + +If you discover any security related issues, please email info@3x1.io instead of using the issue tracker. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..0bc378d --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +# Please see the documentation for all configuration options: +# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + labels: + - "dependencies" diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml new file mode 100644 index 0000000..27c23a4 --- /dev/null +++ b/.github/workflows/dependabot-auto-merge.yml @@ -0,0 +1,32 @@ +name: dependabot-auto-merge +on: pull_request_target + +permissions: + pull-requests: write + contents: write + +jobs: + dependabot: + runs-on: ubuntu-latest + if: ${{ github.actor == 'dependabot[bot]' }} + steps: + + - name: Dependabot metadata + id: metadata + uses: dependabot/fetch-metadata@v2.2.0 + with: + github-token: "${{ secrets.GITHUB_TOKEN }}" + + - name: Auto-merge Dependabot PRs for semver-minor updates + if: ${{steps.metadata.outputs.update-type == 'version-update:semver-minor'}} + run: gh pr merge --auto --merge "$PR_URL" + env: + PR_URL: ${{github.event.pull_request.html_url}} + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} + + - name: Auto-merge Dependabot PRs for semver-patch updates + if: ${{steps.metadata.outputs.update-type == 'version-update:semver-patch'}} + run: gh pr merge --auto --merge "$PR_URL" + env: + PR_URL: ${{github.event.pull_request.html_url}} + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} diff --git a/.github/workflows/fix-php-code-styling.yml b/.github/workflows/fix-php-code-styling.yml new file mode 100644 index 0000000..e71024d --- /dev/null +++ b/.github/workflows/fix-php-code-styling.yml @@ -0,0 +1,30 @@ +name: 'PHP Code Styling' + +on: + workflow_dispatch: + push: + branches: + - master + paths: + - '**.php' + +permissions: + contents: write + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref }} + + - name: Fix PHP code style issues + uses: aglipanci/laravel-pint-action@v2 + + - name: Commit changes + uses: stefanzweifel/git-auto-commit-action@v5 + with: + commit_message: "Format Code" + commit_user_name: 'GitHub Actions' diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..14a3ed3 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,68 @@ +name: "Tests" + +on: + workflow_dispatch: + push: + branches: + - master + paths: + - '**.php' + pull_request: + types: + - opened + - synchronize + branches: + - master + paths: + - '**.php' + - '.github/workflows/tests.yml' + - 'phpunit.xml.dist' + - 'composer.json' + - 'composer.lock' + +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: true + matrix: + os: [ubuntu-latest] + php: [8.3, 8.2] + laravel: [11.*] + stability: [prefer-stable] + include: + - laravel: 11.* + testbench: 9.* + carbon: 3.* + collision: 8.* + name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.stability }} - ${{ matrix.os }} + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Cache Dependencies + uses: actions/cache@v4 + with: + path: ~/.composer/cache/files + key: dependencies-laravel-${{ matrix.laravel }}-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }} + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo + coverage: none + + - name: Install Dependencies + run: | + echo "::add-matcher::${{ runner.tool_cache }}/php.json" + echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" + + - name: Install Dependencies + run: | + composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" "nesbot/carbon:${{ matrix.carbon }}" "nunomaduro/collision:${{ matrix.collision }}" --no-interaction --no-update + composer update --${{ matrix.stability }} --prefer-dist --no-interaction + composer db + + - name: Execute tests + run: vendor/bin/pest diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php new file mode 100644 index 0000000..4123157 --- /dev/null +++ b/.php-cs-fixer.dist.php @@ -0,0 +1,37 @@ +notPath('bootstrap/*') + ->notPath('storage/*') + ->notPath('resources/view/mail/*') + ->in([ + __DIR__ . '/src', + __DIR__ . '/tests', + ]) + ->name('*.php') + ->notName('*.blade.php') + ->ignoreDotFiles(true) + ->ignoreVCS(true); + +return (new PhpCsFixer\Config()) + ->setRules([ + '@PSR2' => true, + 'array_syntax' => ['syntax' => 'short'], + 'ordered_imports' => ['sort_algorithm' => 'alpha'], + 'no_unused_imports' => true, + 'not_operator_with_successor_space' => true, + 'trailing_comma_in_multiline' => true, + 'phpdoc_scalar' => true, + 'unary_operator_spaces' => true, + 'binary_operator_spaces' => true, + 'blank_line_before_statement' => [ + 'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'], + ], + 'phpdoc_single_line_var_spacing' => true, + 'phpdoc_var_without_name' => true, + 'method_argument_space' => [ + 'on_multiline' => 'ensure_fully_multiline', + 'keep_multiple_spaces_after_comma' => true, + ] + ]) + ->setFinder($finder); diff --git a/3x1io-tomato-form-builder.md b/3x1io-tomato-form-builder.md new file mode 100644 index 0000000..0c4cb8a --- /dev/null +++ b/3x1io-tomato-form-builder.md @@ -0,0 +1,14 @@ +--- +name: Form Builder +slug: 3x1io-tomato-form-builder +author_slug: 3x1io +categories: [developer-tools] +description: Manage your forms using database and drop/drag component to build the form with Livewire component support for FilamentPHP +discord_url: +docs_url: https://raw.githubusercontent.com/tomatophp/filament-form-builder/master/README.md +github_repository: tomatophp/filament-form-builder +has_dark_theme: true +has_translations: true +versions: [3] +publish_date: 2024-11-27 +--- diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e0a5086 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +# V1.0.0 + +First release of the package diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..18c9147 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..e66364e --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..4059786 --- /dev/null +++ b/README.md @@ -0,0 +1,69 @@ +![Screenshot](https://raw.githubusercontent.com/tomatophp/filament-form-builder/master/art/screenshot.jpg) + +# Filament form builder + +[![Latest Stable Version](https://poser.pugx.org/tomatophp/filament-form-builder/version.svg)](https://packagist.org/packages/tomatophp/filament-form-builder) +[![License](https://poser.pugx.org/tomatophp/filament-form-builder/license.svg)](https://packagist.org/packages/tomatophp/filament-form-builder) +[![Downloads](https://poser.pugx.org/tomatophp/filament-form-builder/d/total.svg)](https://packagist.org/packages/tomatophp/filament-form-builder) + +Manage your forms using database and drop/drag component to build the form with Livewire component support for FilamentPHP + +## Installation + +```bash +composer require tomatophp/filament-form-builder +``` +after install your package please run this command + +```bash +php artisan filament-form-builder:install +``` + +finally register the plugin on `/app/Providers/Filament/AdminPanelProvider.php` + +```php +->plugin(\TomatoPHP\FilamentFormBuilder\FilamentFormBuilderPlugin::make()) +``` + + +## Publish Assets + +you can publish config file by use this command + +```bash +php artisan vendor:publish --tag="filament-form-builder-config" +``` + +you can publish views file by use this command + +```bash +php artisan vendor:publish --tag="filament-form-builder-views" +``` + +you can publish languages file by use this command + +```bash +php artisan vendor:publish --tag="filament-form-builder-lang" +``` + +you can publish migrations file by use this command + +```bash +php artisan vendor:publish --tag="filament-form-builder-migrations" +``` + +## Changelog + +Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. + +## Security + +Please see [SECURITY](SECURITY.md) for more information about security. + +## Credits + +- [Fady Mondy](mailto:info@3x1.io) + +## License + +The MIT License (MIT). Please see [License File](LICENSE.md) for more information. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..b2490a9 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,3 @@ +# Security Policy + +If you discover any security related issues, please email info@3x1.io instead of using the issue tracker. diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..8788718 --- /dev/null +++ b/composer.json @@ -0,0 +1,68 @@ +{ + "name": "tomatophp/filament-form-builder", + "type": "library", + "description": "Manage your forms using database and drop/drag component to build the form with Livewire component support for FilamentPHP", + "keywords": [ + "php", + "laravel", + "template" + ], + "license": "MIT", + "autoload": { + "psr-4": { + "TomatoPHP\\FilamentFormBuilder\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "TomatoPHP\\FilamentFormBuilder\\Tests\\": "tests/src/", + "TomatoPHP\\FilamentFormBuilder\\Tests\\Database\\Factories\\": "tests/database/factories" + } + }, + "extra": { + "laravel": { + "providers": [ + "TomatoPHP\\FilamentFormBuilder\\FilamentFormBuilderServiceProvider" + ] + } + }, + "authors": [ + { + "name": "Fady Mondy", + "email": "info@3x1.io" + } + ], + "config": { + "sort-packages": true, + "allow-plugins": { + "pestphp/pest-plugin": true, + "phpstan/extension-installer": true + } + }, + "scripts": { + "testbench": "vendor/bin/testbench package:discover --ansi", + "db": "vendor/bin/testbench package:create-sqlite-db && vendor/bin/testbench migrate", + "analyse": "vendor/bin/phpstan analyse src tests", + "test": "vendor/bin/pest", + "test-coverage": "vendor/bin/pest --coverage", + "format": "vendor/bin/pint" + }, + "require": { + "php": "^8.1|^8.2", + "tomatophp/console-helpers": "^1.1", + "filament/filament": "^3.2" + }, + "require-dev": { + "laravel/pint": "^1.18", + "livewire/livewire": "^2.10|^3.0", + "nunomaduro/larastan": "^2.9", + "orchestra/testbench": "^9.5", + "pestphp/pest": "^2.36", + "pestphp/pest-plugin-laravel": "^2.4", + "pestphp/pest-plugin-livewire": "^2.1", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan-deprecation-rules": "^1.2", + "phpstan/phpstan-phpunit": "^1.4" + }, + "version": "v1.0.0" +} diff --git a/config/.gitkeep b/config/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/config/filament-form-builder.php b/config/filament-form-builder.php new file mode 100644 index 0000000..f5bf951 --- /dev/null +++ b/config/filament-form-builder.php @@ -0,0 +1,5 @@ +id(); + + //Set Type from page/modal/slideover + $table->string('type')->default('page')->nullable(); + + //Set Name And Key + $table->json('title')->nullable(); + $table->json('description')->nullable(); + $table->string('key')->unique()->index(); + + //Set Form Action + $table->string('endpoint')->default('/')->nullable(); + $table->string('method')->default('POST')->nullable(); + + //Form Control + $table->boolean('is_active')->default(0)->nullable(); + + $table->timestamps(); + $table->softDeletes(); + }); + } + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('forms'); + } +}; diff --git a/database/migrations/2023_01_19_115147_create_form_options_table.php b/database/migrations/2023_01_19_115147_create_form_options_table.php new file mode 100644 index 0000000..f3eaed3 --- /dev/null +++ b/database/migrations/2023_01_19_115147_create_form_options_table.php @@ -0,0 +1,79 @@ +id(); + + $table->foreignId('form_id')->constrained('forms')->onDelete('cascade'); + + //Set type of filed like text, email, number, select, checkbox, radio, textarea, file, date, time, datetime, password + $table->string('type')->default('text')->nullable(); + + //Set Name and Key for this filed + $table->json('label')->nullable(); + $table->json('placeholder')->nullable(); + $table->json('hint')->nullable(); + $table->string('name')->index(); + $table->string('group')->nullable(); + + //Set Default value for this field + $table->json('default')->nullable(); + + $table->integer('order')->default(0)->nullable(); + + //Is Filed Required? + $table->boolean('is_required')->default(0)->nullable(); + $table->boolean('is_multi')->default(0)->nullable(); + $table->json('required_message')->nullable(); + + //Is Field Reactive? + $table->boolean('is_reactive')->default(0)->nullable(); + $table->string('reactive_field')->nullable(); + $table->string('reactive_where')->nullable(); + + //Is Table Select? + $table->boolean('is_relation')->default(0)->nullable(); + $table->string('relation_name')->nullable(); + $table->string('relation_column')->nullable(); + + //Check if Field is Has options like Select + $table->boolean('has_options')->default(0)->nullable(); + $table->json('options')->nullable(); + + //Valdations + $table->boolean('has_validation')->default(0)->nullable(); + $table->json('validation')->nullable(); + + //For Meta Injection + $table->json('meta')->nullable(); + + $table->timestamps(); + }); + } + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::disableForeignKeyConstraints(); + Schema::dropIfExists('form_options'); + Schema::enableForeignKeyConstraints(); + } +}; diff --git a/database/migrations/2023_01_19_115148_create_form_requests_table.php b/database/migrations/2023_01_19_115148_create_form_requests_table.php new file mode 100644 index 0000000..541a54a --- /dev/null +++ b/database/migrations/2023_01_19_115148_create_form_requests_table.php @@ -0,0 +1,48 @@ +id(); + + //Morph + $table->string('model_type')->nullable(); + $table->unsignedBigInteger('model_id')->nullable(); + + //Morph Service + $table->string('service_type')->nullable(); + $table->unsignedBigInteger('service_id')->nullable(); + + $table->foreignId('form_id')->constrained('forms'); + $table->string('status')->default('pending')->nullable(); + $table->json('payload')->nullable(); + + $table->text('description')->nullable(); + $table->date('date')->nullable(); + $table->time('time')->nullable(); + + $table->timestamps(); + }); + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::disableForeignKeyConstraints(); + Schema::dropIfExists('form_requests'); + Schema::enableForeignKeyConstraints(); + } +}; diff --git a/database/migrations/2023_01_19_115148_update_form_options_table.php b/database/migrations/2023_01_19_115148_update_form_options_table.php new file mode 100644 index 0000000..357fdc8 --- /dev/null +++ b/database/migrations/2023_01_19_115148_update_form_options_table.php @@ -0,0 +1,37 @@ +foreignId('sub_form')->nullable()->constrained('forms'); + }); + } + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + if(config('filament-cms.features.forms')) { + Schema::table('form_options', function (Blueprint $table) { + $table->dropForeign(['sub_form']); + $table->dropColumn('sub_form'); + }); + } + } +}; diff --git a/database/migrations/2023_01_19_115149_create_form_request_metas_table.php b/database/migrations/2023_01_19_115149_create_form_request_metas_table.php new file mode 100644 index 0000000..7e1a0d0 --- /dev/null +++ b/database/migrations/2023_01_19_115149_create_form_request_metas_table.php @@ -0,0 +1,38 @@ +id(); + + $table->unsignedBigInteger('model_id')->nullable(); + $table->string('model_type')->nullable(); + + $table->foreignId('form_request_id')->references('id')->on('form_requests')->onDelete('cascade'); + $table->string('key')->index(); + $table->json('value')->nullable(); + $table->timestamps(); + }); + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::disableForeignKeyConstraints(); + Schema::dropIfExists('form_request_metas'); + Schema::enableForeignKeyConstraints(); + } +}; diff --git a/module.json b/module.json new file mode 100644 index 0000000..4de0b70 --- /dev/null +++ b/module.json @@ -0,0 +1,29 @@ +{ + "name": "FilamentFormBuilder", + "alias": "filament-form-builder", + "description": { + "ar": "Manage your forms using database and drop/drag component to build the form with Livewire component support for FilamentPHP", + "en": "Manage your forms using database and drop/drag component to build the form with Livewire component support for FilamentPHP", + "gr": "Manage your forms using database and drop/drag component to build the form with Livewire component support for FilamentPHP", + "sp": "Manage your forms using database and drop/drag component to build the form with Livewire component support for FilamentPHP" + }, + "keywords": [], + "priority": 0, + "providers": [ + "TomatoPHP\\FilamentFormBuilder\\FilamentFormBuilderServiceProvider" + ], + "files": [], + "title": { + "ar": "Filament form builder", + "en": "Filament form builder", + "gr": "Filament form builder", + "sp": "Filament form builder" + }, + "color": "#cc1448", + "icon": "heroicon-c-users", + "placeholder": "https://raw.githubusercontent.com/tomatophp/filament-form-builder/master/art/screenshot.jpg", + "type": "plugin", + "version": "v1.0.0", + "github" : "https://github.com/tomatophp/filament-form-builder", + "docs" : "https://github.com/tomatophp/filament-form-builder" +} diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..e542661 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,26 @@ + + + + + ./tests/ + + + + + ./src + + + + + + + diff --git a/pint.json b/pint.json new file mode 100644 index 0000000..c6ddb49 --- /dev/null +++ b/pint.json @@ -0,0 +1,14 @@ +{ + "preset": "laravel", + "rules": { + "blank_line_before_statement": true, + "concat_space": { + "spacing": "one" + }, + "method_argument_space": true, + "single_trait_insert_per_statement": true, + "types_spaces": { + "space": "single" + } + } +} diff --git a/resources/lang/.gitkeep b/resources/lang/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/resources/lang/ar/messages.php b/resources/lang/ar/messages.php new file mode 100644 index 0000000..4b480a2 --- /dev/null +++ b/resources/lang/ar/messages.php @@ -0,0 +1,73 @@ + [ + "section" => [ + "information" => "تفاصيل النموذج" + ], + "title" => "منشيء النماذج", + "single" => "نموذج", + "columns" => [ + "type" => "النوع", + "method" => "الطريقة", + "title" => "العنوان", + "key" => "المفتاح", + "description" => "الوصف", + "endpoint" => "الرابط", + "is_active" => "نشط", + ], + "fields" => [ + "title" => "الحقول", + "single" => "حقل", + "columns" => [ + "type" => "النوع", + "name" => "الاسم", + "group" => "المجموعة", + "default" => "القيمة الافتراضية", + "is_relation" => "علاقة", + "relation_name" => "اسم العلاقة", + "relation_column" => "عمود العلاقة", + "sub_form" => "نموذج فرعي", + "is_multi" => "متعدد", + "has_options" => "لديه خيارات", + "options" => "الخيارات", + "label" => "التسمية", + "value"=> "القيمة", + "placeholder" => "النص البديل", + "hint" => "تلميح", + "is_required" => "مطلوب", + "required_message" => "رسالة الخطأ", + "has_validation" => "لديه تحقق", + "validation" => "التحقق", + "rule" => "القاعدة", + "message" => "الرسالة", + ], + "tabs" => [ + "general" => "عام", + "options" => "الخيارات", + "validation" => "التحقق", + "relation" => "العلاقة", + "labels" => "التسميات", + ], + "actions" => [ + "preview" => "معاينة", + ] + ], + "requests" => [ + "title" => "طلبات النموذج", + "single" => "طلب", + "columns" => [ + "status" => "الحالة", + "description" => "الوصف", + "time" => "الوقت", + "date" => "التاريخ", + "payload" => "الرسالة", + "pending" => "قيد الانتظار", + "processing" => "جاري المعالجة", + "completed" => "تم الانتهاء", + "cancelled" => "تم الإلغاء", + + ] + ] + ], +]; diff --git a/resources/lang/en/messages.php b/resources/lang/en/messages.php new file mode 100644 index 0000000..51248be --- /dev/null +++ b/resources/lang/en/messages.php @@ -0,0 +1,73 @@ + [ + "section" => [ + "information" => "Form Information" + ], + "title" => "Form Builder", + "single" => "Form", + "columns" => [ + "type" => "Type", + "method" => "Method", + "title" => "Title", + "key" => "Key", + "description" => "Description", + "endpoint" => "Endpoint", + "is_active" => "Is Active", + ], + "fields" => [ + "title" => "Fields", + "single" => "Field", + "columns" => [ + "type" => "Type", + "name" => "Name", + "group" => "Group", + "default" => "Default", + "is_relation" => "Is Relation", + "relation_name" => "Relation Name", + "relation_column" => "Relation Column", + "sub_form" => "Sub Form", + "is_multi" => "Is Multi", + "has_options" => "Has Options", + "options" => "Options", + "label" => "Label", + "value"=> "Value", + "placeholder" => "Placeholder", + "hint" => "Hint", + "is_required" => "Is Required", + "required_message" => "Required Message", + "has_validation" => "Has Validation", + "validation" => "Validation", + "rule" => "Rule", + "message" => "Message" + ], + "tabs" => [ + "general" => "General", + "options" => "Options", + "validation" => "Validation", + "relation" => "Relation", + "labels" => "Labels", + ], + "actions" => [ + "preview" => "Preview", + ] + ], + "requests" => [ + "title" => "Form Requests", + "single" => "Request", + "columns" => [ + "status" => "Status", + "description" => "Description", + "time" => "Time", + "date" => "Date", + "payload" => "Payload", + "pending" => "Pending", + "processing" => "Processing", + "completed" => "Completed", + "cancelled" => "Cancelled", + + ] + ] + ], +]; diff --git a/src/Console/.gitkeep b/src/Console/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/Console/FilamentFormBuilderInstall.php b/src/Console/FilamentFormBuilderInstall.php new file mode 100644 index 0000000..9b6ba4d --- /dev/null +++ b/src/Console/FilamentFormBuilderInstall.php @@ -0,0 +1,44 @@ +info('Publish Vendor Assets'); + $this->artisanCommand(["migrate"]); + $this->artisanCommand(["optimize:clear"]); + $this->info('Filament form builder installed successfully.'); + } +} diff --git a/src/Console/FilamentFormGenerator.php b/src/Console/FilamentFormGenerator.php new file mode 100644 index 0000000..4e7e73b --- /dev/null +++ b/src/Console/FilamentFormGenerator.php @@ -0,0 +1,197 @@ +ask('please input your form title'); + $key = $this->ask('please input your form key', Str::slug($name)); + $checkIfFormExists = Form::where('key', $key??Str::slug($name))->first(); + $key= $key??Str::slug($name); + while ($checkIfFormExists){ + $this->error('Form already exists'); + $key = $this->ask('please input your form key [unique]', Str::slug($name)); + $checkIfFormExists = Form::where('key', $key??Str::slug($name))->first(); + $key= $key??Str::slug($name); + } + $endpoint = $this->ask('please input your form endpoint', '/'); + $method = $this->ask('please input your form method', 'POST'); + $description = $this->ask('please input your form description'); + $type = $this->ask('please input your form type', 'page'); + + $fields = []; + $add = true; + while ($add){ + $fieldKey = $this->ask('please input your first field key'); + $checkIfFieldExists = Field::where('key', $fieldKey)->first(); + if(!$checkIfFieldExists){ + $ask = $this->ask('Field not exits exists do you went to create new? [yes/no]', 'no'); + if($ask === 'yes' || $ask === 'y'){ + $type = $this->ask('type', 'text'); + $label = $this->ask('label'); + $fieldKey = $this->ask('key [unique]', Str::slug($label)); + $placeholder = $this->ask('placeholder'); + $required = $this->ask('Is Required? [yes/no]', 'yes'); + $hasOptions = $this->ask('Has Options? [yes/no]', 'no'); + + $buildFieldsArray->push(" ["); + $buildFieldsArray->push(" 'label'=>'".$label. "',"); + $buildFieldsArray->push(" 'key'=>'".$fieldKey ?? Str::slug($fieldKey). "',"); + $buildFieldsArray->push(" 'type'=>'".$type. "',"); + $buildFieldsArray->push(" 'placeholder'=>'".$placeholder. "',"); + if($required || $required === 'n' || $required === 'no'){ + $buildFieldsArray->push(" 'is_required'=>false,"); + } + else { + $buildFieldsArray->push(" 'is_required'=>true,"); + } + if($hasOptions || $hasOptions === 'y' || $hasOptions === 'yes'){ + $buildFieldsArray->push(" 'has_options'=>true,"); + } + else { + $buildFieldsArray->push(" 'has_options'=>false,"); + } + + + if($hasOptions === 'y' || $hasOptions === 'yes'){ + $options = $this->ask('options LIKE: male, female'); + $buildFieldsArray->push(" 'options'=>".$options. ","); + + $options = explode(',', $options); + $options = array_map(function ($option){ + return trim($option); + }, $options); + } + + $buildFieldsArray->push(" ],"); + + $createNewField = new Field(); + $createNewField->label = $label; + $createNewField->key = $fieldKey ?? Str::slug($fieldKey); + $createNewField->type = $type; + $createNewField->placeholder = $placeholder; + $createNewField->is_required = ($required || $required === 'n' || $required === 'no') ? false : true; + $createNewField->has_options = ($hasOptions === 'y' || $hasOptions === 'yes') ? true : false; + + $createNewField->save(); + + if($hasOptions === 'y' || $hasOptions === 'yes'){ + foreach ($options as $option){ + $createNewField->options()->create([ + 'type' => 'text', + 'label' => $option, + 'value' => $option, + ]); + } + } + + $fields[] = $createNewField->id; + } + + } + else { + $buildFieldsArray->push(" ["); + $buildFieldsArray->push(" 'label'=>'".$checkIfFieldExists->label. "',"); + $buildFieldsArray->push(" 'key'=>'".$checkIfFieldExists->key. "',"); + $buildFieldsArray->push(" 'type'=>'".$checkIfFieldExists->type. "',"); + $buildFieldsArray->push(" 'placeholder'=>'".$checkIfFieldExists->placeholder. "',"); + if($checkIfFieldExists->is_required){ + $buildFieldsArray->push(" 'is_required'=>true,"); + } + else { + $buildFieldsArray->push(" 'is_required'=>false,"); + } + if($checkIfFieldExists->has_options){ + $buildFieldsArray->push(" 'has_options'=>true,"); + } + else { + $buildFieldsArray->push(" 'has_options'=>false,"); + } + if($checkIfFieldExists->has_options){ + $getOptions = ""; + foreach($checkIfFieldExists->options as $optionKey=>$option){ + $getOptions .= $option->value; + if($optionKey !== count($option)-1){ + $getOptions.= ","; + } + } + $buildFieldsArray->push(" 'options'=>".$getOptions. ","); + } + $buildFieldsArray->push(" ],"); + + $fields[] = $checkIfFieldExists->id; + } + + $addMore = $this->ask('Do you want to add more fields? [yes/no]', 'no'); + $add = $addMore === 'yes' || $addMore === 'y' ? true : false; + } + + $createNewForm = new Form(); + $createNewForm->name = $title; + $createNewForm->key = $key; + $createNewForm->endpoint = $endpoint ?? '/'; + $createNewForm->method = $method ?? 'POST'; + $createNewForm->description = $description ?? null; + $createNewForm->type = $type; + $createNewForm->save(); + + $createNewForm->fields()->attach($fields); + + $this->generateStubs( + __DIR__ . '/../../stubs/migration.stub', + database_path('migrations/'.date('Y_m_d_His').'_fill_form_for_'.Str::lower($key).'.php'), + [ + 'name' => Str::ucfirst(Str::camel($key)), + 'key' => $key, + 'fields' => $buildFieldsArray->implode("\n"), + 'formName' => $title, + 'formEndpoint' => $endpoint ?? '/', + 'formMethod' => $method ?? 'POST', + 'formDescription' => $description ?? '', + 'formType' => $type, + ] + ); + + $this->info('Form created successfully'); + } +} diff --git a/src/Filament/Resources/.gitkeep b/src/Filament/Resources/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/Filament/Resources/FormOptionResource.php b/src/Filament/Resources/FormOptionResource.php new file mode 100644 index 0000000..8be067e --- /dev/null +++ b/src/Filament/Resources/FormOptionResource.php @@ -0,0 +1,138 @@ +schema([ + Forms\Components\TextInput::make('form_id') + ->required() + ->numeric(), + Forms\Components\TextInput::make('type') + ->maxLength(255) + ->default('text'), + Forms\Components\TextInput::make('label'), + Forms\Components\TextInput::make('placeholder'), + Forms\Components\TextInput::make('hint'), + Forms\Components\TextInput::make('name') + ->required() + ->maxLength(255), + Forms\Components\TextInput::make('group') + ->maxLength(255), + Forms\Components\TextInput::make('default'), + Forms\Components\TextInput::make('order') + ->numeric() + ->default(0), + Forms\Components\Toggle::make('is_required'), + Forms\Components\Toggle::make('is_multi'), + Forms\Components\TextInput::make('required_message'), + Forms\Components\Toggle::make('is_reactive'), + Forms\Components\TextInput::make('reactive_field') + ->maxLength(255), + Forms\Components\TextInput::make('reactive_where') + ->maxLength(255), + Forms\Components\Toggle::make('is_relation'), + Forms\Components\TextInput::make('relation_name') + ->maxLength(255), + Forms\Components\TextInput::make('relation_column') + ->maxLength(255), + Forms\Components\Toggle::make('has_options'), + Forms\Components\TextInput::make('options'), + Forms\Components\Toggle::make('has_validation'), + Forms\Components\TextInput::make('validation'), + Forms\Components\TextInput::make('meta'), + ]); + } + + public static function table(Table $table): Table + { + return $table + ->columns([ + Tables\Columns\TextColumn::make('form_id') + ->numeric() + ->sortable(), + Tables\Columns\TextColumn::make('type') + ->searchable(), + Tables\Columns\TextColumn::make('name') + ->searchable(), + Tables\Columns\TextColumn::make('group') + ->searchable(), + Tables\Columns\TextColumn::make('order') + ->numeric() + ->sortable(), + Tables\Columns\IconColumn::make('is_required') + ->boolean(), + Tables\Columns\IconColumn::make('is_multi') + ->boolean(), + Tables\Columns\IconColumn::make('is_reactive') + ->boolean(), + Tables\Columns\TextColumn::make('reactive_field') + ->searchable(), + Tables\Columns\TextColumn::make('reactive_where') + ->searchable(), + Tables\Columns\IconColumn::make('is_relation') + ->boolean(), + Tables\Columns\TextColumn::make('relation_name') + ->searchable(), + Tables\Columns\TextColumn::make('relation_column') + ->searchable(), + Tables\Columns\IconColumn::make('has_options') + ->boolean(), + Tables\Columns\IconColumn::make('has_validation') + ->boolean(), + Tables\Columns\TextColumn::make('created_at') + ->dateTime() + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + Tables\Columns\TextColumn::make('updated_at') + ->dateTime() + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + ]) + ->filters([ + // + ]) + ->actions([ + Tables\Actions\EditAction::make(), + ]) + ->bulkActions([ + Tables\Actions\BulkActionGroup::make([ + Tables\Actions\DeleteBulkAction::make(), + ]), + ]); + } + + public static function getRelations(): array + { + return [ + // + ]; + } + + public static function getPages(): array + { + return [ + 'index' => Pages\ListFormOptions::route('/'), + 'create' => Pages\CreateFormOption::route('/create'), + 'edit' => Pages\EditFormOption::route('/{record}/edit'), + ]; + } +} diff --git a/src/Filament/Resources/FormOptionResource/Pages/CreateFormOption.php b/src/Filament/Resources/FormOptionResource/Pages/CreateFormOption.php new file mode 100644 index 0000000..51aabc9 --- /dev/null +++ b/src/Filament/Resources/FormOptionResource/Pages/CreateFormOption.php @@ -0,0 +1,12 @@ +schema([ + Forms\Components\TextInput::make('model_id') + ->numeric(), + Forms\Components\TextInput::make('model_type') + ->maxLength(255), + Forms\Components\TextInput::make('form_request_id') + ->required() + ->numeric(), + Forms\Components\TextInput::make('key') + ->required() + ->maxLength(255), + Forms\Components\TextInput::make('value'), + ]); + } + + public static function table(Table $table): Table + { + return $table + ->columns([ + Tables\Columns\TextColumn::make('model_id') + ->numeric() + ->sortable(), + Tables\Columns\TextColumn::make('model_type') + ->searchable(), + Tables\Columns\TextColumn::make('form_request_id') + ->numeric() + ->sortable(), + Tables\Columns\TextColumn::make('key') + ->searchable(), + Tables\Columns\TextColumn::make('created_at') + ->dateTime() + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + Tables\Columns\TextColumn::make('updated_at') + ->dateTime() + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + ]) + ->filters([ + // + ]) + ->actions([ + Tables\Actions\EditAction::make(), + ]) + ->bulkActions([ + Tables\Actions\BulkActionGroup::make([ + Tables\Actions\DeleteBulkAction::make(), + ]), + ]); + } + + public static function getRelations(): array + { + return [ + // + ]; + } + + public static function getPages(): array + { + return [ + 'index' => Pages\ListFormRequestMetas::route('/'), + 'create' => Pages\CreateFormRequestMeta::route('/create'), + 'edit' => Pages\EditFormRequestMeta::route('/{record}/edit'), + ]; + } +} diff --git a/src/Filament/Resources/FormRequestMetaResource/Pages/CreateFormRequestMeta.php b/src/Filament/Resources/FormRequestMetaResource/Pages/CreateFormRequestMeta.php new file mode 100644 index 0000000..329bf4d --- /dev/null +++ b/src/Filament/Resources/FormRequestMetaResource/Pages/CreateFormRequestMeta.php @@ -0,0 +1,12 @@ +schema([ + Forms\Components\TextInput::make('model_type') + ->maxLength(255), + Forms\Components\TextInput::make('model_id') + ->numeric(), + Forms\Components\TextInput::make('service_type') + ->maxLength(255), + Forms\Components\TextInput::make('service_id') + ->numeric(), + Forms\Components\TextInput::make('form_id') + ->required() + ->numeric(), + Forms\Components\TextInput::make('status') + ->maxLength(255) + ->default('pending'), + Forms\Components\TextInput::make('payload'), + Forms\Components\Textarea::make('description') + ->columnSpanFull(), + Forms\Components\DatePicker::make('date'), + Forms\Components\TextInput::make('time'), + ]); + } + + public static function table(Table $table): Table + { + return $table + ->columns([ + Tables\Columns\TextColumn::make('model_type') + ->searchable(), + Tables\Columns\TextColumn::make('model_id') + ->numeric() + ->sortable(), + Tables\Columns\TextColumn::make('service_type') + ->searchable(), + Tables\Columns\TextColumn::make('service_id') + ->numeric() + ->sortable(), + Tables\Columns\TextColumn::make('form_id') + ->numeric() + ->sortable(), + Tables\Columns\TextColumn::make('status') + ->searchable(), + Tables\Columns\TextColumn::make('date') + ->date() + ->sortable(), + Tables\Columns\TextColumn::make('time'), + Tables\Columns\TextColumn::make('created_at') + ->dateTime() + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + Tables\Columns\TextColumn::make('updated_at') + ->dateTime() + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + ]) + ->filters([ + // + ]) + ->actions([ + Tables\Actions\EditAction::make(), + ]) + ->bulkActions([ + Tables\Actions\BulkActionGroup::make([ + Tables\Actions\DeleteBulkAction::make(), + ]), + ]); + } + + public static function getRelations(): array + { + return [ + // + ]; + } + + public static function getPages(): array + { + return [ + 'index' => Pages\ListFormRequests::route('/'), + 'create' => Pages\CreateFormRequest::route('/create'), + 'edit' => Pages\EditFormRequest::route('/{record}/edit'), + ]; + } +} diff --git a/src/Filament/Resources/FormRequestResource/Pages/CreateFormRequest.php b/src/Filament/Resources/FormRequestResource/Pages/CreateFormRequest.php new file mode 100644 index 0000000..abf8126 --- /dev/null +++ b/src/Filament/Resources/FormRequestResource/Pages/CreateFormRequest.php @@ -0,0 +1,12 @@ +label(trans('filament-cms::messages.forms.columns.type')) + ->searchable() + ->options([ + "page" => "Page", + "modal" => "Modal", + "slideover" => "Slideover", + ]) + ->default('page'), + Forms\Components\Select::make('method') + ->label(trans('filament-cms::messages.forms.columns.method')) + ->searchable() + ->options([ + "POST" => "POST", + "GET" => "GET", + "PUT" => "PUT", + "DELETE" => "DELETE", + "PATCH" => "PATCH", + ]) + ->default('POST'), + Forms\Components\TextInput::make('title') + ->label(trans('filament-cms::messages.forms.columns.title')), + Forms\Components\TextInput::make('key') + ->label(trans('filament-cms::messages.forms.columns.key')) + ->default(Str::random(6)) + ->unique(ignoreRecord: true) + ->required() + ->maxLength(255), + Forms\Components\Textarea::make('description') + ->label(trans('filament-cms::messages.forms.columns.description')) + ->columnSpanFull(), + Forms\Components\TextInput::make('endpoint') + ->label(trans('filament-cms::messages.forms.columns.endpoint')) + ->columnSpanFull() + ->maxLength(255) + ->default('/'), + Forms\Components\Toggle::make('is_active') + ->label(trans('filament-cms::messages.forms.columns.is_active')), + ]; + return $form + ->schema(fn($record) => $record ? [ + Forms\Components\Section::make(trans('filament-cms::messages.forms.section.information')) + ->collapsible() + ->collapsed(fn($record) => $record) + ->schema($formSchema), + ] : $formSchema); + } + + public static function table(Table $table): Table + { + return $table + ->columns([ + Tables\Columns\TextColumn::make('type') + ->label(trans('filament-cms::messages.forms.columns.type')) + ->searchable(), + Tables\Columns\TextColumn::make('title') + ->label(trans('filament-cms::messages.forms.columns.title')) + ->searchable(), + Tables\Columns\TextColumn::make('key') + ->label(trans('filament-cms::messages.forms.columns.key')) + ->searchable(), + Tables\Columns\TextColumn::make('endpoint') + ->label(trans('filament-cms::messages.forms.columns.endpoint')) + ->searchable(), + Tables\Columns\TextColumn::make('method') + ->label(trans('filament-cms::messages.forms.columns.method')) + ->searchable(), + Tables\Columns\IconColumn::make('is_active') + ->label(trans('filament-cms::messages.forms.columns.is_active')) + ->boolean(), + Tables\Columns\TextColumn::make('created_at') + ->dateTime() + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + Tables\Columns\TextColumn::make('updated_at') + ->dateTime() + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + ]) + ->filters([ + // + ]) + ->actions([ + Tables\Actions\EditAction::make(), + ]) + ->bulkActions([ + Tables\Actions\BulkActionGroup::make([ + Tables\Actions\DeleteBulkAction::make(), + ]), + ]); + } + + public static function getRelations(): array + { + return [ + RelationManagers\FormFieldsRelation::class, + RelationManagers\FormRequestsRelation::class, + ]; + } + + public static function getPages(): array + { + return [ + 'index' => Pages\ListForms::route('/'), + 'edit' => Pages\EditForm::route('/{record}/edit'), + ]; + } +} diff --git a/src/Filament/Resources/FormResource/Pages/CreateForm.php b/src/Filament/Resources/FormResource/Pages/CreateForm.php new file mode 100644 index 0000000..bd6d7bc --- /dev/null +++ b/src/Filament/Resources/FormResource/Pages/CreateForm.php @@ -0,0 +1,12 @@ +schema([ + Forms\Components\Tabs::make() + ->schema([ + Forms\Components\Tabs\Tab::make(trans('filament-cms::messages.forms.fields.tabs.general')) + ->icon('heroicon-s-information-circle') + ->schema([ + Forms\Components\Select::make('type') + ->label(trans('filament-cms::messages.forms.fields.columns.type')) + ->searchable() + ->options(FilamentCMSFormFields::getOptions()->pluck('label', 'name')->toArray()) + ->default('text'), + Forms\Components\TextInput::make('name') + ->label(trans('filament-cms::messages.forms.fields.columns.name')) + ->live() + ->afterStateUpdated(function (Forms\Get $get, Forms\Set $set, $state){ + if(str($state)->contains('email')){ + $set('type', 'email'); + } + if(str($state)->contains('phone')){ + $set('type', 'tel'); + } + if(str($state)->contains(['is_', 'has_'])){ + $set('type', 'toggle'); + } + if(str($state)->contains(['at', 'date'])){ + $set('type', 'date'); + } + if(str($state)->contains('password')){ + $set('type', 'password'); + } + if(str($state)->contains(['description', 'message'])){ + $set('type', 'textarea'); + } + if(str($state)->contains(['body', 'about'])){ + $set('type', 'rich'); + } + if(str($state)->contains('price')){ + $set('type', 'number'); + } + + }) + ->required() + ->maxLength(255), + Forms\Components\TextInput::make('group') + ->label(trans('filament-cms::messages.forms.fields.columns.group')) + ->maxLength(255), + Forms\Components\TextInput::make('default') + ->label(trans('filament-cms::messages.forms.fields.columns.default')), + ])->columns(2), +// Forms\Components\Tabs\Tab::make('Reactive') +// ->schema([ +// Forms\Components\Toggle::make('is_reactive') +// ->live(), +// Forms\Components\Select::make('reactive_field') +// ->hidden(fn(Forms\Get $get) => !$get('is_reactive')) +// ->searchable() +// ->options(function(){ +// return FormOption::query()->where('form_id', $this->getOwnerRecord()->id)->pluck('name', 'name')->toArray(); +// }), +// Forms\Components\Repeater::make('reactive_where') +// ->hidden(fn(Forms\Get $get) => !$get('is_reactive')) +// ->schema([ +// Forms\Components\Select::make('field') +// ->searchable() +// ->options(function(){ +// return FormOption::query()->where('form_id', $this->getOwnerRecord()->id)->pluck('name', 'name')->toArray(); +// }), +// Forms\Components\Select::make('operator') +// ->options([ +// '=' => '=', +// '!=' => '!=', +// '>' => '>', +// '<' => '<', +// '>=' => '>=', +// '<=' => '<=' +// ]), +// Forms\Components\TextInput::make('value') +// ->maxLength(255) +// ])->columns(3), +// ]), + Forms\Components\Tabs\Tab::make(trans('filament-cms::messages.forms.fields.tabs.relation')) + ->icon('heroicon-s-squares-plus') + ->schema([ + Forms\Components\Toggle::make('is_relation') + ->label(trans('filament-cms::messages.forms.fields.columns.is_relation')) + ->columnSpanFull() + ->live(), + Forms\Components\TextInput::make('relation_name') + ->label(trans('filament-cms::messages.forms.fields.columns.relation_name')) + ->hidden(fn(Forms\Get $get) => !$get('is_relation')) + ->maxLength(255), + Forms\Components\TextInput::make('relation_column') + ->label(trans('filament-cms::messages.forms.fields.columns.relation_column')) + ->hidden(fn(Forms\Get $get) => !$get('is_relation')) + ->maxLength(255), + ])->columns(2), + Forms\Components\Tabs\Tab::make(trans('filament-cms::messages.forms.fields.tabs.options')) + ->icon('heroicon-s-rectangle-group') + ->schema([ + Forms\Components\Select::make('sub_form') + ->label(trans('filament-cms::messages.forms.fields.columns.sub_form')) + ->searchable() + ->options(\TomatoPHP\FilamentFormBuilder\Models\Form::query()->where('id', '!=', $this->getOwnerRecord()->id)->pluck('key', 'id')->toArray()), + Forms\Components\Toggle::make('is_multi') + ->label(trans('filament-cms::messages.forms.fields.columns.is_multi')), + Forms\Components\Toggle::make('has_options') + ->label(trans('filament-cms::messages.forms.fields.columns.has_options')) + ->live(), + Forms\Components\Repeater::make('options') + ->label(trans('filament-cms::messages.forms.fields.columns.options')) + ->schema([ + Translation::make('label')->label(trans('filament-cms::messages.forms.fields.columns.label')), + Forms\Components\TextInput::make('value')->label(trans('filament-cms::messages.forms.fields.columns.value')), + ]) + ->hidden(fn(Forms\Get $get) => !$get('has_options')), + ]), + Forms\Components\Tabs\Tab::make(trans('filament-cms::messages.forms.fields.tabs.labels')) + ->icon('heroicon-s-language') + ->schema([ + Translation::make('label') + ->label(trans('filament-cms::messages.forms.fields.columns.label')), + Translation::make('placeholder') + ->label(trans('filament-cms::messages.forms.fields.columns.placeholder')), + Translation::make('hint') + ->label(trans('filament-cms::messages.forms.fields.columns.hint')), + ]), + Forms\Components\Tabs\Tab::make(trans('filament-cms::messages.forms.fields.tabs.validation')) + ->icon('heroicon-s-variable') + ->schema([ + Forms\Components\Toggle::make('is_required') + ->label(trans('filament-cms::messages.forms.fields.columns.is_required')) + ->live(), + Translation::make('required_message') + ->label(trans('filament-cms::messages.forms.fields.columns.required_message')) + ->hidden(fn(Forms\Get $get) => !$get('is_required')), + Forms\Components\Toggle::make('has_validation') + ->label(trans('filament-cms::messages.forms.fields.columns.has_validation')) + ->live(), + Forms\Components\Repeater::make('validation') + ->label(trans('filament-cms::messages.forms.fields.columns.validation')) + ->schema([ + Forms\Components\TextInput::make('rule')->label(trans('filament-cms::messages.forms.fields.columns.rule')), + Translation::make('message')->label(trans('filament-cms::messages.forms.fields.columns.message')), + ]) + ->hidden(fn(Forms\Get $get) => !$get('has_validation')), + ]) + ]) + ])->columns(1); + } + + public function table(Table $table): Table + { + return $table + ->headerActions([ + Tables\Actions\CreateAction::make() + ->icon('heroicon-s-plus-circle') + ->after(function(array $data, $record){ + $record->name = Str::of($record->name)->replace(' ', '_')->lower()->toString(); + $record->save(); + }), + Tables\Actions\Action::make('preview') + ->label(trans('filament-cms::messages.forms.fields.actions.preview')) + ->icon('heroicon-s-eye') + ->color('info') + ->form(function (){ + return FilamentCMSFormBuilder::make($this->getOwnerRecord()->key)->build(); + })->action(function (array $data){ + FilamentCMSFormBuilder::make($this->getOwnerRecord()->key)->send($data); + }) + ]) + ->actions([ + Tables\Actions\EditAction::make()->after(function(array $data, $record){ + $record->name = Str::of($record->name)->replace(' ', '_')->lower()->toString(); + $record->save(); + }), + Tables\Actions\DeleteAction::make() + ]) + ->columns([ + Tables\Columns\TextColumn::make('type') + ->label(trans('filament-cms::messages.forms.fields.columns.type')) + ->badge() + ->icon(fn($record) => FilamentCMSFormFields::getOptions()->where('name', $record->type)->first()->icon) + ->color(fn($record) => FilamentCMSFormFields::getOptions()->where('name', $record->type)->first()->color) + ->state(fn($record) => FilamentCMSFormFields::getOptions()->where('name', $record->type)->first()->label) + ->searchable(), + Tables\Columns\TextColumn::make('name') + ->label(trans('filament-cms::messages.forms.fields.columns.name')) + ->searchable(), + Tables\Columns\ToggleColumn::make('is_required') + ->label(trans('filament-cms::messages.forms.fields.columns.is_required')) + ->searchable(), + Tables\Columns\TextColumn::make('created_at') + ->dateTime() + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + Tables\Columns\TextColumn::make('updated_at') + ->dateTime() + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + ]) + ->groups([ + Tables\Grouping\Group::make('group') + ]) + ->defaultSort('created_at') + ->bulkActions([ + Tables\Actions\DeleteBulkAction::make() + ]) + ->reorderable('order'); + } +} diff --git a/src/Filament/Resources/FormResource/RelationManagers/FormRequestsRelation.php b/src/Filament/Resources/FormResource/RelationManagers/FormRequestsRelation.php new file mode 100644 index 0000000..fef1793 --- /dev/null +++ b/src/Filament/Resources/FormResource/RelationManagers/FormRequestsRelation.php @@ -0,0 +1,173 @@ +schema([ + Forms\Components\Select::make('status') + ->label(trans('filament-cms::messages.forms.requests.columns.status')) + ->searchable() + ->options([ + "pending" => trans('filament-cms::messages.forms.requests.columns.pending'), + "processing" => trans('filament-cms::messages.forms.requests.columns.processing'), + "completed" => trans('filament-cms::messages.forms.requests.columns.completed'), + "cancelled" => trans('filament-cms::messages.forms.requests.columns.cancelled'), + ]) + ->columnSpanFull() + ->default('pending'), + ]); + } + + public function infolist(Infolist $infolist): Infolist + { + + return $infolist->schema([ + TextEntry::make('description') + ->label(trans('filament-cms::messages.forms.requests.columns.description')) + ->columnSpanFull(), + TextEntry::make('time') + ->label(trans('filament-cms::messages.forms.requests.columns.time')), + TextEntry::make('date') + ->label(trans('filament-cms::messages.forms.requests.columns.date')), + KeyValueEntry::make('payload') + ->label(trans('filament-cms::messages.forms.requests.columns.payload')) + ->columnSpanFull() + ->schema(function(FormRequest $record){ + $getEntryText = []; + foreach ($record->payload as $key=>$value){ + $field = $record->form->fields->where('key', $key)->first(); + $getEntryText[] = TextEntry::make($key) + ->label($field->label ?? str($key)->title()) + ->default($value) + ->columnSpanFull(); + } + + return $getEntryText; + }) + ->columns(2) + ]); + } + + public function table(Table $table): Table + { + return $table + ->columns([ + Tables\Columns\TextColumn::make('status') + ->label(trans('filament-cms::messages.forms.requests.columns.status')) + ->badge() + ->state(fn($record) => match($record->status) { + "pending" => trans('filament-cms::messages.forms.requests.columns.pending'), + "processing" => trans('filament-cms::messages.forms.requests.columns.processing'), + "completed" => trans('filament-cms::messages.forms.requests.columns.completed'), + "cancelled" => trans('filament-cms::messages.forms.requests.columns.cancelled'), + default => $record->status, + }) + ->icon(fn($record) => match($record->status) { + 'pending' => 'heroicon-s-rectangle-stack', + 'processing' => 'heroicon-s-arrow-path', + 'completed' => 'heroicon-s-check-circle', + 'cancelled' => 'heroicon-s-x-circle', + default => 'heroicon-s-x-circle', + }) + ->color(fn($record) => match($record->status) { + 'pending' => 'info', + 'processing' => 'warning', + 'completed' => 'success', + 'cancelled' => 'danger', + default => 'secondary', + }) + ->searchable(), + Tables\Columns\TextColumn::make('description') + ->label(trans('filament-cms::messages.forms.requests.columns.description')), + Tables\Columns\TextColumn::make('date') + ->label(trans('filament-cms::messages.forms.requests.columns.date')) + ->date() + ->sortable(), + Tables\Columns\TextColumn::make('time') + ->label(trans('filament-cms::messages.forms.requests.columns.time')), + Tables\Columns\TextColumn::make('created_at') + ->dateTime() + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + Tables\Columns\TextColumn::make('updated_at') + ->dateTime() + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + ]) + ->filters([ + Tables\Filters\SelectFilter::make('status') + ->label(trans('filament-cms::messages.forms.requests.columns.status')) + ->searchable() + ->options([ + "pending" => trans('filament-cms::messages.forms.requests.columns.pending'), + "processing" => trans('filament-cms::messages.forms.requests.columns.processing'), + "completed" => trans('filament-cms::messages.forms.requests.columns.completed'), + "cancelled" => trans('filament-cms::messages.forms.requests.columns.cancelled'), + ]) + ->columnSpanFull(), + ]) + ->defaultSort('created_at', 'desc') + ->groups([ + Tables\Grouping\Group::make('status') + ]) + ->actions([ + Tables\Actions\ViewAction::make(), + Tables\Actions\EditAction::make(), + ]) + ->bulkActions([ + Tables\Actions\BulkActionGroup::make([ + Tables\Actions\DeleteBulkAction::make(), + ]), + ]); + } + +} diff --git a/src/FilamentFormBuilderPlugin.php b/src/FilamentFormBuilderPlugin.php new file mode 100644 index 0000000..6a48388 --- /dev/null +++ b/src/FilamentFormBuilderPlugin.php @@ -0,0 +1,30 @@ +commands([ + \TomatoPHP\FilamentFormBuilder\Console\FilamentFormBuilderInstall::class, + ]); + + //Register Config file + $this->mergeConfigFrom(__DIR__.'/../config/filament-form-builder.php', 'filament-form-builder'); + + //Publish Config + $this->publishes([ + __DIR__.'/../config/filament-form-builder.php' => config_path('filament-form-builder.php'), + ], 'filament-form-builder-config'); + + //Register Migrations + $this->loadMigrationsFrom(__DIR__.'/../database/migrations'); + + //Publish Migrations + $this->publishes([ + __DIR__.'/../database/migrations' => database_path('migrations'), + ], 'filament-form-builder-migrations'); + //Register views + $this->loadViewsFrom(__DIR__.'/../resources/views', 'filament-form-builder'); + + //Publish Views + $this->publishes([ + __DIR__.'/../resources/views' => resource_path('views/vendor/filament-form-builder'), + ], 'filament-form-builder-views'); + + //Register Langs + $this->loadTranslationsFrom(__DIR__.'/../resources/lang', 'filament-form-builder'); + + //Publish Lang + $this->publishes([ + __DIR__.'/../resources/lang' => base_path('lang/vendor/filament-form-builder'), + ], 'filament-form-builder-lang'); + + //Register Routes + $this->loadRoutesFrom(__DIR__.'/../routes/web.php'); + + } + + public function boot(): void + { + FilamentCMSFormFields::register([ + CmsFormFieldType::make('text') + ->label('Text'), + CmsFormFieldType::make('textarea') + ->className(Textarea::class) + ->color('warning') + ->icon('heroicon-s-document-text') + ->label('Textarea'), + CmsFormFieldType::make('select') + ->className(Select::class) + ->color('info') + ->icon('heroicon-s-squares-plus') + ->label('Select'), + CmsFormFieldType::make('checkbox') + ->className(Checkbox::class) + ->color('danger') + ->icon('heroicon-s-check') + ->label('Checkbox'), + CmsFormFieldType::make('radio') + ->className(Radio::class) + ->color('success') + ->icon('heroicon-s-check-circle') + ->label('Radio'), + CmsFormFieldType::make('file') + ->className(FileUpload::class) + ->color('info') + ->icon('heroicon-s-document-arrow-up') + ->label('File'), + CmsFormFieldType::make('date') + ->className(DatePicker::class) + ->color('success') + ->icon('heroicon-s-calendar') + ->label('Date'), + CmsFormFieldType::make('time') + ->className(TimePicker::class) + ->color('info') + ->icon('heroicon-s-clock') + ->label('Time'), + CmsFormFieldType::make('datetime') + ->className(DateTimePicker::class) + ->color('warning') + ->icon('heroicon-s-calendar-days') + ->label('DateTime'), + CmsFormFieldType::make('color') + ->className(ColorPicker::class) + ->color('success') + ->icon('heroicon-s-swatch') + ->label('Color'), + CmsFormFieldType::make('icon') + ->className(IconPicker::class) + ->color('info') + ->icon('heroicon-s-heart') + ->label('Icon'), + CmsFormFieldType::make('toggle') + ->className(Toggle::class) + ->color('success') + ->icon('heroicon-s-adjustments-horizontal') + ->label('Toggle'), + CmsFormFieldType::make('password') + ->color('danger') + ->icon('heroicon-s-lock-closed') + ->label('Password'), + CmsFormFieldType::make('email') + ->color('info') + ->icon('heroicon-s-envelope') + ->label('Email'), + CmsFormFieldType::make('number') + ->color('success') + ->icon('heroicon-s-minus-circle') + ->label('Number'), + CmsFormFieldType::make('url') + ->color('primary') + ->icon('heroicon-s-globe-alt') + ->label('URL'), + CmsFormFieldType::make('tel') + ->color('warning') + ->icon('heroicon-s-phone') + ->label('Tel'), + CmsFormFieldType::make('markdown') + ->className(MarkdownEditor::class) + ->color('warning') + ->icon('heroicon-s-hashtag') + ->label('Markdown'), + CmsFormFieldType::make('rich') + ->className(RichEditor::class) + ->color('info') + ->icon('heroicon-s-document-text') + ->label('RichText'), + CmsFormFieldType::make('keyValue') + ->className(KeyValue::class) + ->color('danger') + ->icon('heroicon-s-key') + ->label('Key/Value'), + CmsFormFieldType::make('repeater') + ->className(Repeater::class) + ->icon('heroicon-s-rectangle-group') + ->label('Repeater'), + ]); + } +} diff --git a/src/Models/.gitkeep b/src/Models/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/Models/Form.php b/src/Models/Form.php new file mode 100644 index 0000000..201e2a3 --- /dev/null +++ b/src/Models/Form.php @@ -0,0 +1,57 @@ + 'boolean', + ]; + /** + * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany + */ + public function fields() + { + return $this->hasMany(FormOption::class, 'form_id', 'id')->orderBy('order', 'asc'); + } + + public function requests() + { + return $this->hasMany(FormRequest::class, 'form_id', 'id'); + } +} diff --git a/src/Models/FormOption.php b/src/Models/FormOption.php new file mode 100644 index 0000000..d77344c --- /dev/null +++ b/src/Models/FormOption.php @@ -0,0 +1,71 @@ + "json", + 'options' => "array", + 'validation' => "array", + 'lable' => "array", + 'hint'=> "array", + 'placeholder'=> "array", + 'required_message'=> "array", + 'reactive_where'=> "array", + 'has_options' => "boolean", + 'has_validation' => "boolean", + 'is_required'=> "boolean", + 'is_multi'=> "boolean", + 'is_reactive'=> "boolean", + 'is_from_table'=> "boolean" + ]; + + + public function subForm() + { + return $this->belongsTo(Form::class, 'sub_form', 'id'); + } + +} diff --git a/src/Models/FormRequest.php b/src/Models/FormRequest.php new file mode 100644 index 0000000..5139602 --- /dev/null +++ b/src/Models/FormRequest.php @@ -0,0 +1,82 @@ + "array" + ]; + + /** + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function form() + { + return $this->belongsTo('TomatoPHP\FilamentFormBuilder\Models\Form'); + } + + public function formRequestsMetas(){ + return $this->hasMany(FormRequestMeta::class, 'form_request_id'); + } + + public function meta(string $key, string|null $value=null): Model|string|null + { + if($value){ + return $this->formRequestsMetas()->updateOrCreate(['key' => $key], ['value' => $value]); + } + else { + return $this->formRequestsMetas()->where('key', $key)->first()?->value; + } + } + + public function modelable() + { + return $this->morphTo(); + } + + public function serviceable() + { + return $this->morphTo(); + } +} diff --git a/src/Models/FormRequestMeta.php b/src/Models/FormRequestMeta.php new file mode 100644 index 0000000..ff16090 --- /dev/null +++ b/src/Models/FormRequestMeta.php @@ -0,0 +1,36 @@ + 'array', + ]; + + public function formRequest(){ + return $this->belongsTo('TomatoPHP\FilamentFormBuilder\Models\FormRequest'); + } +} diff --git a/src/Services/Contracts/CmsFormFieldType.php b/src/Services/Contracts/CmsFormFieldType.php new file mode 100644 index 0000000..12262e6 --- /dev/null +++ b/src/Services/Contracts/CmsFormFieldType.php @@ -0,0 +1,102 @@ +name($name); + } + + /** + * @param string $name + * @return $this + */ + public function name(string $name): self + { + $this->name = $name; + return $this; + } + + /** + * @param string $label + * @return $this + */ + public function label(string $label): self + { + $this->label = $label; + return $this; + } + + /** + * @param string $color + * @return $this + */ + public function color(string $color): self + { + $this->color = $color; + return $this; + } + + /** + * @param string $icon + * @return $this + */ + public function icon(string $icon): self + { + $this->icon = $icon; + return $this; + } + + /** + * @param string $className + * @return $this + */ + public function className(string $className): self + { + $this->className = $className; + return $this; + } + + /** + * @return array + */ + public function toArray(): array + { + return [ + 'name' => $this->name, + 'label' => $this->label, + 'color' => $this->color, + 'icon' => $this->icon, + 'className' => $this->className, + ]; + } +} diff --git a/src/Services/Contracts/Form.php b/src/Services/Contracts/Form.php new file mode 100644 index 0000000..e1306c4 --- /dev/null +++ b/src/Services/Contracts/Form.php @@ -0,0 +1,97 @@ +setLocale($lang->id ?? config('app.locale')); + }catch (\Exception $exception) {} + } + + public function toArray(): array + { + return [ + 'title' => $this->title, + 'description' => $this->description, + 'name' => $this->name, + 'key' => $this->key, + 'type' => $this->type, + 'method' => $this->method, + 'endpoint' => $this->endpoint, + 'inputs' => $this->inputs, + ]; + } + + /** + * @return static + */ + public static function make(): static + { + return (new static); + } + + public function title(string $title): static + { + $this->title = $title; + return $this; + } + + public function description(string $description): static + { + $this->description = $description; + return $this; + } + + public function key(string $key): static + { + $this->key = $key; + return $this; + } + + public function name(string $name): static + { + $this->name = $name; + return $this; + } + + public function method(string $method): static + { + $this->method = $method; + return $this; + } + + public function type(string $type): static + { + $this->type = $type; + return $this; + } + + public function endpoint(string $endpoint): static + { + $this->endpoint = $endpoint; + return $this; + } + + public function inputs(array $inputs): static + { + $this->inputs = $inputs; + return $this; + } +} diff --git a/src/Services/Contracts/FormInput.php b/src/Services/Contracts/FormInput.php new file mode 100644 index 0000000..cb3e1c7 --- /dev/null +++ b/src/Services/Contracts/FormInput.php @@ -0,0 +1,238 @@ +setLocale($lang->id ?? config('app.locale')); + }catch (\Exception $exception) {} + } + + public function toArray(){ + return [ + 'label' => $this->label, + 'placeholder' => $this->placeholder, + 'hint' => $this->hint, + 'type' => $this->type, + 'name' => $this->name, + 'group' => $this->group, + 'default' => $this->default, + 'order' => $this->order, + 'required_message' => $this->required_message, + 'reactive_field' => $this->reactive_field, + 'reactive_where' => $this->reactive_where, + 'table_name' => $this->table_name, + 'options' => $this->options, + 'has_valdation' => $this->has_valdation, + 'is_requred' => $this->is_requred, + 'is_reavtive' => $this->is_reavtive, + 'is_from_table' => $this->is_from_table, + 'is_multi' => $this->is_multi, + 'has_options' => $this->has_options, + 'validation' => [ + 'max' => $this->max, + 'min' => $this->min, + 'type' => $this->input_type, + 'option_root' => $this->option_root, + 'option_value' => $this->option_value, + 'option_label' => $this->option_label, + ] + ]; + } + + + /** + * @return static + */ + public static function make(): static + { + return (new static); + } + + public function label(string $label): static + { + $this->label = $label; + return $this; + } + + public function input_type(string $input_type): static + { + $this->input_type = $input_type; + return $this; + } + + public function placeholder(string $placeholder): static + { + $this->placeholder = $placeholder; + return $this; + } + + public function hint(string $hint): static + { + $this->hint = $hint; + return $this; + } + + public function name(string $name): static + { + $this->name = $name; + return $this; + } + + public function group(string $group): static + { + $this->group = $group; + return $this; + } + + public function default(string $default): static + { + $this->default = $default; + return $this; + } + + public function order(int $order): static + { + $this->order = $order; + return $this; + } + + public function required_message(string $required_message): static + { + $this->required_message = $required_message; + return $this; + } + + public function reactive_field(string $reactive_field): static + { + $this->reactive_field = $reactive_field; + return $this; + } + + public function reactive_where(string $reactive_where): static + { + $this->reactive_where = $reactive_where; + return $this; + } + + public function table_name(string $table_name): static + { + $this->table_name = $table_name; + return $this; + } + + public function options(array $options): static + { + $this->options = $options; + return $this; + } + + public function has_valdation(bool $has_valdation): static + { + $this->has_valdation = $has_valdation; + return $this; + } + + public function is_requred(bool $is_requred): static + { + $this->is_requred = $is_requred; + return $this; + } + + public function is_reavtive(bool $is_reavtive): static + { + $this->is_reavtive = $is_reavtive; + return $this; + } + + public function is_from_table(bool $is_from_table): static + { + $this->is_from_table = $is_from_table; + return $this; + } + + public function is_multi(bool $is_multi): static + { + $this->is_multi = $is_multi; + return $this; + } + + public function has_options(bool $has_options): static + { + $this->has_options = $has_options; + return $this; + } + + public function max(int $max): static + { + $this->max = $max; + return $this; + } + + public function min(int $min): static + { + $this->min = $min; + return $this; + } + + public function type(string $type): static + { + $this->type = $type; + return $this; + } + + public function option_label(string $option_label): static + { + $this->option_label = $option_label; + return $this; + } + + public function option_value(string $option_value): static + { + $this->option_value = $option_value; + return $this; + } + + public function option_root(string $option_root): static + { + $this->option_root = $option_root; + return $this; + } + + + +} diff --git a/src/Services/Contracts/FormInputOption.php b/src/Services/Contracts/FormInputOption.php new file mode 100644 index 0000000..248f25f --- /dev/null +++ b/src/Services/Contracts/FormInputOption.php @@ -0,0 +1,58 @@ +setLocale($lang->id ?? config('app.locale')); + }catch (\Exception $exception) {} + } + + public function toArray():array + { + return [ + 'value' => $this->value, + 'label_ar' => $this->label_ar, + 'label_en' => $this->label_en, + ]; + } + + + /** + * @return static + */ + public static function make(): static + { + return (new static); + } + + public function value(string $value): static + { + $this->value = $value; + return $this; + } + + public function label_ar(string $label_ar): static + { + $this->label_ar = $label_ar; + return $this; + } + + public function label_en(string $label_en): static + { + $this->label_en = $label_en; + return $this; + } +} diff --git a/src/Services/FilamentCMSFormBuilder.php b/src/Services/FilamentCMSFormBuilder.php new file mode 100644 index 0000000..ae6ac30 --- /dev/null +++ b/src/Services/FilamentCMSFormBuilder.php @@ -0,0 +1,142 @@ +key($key); + } + + public function key(string $key): static + { + $this->key = $key; + $this->form = Form::query()->where('key', $this->key)->orWhere('id', (int)$this->key)->first(); + return $this; + } + + public function build(): array + { + $schema = []; + $form = $this->form; + if($form){ + $fields = $form->fields()->orderBy('order')->get(); + + foreach ($fields as $key=>$field){ + $getFiledBuilder = FilamentCMSFormFields::getOptions()->where('name', $field->type)->first(); + if($getFiledBuilder){ + $messages = []; + $title = Str::of($field->name)->title()->toString(); + $fieldBuild = $getFiledBuilder->className::make($field->name); + if($field->label){ + $fieldBuild->label($field->label); + } + if($field->hint){ + $fieldBuild->hint($field->hint); + } + if($field->placeholder){ + $fieldBuild->placeholder($field->placeholder); + } + if($field->is_required){ + $fieldBuild->required(); + $messages['required'] = $field->required_message[app()->getLocale()]??null; + } + if($field->default){ + $fieldBuild->default($field->default); + } + if($field->is_multi){ + $fieldBuild->multiple(); + } + if($field->type === 'number'){ + $fieldBuild->numeric(); + } + if($field->type === 'email'){ + $fieldBuild->email(); + } + if($field->type === 'tel'){ + $fieldBuild->tel(); + } + if($field->type === 'url'){ + $fieldBuild->url(); + } + if($field->type === 'password'){ + $fieldBuild->password(); + } +// if($field->is_reactive){ +// $fieldBuild->live(); +// } + if($field->has_options){ + $fieldBuild->options(collect($field->options)->map(function ($item){ + $item['label'] = $item['label'][app()->getLocale()]??null; + return $item; + })->pluck('label', 'value')->toArray()); + } + if($field->has_validation){ + $rules = []; + foreach ($field->validation as $rule){ + $messages[$rule['rule']] = $rule['message'][app()->getLocale()]??null; + $rules[] = $rule['rule']; + } + $fieldBuild->rules($rules); + } + if(count($messages)){ + $fieldBuild->validationMessages($messages); + } + if($field->sub_form){ + $fieldBuild->schema(static::make($field->sub_form)->build()); + } + if($field->is_relation){ + $fieldBuild->searchable(); + if(str($field->relation_name)->contains('\\')){ + $fieldBuild->options($field->relation_name::all()->pluck($field->relation_column, 'id')->toArray()); + } + else { + $fieldBuild->relationship($field->relation_name, $field->relation_column); + } + } + $schema[] = $fieldBuild; + } + + } + } + + return $schema; + } + + public function send(array $data): void + { + if(count($data)){ + $formRequest = new FormRequest(); + $formRequest->form_id = $this->form->id; + $formRequest->status = 'pending'; + $formRequest->payload = $data; + $formRequest->description = "Created From Form Preview"; + $formRequest->date = Carbon::now()->toDateString(); + $formRequest->time = Carbon::now()->toTimeString(); + $formRequest->save(); + + Notification::make() + ->title('Form Preview') + ->body('Form Preview has been created successfully') + ->success() + ->send(); + } + else { + Notification::make() + ->title('Form Preview') + ->body('Form Empty!') + ->danger() + ->send(); + } + } +} diff --git a/src/Services/FilamentCMSFormFields.php b/src/Services/FilamentCMSFormFields.php new file mode 100644 index 0000000..116c222 --- /dev/null +++ b/src/Services/FilamentCMSFormFields.php @@ -0,0 +1,27 @@ +forms[] = $form; + } + + public function getForms(): array + { + return $this->forms; + } + + public function build(): void + { + foreach ($this->forms as $form){ + $checkIfFormExists = \TomatoPHP\FilamentFormBuilder\Models\Form::where('key', $form->key)->first(); + if(!$checkIfFormExists){ + $newForm = \TomatoPHP\FilamentFormBuilder\Models\Form::create($form->toArray()); + $newForm->fields()->createMany($form->inputs); + } + } + } +} diff --git a/testbench.yaml b/testbench.yaml new file mode 100644 index 0000000..b930aa7 --- /dev/null +++ b/testbench.yaml @@ -0,0 +1,24 @@ +providers: + - BladeUI\Icons\BladeIconsServiceProvider + - BladeUI\Heroicons\BladeHeroiconsServiceProvider + - Filament\Actions\ActionsServiceProvider + - Filament\FilamentServiceProvider + - Filament\Forms\FormsServiceProvider + - Filament\Infolists\InfolistsServiceProvider + - Filament\Notifications\NotificationsServiceProvider + - Filament\Support\SupportServiceProvider + - Filament\Tables\TablesServiceProvider + - Filament\Widgets\WidgetsServiceProvider + - RyanChandler\BladeCaptureDirective\BladeCaptureDirectiveServiceProvider + - TomatoPHP\FilamentFormBuilder\FilamentFormBuilderServiceProvider + - TomatoPHP\FilamentFormBuilder\Tests\AdminPanelProvider +workbench: + welcome: true + install: true + start: / + guard: testing + discovers: + web: true + api: false + commands: false + views: true diff --git a/tests/Pest.php b/tests/Pest.php new file mode 100644 index 0000000..56c7a28 --- /dev/null +++ b/tests/Pest.php @@ -0,0 +1,5 @@ +in(__DIR__); diff --git a/tests/database/database.sqlite b/tests/database/database.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..cb9c4567851a96933c2d20eb6c1440eeb72754e6 GIT binary patch literal 245760 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 NswK-7XUpQz{{@MIBj5l4 literal 0 HcmV?d00001 diff --git a/tests/database/factories/.gitkeep b/tests/database/factories/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/database/factories/UserFactory.php b/tests/database/factories/UserFactory.php new file mode 100644 index 0000000..fb3ea56 --- /dev/null +++ b/tests/database/factories/UserFactory.php @@ -0,0 +1,23 @@ + $this->faker->name(), + 'email' => $this->faker->unique()->safeEmail(), + 'email_verified_at' => now(), + 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password + 'remember_token' => Str::random(10), + ]; + } +} diff --git a/tests/src/AdminPanelProvider.php b/tests/src/AdminPanelProvider.php new file mode 100644 index 0000000..e0ad8de --- /dev/null +++ b/tests/src/AdminPanelProvider.php @@ -0,0 +1,50 @@ +default() + ->id('admin') + ->path('admin') + ->login() + ->pages([ + Pages\Dashboard::class, + ]) + ->plugin( + FilamentFormBuilderPlugin::make() + ) + ->middleware([ + EncryptCookies::class, + AddQueuedCookiesToResponse::class, + StartSession::class, + AuthenticateSession::class, + ShareErrorsFromSession::class, + VerifyCsrfToken::class, + SubstituteBindings::class, + DisableBladeIconComponents::class, + DispatchServingFilamentEvent::class, + ]) + ->authMiddleware([ + Authenticate::class, + ]); + } +} diff --git a/tests/src/DebugTest.php b/tests/src/DebugTest.php new file mode 100644 index 0000000..91bf6cd --- /dev/null +++ b/tests/src/DebugTest.php @@ -0,0 +1,5 @@ +each->not->toBeUsed(); +}); diff --git a/tests/src/Models/.gitkeep b/tests/src/Models/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/src/Models/User.php b/tests/src/Models/User.php new file mode 100644 index 0000000..356810d --- /dev/null +++ b/tests/src/Models/User.php @@ -0,0 +1,34 @@ +set('database.default', 'sqlite'); + $app['config']->set('database.connections.sqlite.database', __DIR__ . '/../database/database.sqlite'); + + $app['config']->set('view.paths', [ + ...$app['config']->get('view.paths'), + __DIR__ . '/../resources/views', + ]); + } +}