Merge branch '2.x' into extract-dropdown

This commit is contained in:
Zep Fietje
2022-08-23 00:19:50 +02:00
262 changed files with 11498 additions and 41198 deletions
+1 -1
View File
@@ -11,5 +11,5 @@ trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
[*.{yml, yaml}]
[*.{yml,yaml}]
indent_size = 2
+87 -86
View File
@@ -2,91 +2,92 @@ name: Bug report
description: Report a problem you're experiencing
labels: bug,unconfirmed
body:
- type: markdown
attributes:
value: |
Before opening a bug report, please search the existing issues.
- type: markdown
attributes:
value: |
Before opening a bug report, please search the existing issues.
---
---
Thank you for taking the time to file a bug report. To address this bug as fast as possible, we need some information.
- type: dropdown
id: package
attributes:
label: Package
description: Which package is this bug report for?
options:
- filament/filament
- filament/forms
- filament/tables
- filament/spatie-laravel-media-library-plugin
- filament/spatie-laravel-settings-plugin
- filament/spatie-laravel-tags-plugin
- filament/spatie-laravel-translatable-plugin
- Other
validations:
required: true
- type: input
id: package-version
attributes:
label: Package Version
description: Please provide the full version of the package you have installed.
placeholder: v2.0.0
validations:
required: true
- type: input
id: laravel-version
attributes:
label: Laravel Version
description: Please provide the full Laravel version of your project.
placeholder: v8.0.0
validations:
required: true
- type: input
id: livewire-version
attributes:
label: Livewire Version
description: Please provide the full Livewire version of your project, if applicable.
placeholder: v2.0.0
- type: input
id: php-version
attributes:
label: PHP Version
description: Please provide the full PHP version of your server.
placeholder: PHP 8.0.0
validations:
required: true
- type: textarea
id: description
attributes:
label: Problem description
description: What happened when you experienced the problem?
validations:
required: true
- type: textarea
id: expectation
attributes:
label: Expected behavior
description: What did you expect to happen instead?
validations:
required: true
- type: textarea
id: steps
attributes:
label: Steps to reproduce
description: Which steps do we need to take to reproduce the problem? Any code examples need to be **as short as possible**, remove any code that is unrelated to the bug. **This issue will be automatically closed and not reviewed if detailed replication steps are missing.**
validations:
required: true
- type: input
id: reproduction
attributes:
label: Reproduction repository
description: The URL of a public Git repository which contains the minimal amount of code to reproduce the problem. This allows us to fix the problem much quicker. **This issue will be automatically closed and not reviewed if this is missing. Please make sure to format the URL starting with `https://github.com`.**
validations:
required: true
- type: textarea
id: logs
attributes:
label: Relevant log output
description: If applicable, provide relevant log output. No need for backticks here.
render: shell
Thank you for taking the time to file a bug report. To address this bug as fast as possible, we need some information.
- type: dropdown
id: package
attributes:
label: Package
description: Which package is this bug report for?
options:
- filament/filament
- filament/forms
- filament/tables
- filament/notifications
- filament/spatie-laravel-media-library-plugin
- filament/spatie-laravel-settings-plugin
- filament/spatie-laravel-tags-plugin
- filament/spatie-laravel-translatable-plugin
- Other
validations:
required: true
- type: input
id: package-version
attributes:
label: Package Version
description: Please provide the full version of the package you have installed.
placeholder: v2.0.0
validations:
required: true
- type: input
id: laravel-version
attributes:
label: Laravel Version
description: Please provide the full Laravel version of your project.
placeholder: v8.0.0
validations:
required: true
- type: input
id: livewire-version
attributes:
label: Livewire Version
description: Please provide the full Livewire version of your project, if applicable.
placeholder: v2.0.0
- type: input
id: php-version
attributes:
label: PHP Version
description: Please provide the full PHP version of your server.
placeholder: PHP 8.0.0
validations:
required: true
- type: textarea
id: description
attributes:
label: Problem description
description: What happened when you experienced the problem?
validations:
required: true
- type: textarea
id: expectation
attributes:
label: Expected behavior
description: What did you expect to happen instead?
validations:
required: true
- type: textarea
id: steps
attributes:
label: Steps to reproduce
description: Which steps do we need to take to reproduce the problem? Any code examples need to be **as short as possible**, remove any code that is unrelated to the bug. **This issue will be automatically closed and not reviewed if detailed replication steps are missing.**
validations:
required: true
- type: input
id: reproduction
attributes:
label: Reproduction repository
description: The URL of a public Git repository which contains the minimal amount of code to reproduce the problem. This allows us to fix the problem much quicker. **This issue will be automatically closed and not reviewed if this is missing. Please make sure to format the URL starting with `https://github.com`.**
validations:
required: true
- type: textarea
id: logs
attributes:
label: Relevant log output
description: If applicable, provide relevant log output. No need for backticks here.
render: shell
+27
View File
@@ -0,0 +1,27 @@
name: fix-code-style
on: push
jobs:
fix-code-style:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
with:
ref: ${{ github.head_ref }}
- name: Set up Node
uses: actions/setup-node@v2
- name: Install dependencies
run: |
composer install --no-interaction
npm ci
- name: Run Pint
run: composer pint
- name: Run Prettier
run: npm run prettier
- name: Commit changes
uses: stefanzweifel/git-auto-commit-action@v4
with:
commit_message: >
chore: fix code style
+147 -147
View File
@@ -1,197 +1,197 @@
name: manage-issue
on:
issues:
types: [opened, edited]
issues:
types: [opened, edited]
jobs:
check-repro:
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v3
with:
script: |
const URL_REGEXP = /### Reproduction repository[\r\n]+([^#]+)###/m
const REPRO_STEPS_REGEXP = /### Steps to reproduce[\r\n]+([^#]+)###/m
const LABEL_NEEDS_MORE_INFORMATION = 'needs more info'
const LABEL_UNCONFIRMED = 'unconfirmed'
check-repro:
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v3
with:
script: |
const URL_REGEXP = /### Reproduction repository[\r\n]+([^#]+)###/m
const REPRO_STEPS_REGEXP = /### Steps to reproduce[\r\n]+([^#]+)###/m
const LABEL_NEEDS_MORE_INFORMATION = 'needs more info'
const LABEL_UNCONFIRMED = 'unconfirmed'
function debug(...args) {
core.info(args.map(JSON.stringify).join(' '))
}
function debug(...args) {
core.info(args.map(JSON.stringify).join(' '))
}
if (context.payload.comment) {
debug('Ignoring comment update.')
if (context.payload.comment) {
debug('Ignoring comment update.')
return
}
return
}
const user = context.payload.sender.login
const issue = context.payload.issue
const body = issue.body
const urlMatch = body.match(URL_REGEXP)
const reproStepsMatch = body.match(REPRO_STEPS_REGEXP)
const url = urlMatch !== null ? urlMatch[1].trim() : null
const reproSteps = reproStepsMatch !== null ? reproStepsMatch[1].trim() : null
const user = context.payload.sender.login
const issue = context.payload.issue
const body = issue.body
const urlMatch = body.match(URL_REGEXP)
const reproStepsMatch = body.match(REPRO_STEPS_REGEXP)
const url = urlMatch !== null ? urlMatch[1].trim() : null
const reproSteps = reproStepsMatch !== null ? reproStepsMatch[1].trim() : null
debug(`Found URL '${url}'`)
debug(`Found repro steps '${reproSteps}'`)
debug(`Found URL '${url}'`)
debug(`Found repro steps '${reproSteps}'`)
async function createComment(comment) {
comment = comment
.split('\n')
.map((line) => line.trim())
.join('\n')
.trim()
async function createComment(comment) {
comment = comment
.split('\n')
.map((line) => line.trim())
.join('\n')
.trim()
await github.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: comment,
})
}
await github.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: comment,
})
}
async function getGitHubActionComments() {
debug(`Loading existing comments...`)
async function getGitHubActionComments() {
debug(`Loading existing comments...`)
const comments = await github.issues.listComments({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
})
const comments = await github.issues.listComments({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
})
return comments.data.filter(comment => {
debug(`comment by user: '${comment.user.login}'`)
return comment.user.login === 'github-actions[bot]'
})
}
return comments.data.filter(comment => {
debug(`comment by user: '${comment.user.login}'`)
return comment.user.login === 'github-actions[bot]'
})
}
async function getIssueLabels() {
const issues = await github.issues.listLabelsOnIssue({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
})
async function getIssueLabels() {
const issues = await github.issues.listLabelsOnIssue({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
})
return issues.data
}
return issues.data
}
async function updateIssue(state, state_reason = null) {
await github.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
state,
state_reason,
})
}
async function updateIssue(state, state_reason = null) {
await github.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
state,
state_reason,
})
}
async function closeWithComment(comment) {
if (issue.state !== 'open') {
debug(`Issue is not open`)
async function closeWithComment(comment) {
if (issue.state !== 'open') {
debug(`Issue is not open`)
return
}
return
}
const comments = await getGitHubActionComments()
const comments = await getGitHubActionComments()
if (comments.length > 0) {
debug(`Already commented on issue won't comment again`)
if (comments.length > 0) {
debug(`Already commented on issue won't comment again`)
return
}
return
}
debug(`Missing required information`)
debug(`Missing required information`)
await github.issues.addLabels({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
labels: [LABEL_NEEDS_MORE_INFORMATION],
})
await github.issues.addLabels({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
labels: [LABEL_NEEDS_MORE_INFORMATION],
})
await createComment(comment)
await createComment(comment)
await updateIssue('closed', 'not_planned')
}
await updateIssue('closed', 'not_planned')
}
async function openWithComment(comment) {
if (issue.state !== 'closed') {
debug(`Issue is already open`)
async function openWithComment(comment) {
if (issue.state !== 'closed') {
debug(`Issue is already open`)
return
}
return
}
const labels = await getIssueLabels()
const label = labels.find(label => label.name === LABEL_NEEDS_MORE_INFORMATION)
const labels = await getIssueLabels()
const label = labels.find(label => label.name === LABEL_NEEDS_MORE_INFORMATION)
if (! label) {
debug(`Issue was not tagged as needs information`)
if (! label) {
debug(`Issue was not tagged as needs information`)
return
}
return
}
const comments = await getGitHubActionComments()
const comments = await getGitHubActionComments()
if (comments.length === 0) {
debug(`Issue was closed by someone else, won't reopen`)
if (comments.length === 0) {
debug(`Issue was closed by someone else, won't reopen`)
return
}
return
}
debug(`Reopening closed issue`)
debug(`Reopening closed issue`)
await github.issues.removeLabel({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
name: LABEL_NEEDS_MORE_INFORMATION,
})
await github.issues.removeLabel({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
name: LABEL_NEEDS_MORE_INFORMATION,
})
await createComment(comment)
await createComment(comment)
await updateIssue('open')
}
await updateIssue('open')
}
const COMMENT_HEADER = `
Hey @${user}! We're sorry to hear that you've hit this issue. 💛
`.trim()
const COMMENT_HEADER = `
Hey @${user}! We're sorry to hear that you've hit this issue. 💛
`.trim()
const NO_REPRO_URL = (! url.includes('https://github.com/')) || url.includes('https://github.com/filamentphp/') || url === 'https://github.com/filamentphp'
const NO_REPRO_STEPS = reproSteps.length < 25
const NO_REPRO_URL = (! url.includes('https://github.com/')) || url.includes('https://github.com/filamentphp/') || url === 'https://github.com/filamentphp'
const NO_REPRO_STEPS = reproSteps.length < 25
if (NO_REPRO_URL || NO_REPRO_STEPS) {
let comment = `
${COMMENT_HEADER}
if (NO_REPRO_URL || NO_REPRO_STEPS) {
let comment = `
${COMMENT_HEADER}
`
`
if (NO_REPRO_URL) {
comment += `
However, it looks like you forgot to fill in the reproduction repository URL. Can you edit your original post and then we'll look at your issue?
if (NO_REPRO_URL) {
comment += `
However, it looks like you forgot to fill in the reproduction repository URL. Can you edit your original post and then we'll look at your issue?
We need a public Git repository which contains a Laravel app with the minimal amount of Filament code to reproduce the problem. That would allow us to download it and review your bug much easier, so it can be fixed quicker. Please make sure to include a database seeder with everything we need to set the app up quickly.
`
}
We need a public Git repository which contains a Laravel app with the minimal amount of Filament code to reproduce the problem. That would allow us to download it and review your bug much easier, so it can be fixed quicker. Please make sure to include a database seeder with everything we need to set the app up quickly.
`
}
if (NO_REPRO_URL && NO_REPRO_STEPS) {
comment += `
if (NO_REPRO_URL && NO_REPRO_STEPS) {
comment += `
Also, `
} else if (NO_REPRO_STEPS) {
comment += `
Also, `
} else if (NO_REPRO_STEPS) {
comment += `
However, `
}
However, `
}
if (NO_REPRO_STEPS) {
comment += `it doesn't look like you've provided much information on how to replicate the issue. Please edit your original post with clear steps we need to take.`
}
if (NO_REPRO_STEPS) {
comment += `it doesn't look like you've provided much information on how to replicate the issue. Please edit your original post with clear steps we need to take.`
}
closeWithComment(comment)
} else {
openWithComment(`
Thank you for providing reproduction steps! Reopening the issue now.
`)
}
closeWithComment(comment)
} else {
openWithComment(`
Thank you for providing reproduction steps! Reopening the issue now.
`)
}
+33 -33
View File
@@ -1,38 +1,38 @@
name: monorepo-split
on:
push:
tags: '*'
push:
tags: '*'
jobs:
split-monorepo:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
package:
- admin
- forms
- notifications
- spatie-laravel-media-library-plugin
- spatie-laravel-settings-plugin
- spatie-laravel-tags-plugin
- spatie-laravel-translatable-plugin
- support
- tables
steps:
- uses: actions/checkout@v2
- id: previous-tag
uses: "WyriHaximus/github-action-get-previous-tag@master"
- name: Monorepo Split of ${{ matrix.package }}
uses: symplify/github-action-monorepo-split@main
env:
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
with:
package_directory: 'packages/${{ matrix.package }}'
repository_organization: 'filamentphp'
repository_name: '${{ matrix.package }}'
branch: 2.x
tag: ${{ steps.previous-tag.outputs.tag }}
user_name: "Dan Harrin"
user_email: "dan@danharrin.com"
split-monorepo:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
package:
- admin
- forms
- notifications
- spatie-laravel-media-library-plugin
- spatie-laravel-settings-plugin
- spatie-laravel-tags-plugin
- spatie-laravel-translatable-plugin
- support
- tables
steps:
- uses: actions/checkout@v2
- id: previous-tag
uses: 'WyriHaximus/github-action-get-previous-tag@master'
- name: Monorepo Split of ${{ matrix.package }}
uses: symplify/github-action-monorepo-split@main
env:
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
with:
package_directory: 'packages/${{ matrix.package }}'
repository_organization: 'filamentphp'
repository_name: '${{ matrix.package }}'
branch: 2.x
tag: ${{ steps.previous-tag.outputs.tag }}
user_name: 'Dan Harrin'
user_email: 'dan@danharrin.com'
+24 -24
View File
@@ -1,33 +1,33 @@
name: npm-build
on:
pull_request:
branches:
- 2.x
types: [closed]
pull_request:
branches:
- 2.x
types: [closed]
jobs:
npm-build:
runs-on: ubuntu-latest
if: github.event.pull_request.merged == true
steps:
- name: Checkout code
uses: actions/checkout@v2
with:
ref: 2.x
npm-build:
runs-on: ubuntu-latest
if: github.event.pull_request.merged == true
steps:
- name: Checkout code
uses: actions/checkout@v2
with:
ref: 2.x
- name: Setup Node
uses: actions/setup-node@v2
- name: Setup Node
uses: actions/setup-node@v2
- name: Install dependencies
run: npm ci
- name: Install dependencies
run: npm ci
- name: Build assets
run: npm run build
- name: Build assets
run: npm run build
- name: Commit changes
uses: stefanzweifel/git-auto-commit-action@v4
with:
branch: 2.x
commit_message: >
chore: build assets
- name: Commit changes
uses: stefanzweifel/git-auto-commit-action@v4
with:
branch: 2.x
commit_message: >
chore: build assets
+33 -33
View File
@@ -1,38 +1,38 @@
name: phpstan
on:
push:
pull_request:
push:
pull_request:
jobs:
phpstan:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php: [ 8.1, '8.0' ]
laravel: [ 9.* ]
dependency-version: [ prefer-stable ]
include:
- laravel: 9.*
testbench: 7.*
name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }}
steps:
- uses: actions/checkout@v2
- name: Cache dependencies
uses: actions/cache@v2
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: mbstring, pdo, pdo_sqlite
coverage: none
- name: Install dependencies
run: |
composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" "filament/support" --no-interaction --no-update
composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction
- name: Run PHPStan
run: ./vendor/bin/phpstan --error-format=github
phpstan:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php: [8.1, '8.0']
laravel: [9.*]
dependency-version: [prefer-stable]
include:
- laravel: 9.*
testbench: 7.*
name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }}
steps:
- uses: actions/checkout@v2
- name: Cache dependencies
uses: actions/cache@v2
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: mbstring, pdo, pdo_sqlite
coverage: none
- name: Install dependencies
run: |
composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" "filament/support" --no-interaction --no-update
composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction
- name: Run PHPStan
run: ./vendor/bin/phpstan --error-format=github
-21
View File
@@ -1,21 +0,0 @@
name: pint
on: push
jobs:
pint:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
with:
ref: ${{ github.head_ref }}
- name: Install dependencies
run: composer install --no-interaction
- name: Run Pint
run: ./vendor/bin/pint
- name: Commit changes
uses: stefanzweifel/git-auto-commit-action@v4
with:
commit_message: >
chore: styling
+35 -35
View File
@@ -1,40 +1,40 @@
name: tests
on:
push:
pull_request:
push:
pull_request:
jobs:
run-tests:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php: [ 8.1, '8.0' ]
laravel: [ 9.*, 8.* ]
dependency-version: [ prefer-stable ]
include:
- laravel: 9.*
testbench: 7.*
- laravel: 8.*
testbench: 6.*
name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }}
steps:
- uses: actions/checkout@v2
- name: Cache dependencies
uses: actions/cache@v2
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: mbstring, pdo, pdo_sqlite
coverage: none
- name: Install dependencies
run: |
composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" "filament/support" --no-interaction --no-update
composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction
- name: Execute tests
run: ./vendor/bin/pest --verbose
run-tests:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php: [8.1, '8.0']
laravel: [9.*, 8.*]
dependency-version: [prefer-stable]
include:
- laravel: 9.*
testbench: 7.*
- laravel: 8.*
testbench: 6.*
name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }}
steps:
- uses: actions/checkout@v2
- name: Cache dependencies
uses: actions/cache@v2
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: mbstring, pdo, pdo_sqlite
coverage: none
- name: Install dependencies
run: |
composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" "filament/support" --no-interaction --no-update
composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction
- name: Execute tests
run: ./vendor/bin/pest --verbose
+1 -2
View File
@@ -1,6 +1,5 @@
{
"trailingComma": "es5",
"tabWidth": 4,
"trailingComma": "all",
"semi": false,
"singleQuote": true
}
+1 -5
View File
@@ -76,11 +76,7 @@ Install the packages in your app's `composer.json`:
{
...
"require": {
"filament/filament": "dev-fix/error-message as 2.x-dev",
"filament/forms": "dev-fix/error-message as 2.x-dev",
"filament/notifications": "dev-fix/error-message as 2.x-dev",
"filament/support": "dev-fix/error-message as 2.x-dev",
"filament/tables": "dev-fix/error-message as 2.x-dev"
"filament/filament": "*",
},
"repositories": [
{
+1
View File
@@ -27,6 +27,7 @@
"spatie/laravel-package-tools": "^1.9",
"spatie/laravel-ray": "^1.29",
"spatie/laravel-tags": "^4.2",
"spatie/invade": "^1.0",
"symplify/monorepo-builder": "^10.0",
"tgalopin/html-sanitizer": "^1.5"
},
Generated
+369 -399
View File
File diff suppressed because it is too large Load Diff
+174 -152
View File
@@ -38,6 +38,7 @@
"marked": "^4.0.10",
"mdhl": "^0.0.6",
"postcss": "^8.4.4",
"prettier": "^2.7.1",
"sortablejs": "^1.15.0",
"tailwindcss": "^3.0.0",
"tippy.js": "^6.3.7",
@@ -98,30 +99,30 @@
}
},
"node_modules/@babel/compat-data": {
"version": "7.18.8",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.18.8.tgz",
"integrity": "sha512-HSmX4WZPPK3FUxYp7g2T6EyO8j96HlZJlxmKPSh6KAcqwyDrfx7hKjXpAW/0FhFfTJsR0Yt4lAjLI2coMptIHQ==",
"version": "7.18.13",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.18.13.tgz",
"integrity": "sha512-5yUzC5LqyTFp2HLmDoxGQelcdYgSpP9xsnMWBphAscOdFrHSAVbLNzWiy32sVNDqJRDiJK6klfDnAgu6PAGSHw==",
"dev": true,
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/core": {
"version": "7.18.10",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.18.10.tgz",
"integrity": "sha512-JQM6k6ENcBFKVtWvLavlvi/mPcpYZ3+R+2EySDEMSMbp7Mn4FexlbbJVrx2R7Ijhr01T8gyqrOaABWIOgxeUyw==",
"version": "7.18.13",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.18.13.tgz",
"integrity": "sha512-ZisbOvRRusFktksHSG6pjj1CSvkPkcZq/KHD45LAkVP/oiHJkNBZWfpvlLmX8OtHDG8IuzsFlVRWo08w7Qxn0A==",
"dev": true,
"dependencies": {
"@ampproject/remapping": "^2.1.0",
"@babel/code-frame": "^7.18.6",
"@babel/generator": "^7.18.10",
"@babel/generator": "^7.18.13",
"@babel/helper-compilation-targets": "^7.18.9",
"@babel/helper-module-transforms": "^7.18.9",
"@babel/helpers": "^7.18.9",
"@babel/parser": "^7.18.10",
"@babel/parser": "^7.18.13",
"@babel/template": "^7.18.10",
"@babel/traverse": "^7.18.10",
"@babel/types": "^7.18.10",
"@babel/traverse": "^7.18.13",
"@babel/types": "^7.18.13",
"convert-source-map": "^1.7.0",
"debug": "^4.1.0",
"gensync": "^1.0.0-beta.2",
@@ -146,12 +147,12 @@
}
},
"node_modules/@babel/generator": {
"version": "7.18.12",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.18.12.tgz",
"integrity": "sha512-dfQ8ebCN98SvyL7IxNMCUtZQSq5R7kxgN+r8qYTGDmmSion1hX2C0zq2yo1bsCDhXixokv1SAWTZUMYbO/V5zg==",
"version": "7.18.13",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.18.13.tgz",
"integrity": "sha512-CkPg8ySSPuHTYPJYo7IRALdqyjM9HCbt/3uOBEFbzyGVP6Mn8bwFPB0jX6982JVNBlYzM1nnPkfjuXSOPtQeEQ==",
"dev": true,
"dependencies": {
"@babel/types": "^7.18.10",
"@babel/types": "^7.18.13",
"@jridgewell/gen-mapping": "^0.3.2",
"jsesc": "^2.5.1"
},
@@ -226,9 +227,9 @@
}
},
"node_modules/@babel/helper-create-class-features-plugin": {
"version": "7.18.9",
"resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.18.9.tgz",
"integrity": "sha512-WvypNAYaVh23QcjpMR24CwZY2Nz6hqdOcFdPbNpV56hL5H6KiFheO7Xm1aPdlLQ7d5emYZX7VZwPp9x3z+2opw==",
"version": "7.18.13",
"resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.18.13.tgz",
"integrity": "sha512-hDvXp+QYxSRL+23mpAlSGxHMDyIGChm0/AwTfTAAK5Ufe40nCsyNdaYCGuK91phn/fVu9kqayImRDkvNAgdrsA==",
"dev": true,
"dependencies": {
"@babel/helper-annotate-as-pure": "^7.18.6",
@@ -601,9 +602,9 @@
}
},
"node_modules/@babel/parser": {
"version": "7.18.11",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.11.tgz",
"integrity": "sha512-9JKn5vN+hDt0Hdqn1PiJ2guflwP+B6Ga8qbDuoF0PzzVhrzsKIJo8yGqVk6CmMHiMei9w1C1Bp9IMJSIK+HPIQ==",
"version": "7.18.13",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.13.tgz",
"integrity": "sha512-dgXcIfMuQ0kgzLB2b9tRZs7TTFFaGM2AbtA4fJgUUYukzGH4jwsS7hzQHEGs67jdehpm22vkgKwvbU+aEflgwg==",
"dev": true,
"bin": {
"parser": "bin/babel-parser.js"
@@ -1185,9 +1186,9 @@
}
},
"node_modules/@babel/plugin-transform-destructuring": {
"version": "7.18.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.18.9.tgz",
"integrity": "sha512-p5VCYNddPLkZTq4XymQIaIfZNJwT9YsjkPOhkVEqt6QIpQFZVM9IltqqYpOEkJoN1DPznmxUDyZ5CTZs/ZCuHA==",
"version": "7.18.13",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.18.13.tgz",
"integrity": "sha512-TodpQ29XekIsex2A+YJPj5ax2plkGa8YYY6mFjCohk/IG9IY42Rtuj1FuDeemfg2ipxIFLzPeA83SIBnlhSIow==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.18.9"
@@ -1763,19 +1764,19 @@
}
},
"node_modules/@babel/traverse": {
"version": "7.18.11",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.18.11.tgz",
"integrity": "sha512-TG9PiM2R/cWCAy6BPJKeHzNbu4lPzOSZpeMfeNErskGpTJx6trEvFaVCbDvpcxwy49BKWmEPwiW8mrysNiDvIQ==",
"version": "7.18.13",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.18.13.tgz",
"integrity": "sha512-N6kt9X1jRMLPxxxPYWi7tgvJRH/rtoU+dbKAPDM44RFHiMH8igdsaSBgFeskhSl/kLWLDUvIh1RXCrTmg0/zvA==",
"dev": true,
"dependencies": {
"@babel/code-frame": "^7.18.6",
"@babel/generator": "^7.18.10",
"@babel/generator": "^7.18.13",
"@babel/helper-environment-visitor": "^7.18.9",
"@babel/helper-function-name": "^7.18.9",
"@babel/helper-hoist-variables": "^7.18.6",
"@babel/helper-split-export-declaration": "^7.18.6",
"@babel/parser": "^7.18.11",
"@babel/types": "^7.18.10",
"@babel/parser": "^7.18.13",
"@babel/types": "^7.18.13",
"debug": "^4.1.0",
"globals": "^11.1.0"
},
@@ -1784,9 +1785,9 @@
}
},
"node_modules/@babel/types": {
"version": "7.18.10",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.18.10.tgz",
"integrity": "sha512-MJvnbEiiNkpjo+LknnmRrqbY1GPUUggjv+wQVjetM/AONoupqRALB7I6jGqNUAZsKcRIEu2J6FRFvsczljjsaQ==",
"version": "7.18.13",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.18.13.tgz",
"integrity": "sha512-ePqfTihzW0W6XAU+aMw2ykilisStJfDnsejDCXRchCcMJ4O0+8DhPXf2YUbZ6wjBlsEmZwLK/sPweWtu8hcJYQ==",
"dev": true,
"dependencies": {
"@babel/helper-string-parser": "^7.18.10",
@@ -1896,9 +1897,9 @@
"dev": true
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.14",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz",
"integrity": "sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==",
"version": "0.3.15",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.15.tgz",
"integrity": "sha512-oWZNOULl+UbhsgB51uuZzglikfIKSUBO/M9W2OfEjn7cmqoAiCgmv9lyACTUacZwBz0ITnJ2NqjU8Tx0DHL88g==",
"dev": true,
"dependencies": {
"@jridgewell/resolve-uri": "^3.0.3",
@@ -1947,9 +1948,9 @@
}
},
"node_modules/@popperjs/core": {
"version": "2.11.5",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.5.tgz",
"integrity": "sha512-9X2obfABZuDVLCgPK9aX0a/x4jaOEweTTWE2+9sr0Qqqevj2Uv5XorvusThmc9XGYpS9yI+fhh8RTafBtGposw==",
"version": "2.11.6",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz",
"integrity": "sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==",
"dev": true,
"funding": {
"type": "opencollective",
@@ -2087,9 +2088,9 @@
}
},
"node_modules/@types/eslint": {
"version": "8.4.5",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.5.tgz",
"integrity": "sha512-dhsC09y1gpJWnK+Ff4SGvCuSnk9DaU0BJZSzOwa6GVSg65XtTugLBITDAAzRU5duGBoXBHpdR/9jHGxJjNflJQ==",
"version": "8.4.6",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.6.tgz",
"integrity": "sha512-/fqTbjxyFUaYNO7VcW5g+4npmqVACz1bB7RTHYuLj+PRjw9hrCwrUXVQFpChUS0JsyEFvMZ7U/PfmvWgxJhI9g==",
"dev": true,
"dependencies": {
"@types/estree": "*",
@@ -2219,9 +2220,9 @@
"dev": true
},
"node_modules/@types/node": {
"version": "18.6.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.6.5.tgz",
"integrity": "sha512-Xjt5ZGUa5WusGZJ4WJPbOT8QOqp6nDynVFRKcUt32bOgvXEoc6o085WNkYTMO7ifAj2isEfQQ2cseE+wT6jsRw==",
"version": "18.7.10",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.10.tgz",
"integrity": "sha512-SST7B//wF7xlGoLo1IEVB0cQ4e7BgXlDk5IaPgb5s0DlYLjb4if07h8+0zbQIvahfPNXs6e7zyT0EH1MqaS+5g==",
"dev": true
},
"node_modules/@types/parse-json": {
@@ -3217,9 +3218,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001375",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001375.tgz",
"integrity": "sha512-kWIMkNzLYxSvnjy0hL8w1NOaWNr2rn39RTAVyIwcw8juu60bZDWiF1/loOYANzjtJmy6qPgNmn38ro5Pygagdw==",
"version": "1.0.30001381",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001381.tgz",
"integrity": "sha512-fEnkDOKpvp6qc+olg7+NzE1SqyfiyKf4uci7fAU38M3zxs0YOyKOxW/nMZ2l9sJbt7KZHcDIxUnbI0Iime7V4w==",
"dev": true,
"funding": [
{
@@ -3397,9 +3398,9 @@
"dev": true
},
"node_modules/colord": {
"version": "2.9.2",
"resolved": "https://registry.npmjs.org/colord/-/colord-2.9.2.tgz",
"integrity": "sha512-Uqbg+J445nc1TKn4FoDPS6ZZqAvEDnwrH42yo8B40JSOgSLxMZ/gt3h4nmCtPLQeXhjJJkqBx7SCY35WnIixaQ==",
"version": "2.9.3",
"resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz",
"integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==",
"dev": true
},
"node_modules/colorette": {
@@ -3847,9 +3848,9 @@
}
},
"node_modules/cssnano": {
"version": "5.1.12",
"resolved": "https://registry.npmjs.org/cssnano/-/cssnano-5.1.12.tgz",
"integrity": "sha512-TgvArbEZu0lk/dvg2ja+B7kYoD7BBCmn3+k58xD0qjrGHsFzXY/wKTo9M5egcUCabPol05e/PVoIu79s2JN4WQ==",
"version": "5.1.13",
"resolved": "https://registry.npmjs.org/cssnano/-/cssnano-5.1.13.tgz",
"integrity": "sha512-S2SL2ekdEz6w6a2epXn4CmMKU4K3KpcyXLKfAYc9UQQqJRkD/2eLUG0vJ3Db/9OvO5GuAdgXw3pFbR6abqghDQ==",
"dev": true,
"dependencies": {
"cssnano-preset-default": "^5.2.12",
@@ -3935,9 +3936,9 @@
}
},
"node_modules/dayjs": {
"version": "1.11.4",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.4.tgz",
"integrity": "sha512-Zj/lPM5hOvQ1Bf7uAvewDaUcsJoI6JmNqmHhHl3nyumwe0XHwt8sWdOVAPACJzCebL8gQCi+K49w7iKWnGwX9g==",
"version": "1.11.5",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.5.tgz",
"integrity": "sha512-CAdX5Q3YW3Gclyo5Vpqkgpj8fSdLQcRuzfX6mC6Phy0nfJ0eGYOeS7m4mt2plDWLAtA4TqTakvbboHvUxfe4iA==",
"dev": true
},
"node_modules/debug": {
@@ -4253,9 +4254,9 @@
"dev": true
},
"node_modules/electron-to-chromium": {
"version": "1.4.213",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.213.tgz",
"integrity": "sha512-+3DbGHGOCHTVB/Ms63bGqbyC1b8y7Fk86+7ltssB8NQrZtSCvZG6eooSl9U2Q0yw++fL2DpHKOdTU0NVEkFObg==",
"version": "1.4.225",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.225.tgz",
"integrity": "sha512-ICHvGaCIQR3P88uK8aRtx8gmejbVJyC6bB4LEC3anzBrIzdzC7aiZHY4iFfXhN4st6I7lMO0x4sgBHf/7kBvRw==",
"dev": true
},
"node_modules/elliptic": {
@@ -6096,9 +6097,9 @@
}
},
"node_modules/marked": {
"version": "4.0.18",
"resolved": "https://registry.npmjs.org/marked/-/marked-4.0.18.tgz",
"integrity": "sha512-wbLDJ7Zh0sqA0Vdg6aqlbT+yPxqLblpAZh1mK2+AO2twQkPywvvqQNfEPVwSSRjZ7dZcdeVBIAgiO7MMp3Dszw==",
"version": "4.0.19",
"resolved": "https://registry.npmjs.org/marked/-/marked-4.0.19.tgz",
"integrity": "sha512-rgQF/OxOiLcvgUAj1Q1tAf4Bgxn5h5JZTp04Fx4XUkVhs7B+7YA9JEWJhJpoO8eJt8MkZMwqLCNeNqj1bCREZQ==",
"dev": true,
"bin": {
"marked": "bin/marked.js"
@@ -6554,9 +6555,9 @@
}
},
"node_modules/object.assign": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.3.tgz",
"integrity": "sha512-ZFJnX3zltyjcYJL0RoCJuzb+11zWGyaDbjgxZbdV7rFEcHQuYxrZqhow67aA7xpes6LhojyFDaBKAFfogQrikA==",
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz",
"integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==",
"dev": true,
"dependencies": {
"call-bind": "^1.0.2",
@@ -7506,6 +7507,21 @@
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"dev": true
},
"node_modules/prettier": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz",
"integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==",
"dev": true,
"bin": {
"prettier": "bin-prettier.js"
},
"engines": {
"node": ">=10.13.0"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/pretty-time": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/pretty-time/-/pretty-time-1.1.0.tgz",
@@ -8410,9 +8426,9 @@
}
},
"node_modules/std-env": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.1.1.tgz",
"integrity": "sha512-/c645XdExBypL01TpFKiG/3RAa/Qmu+zRi0MwAmrdEkwHNuN0ebo8ccAXBBDa5Z0QOJgBskUIbuCK91x0sCVEw==",
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.2.1.tgz",
"integrity": "sha512-D/uYFWkI/31OrnKmXZqGAGK5GbQRPp/BWA1nuITcc6ICblhhuQUPHS5E2GSCVS7Hwhf4ciq8qsATwBUxv+lI6w==",
"dev": true
},
"node_modules/stream-browserify": {
@@ -8687,16 +8703,16 @@
}
},
"node_modules/terser-webpack-plugin": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.3.tgz",
"integrity": "sha512-Fx60G5HNYknNTNQnzQ1VePRuu89ZVYWfjRAeT5rITuCY/1b08s49e5kSQwHDirKZWuoKOBRFS98EUUoZ9kLEwQ==",
"version": "5.3.5",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.5.tgz",
"integrity": "sha512-AOEDLDxD2zylUGf/wxHxklEkOe2/r+seuyOWujejFrIxHf11brA1/dWQNIgXa1c6/Wkxgu7zvv0JhOWfc2ELEA==",
"dev": true,
"dependencies": {
"@jridgewell/trace-mapping": "^0.3.7",
"@jridgewell/trace-mapping": "^0.3.14",
"jest-worker": "^27.4.5",
"schema-utils": "^3.1.1",
"serialize-javascript": "^6.0.0",
"terser": "^5.7.2"
"terser": "^5.14.1"
},
"engines": {
"node": ">= 10.13.0"
@@ -9257,9 +9273,9 @@
}
},
"node_modules/webpack-dev-server": {
"version": "4.9.3",
"resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.9.3.tgz",
"integrity": "sha512-3qp/eoboZG5/6QgiZ3llN8TUzkSpYg1Ko9khWX1h40MIEUNS2mDoIa8aXsPfskER+GbTvs/IJZ1QTBBhhuetSw==",
"version": "4.10.0",
"resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.10.0.tgz",
"integrity": "sha512-7dezwAs+k6yXVFZ+MaL8VnE+APobiO3zvpp3rBHe/HmWQ+avwh0Q3d0xxacOiBybZZ3syTZw9HXzpa3YNbAZDQ==",
"dev": true,
"dependencies": {
"@types/bonjour": "^3.5.9",
@@ -9668,27 +9684,27 @@
}
},
"@babel/compat-data": {
"version": "7.18.8",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.18.8.tgz",
"integrity": "sha512-HSmX4WZPPK3FUxYp7g2T6EyO8j96HlZJlxmKPSh6KAcqwyDrfx7hKjXpAW/0FhFfTJsR0Yt4lAjLI2coMptIHQ==",
"version": "7.18.13",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.18.13.tgz",
"integrity": "sha512-5yUzC5LqyTFp2HLmDoxGQelcdYgSpP9xsnMWBphAscOdFrHSAVbLNzWiy32sVNDqJRDiJK6klfDnAgu6PAGSHw==",
"dev": true
},
"@babel/core": {
"version": "7.18.10",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.18.10.tgz",
"integrity": "sha512-JQM6k6ENcBFKVtWvLavlvi/mPcpYZ3+R+2EySDEMSMbp7Mn4FexlbbJVrx2R7Ijhr01T8gyqrOaABWIOgxeUyw==",
"version": "7.18.13",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.18.13.tgz",
"integrity": "sha512-ZisbOvRRusFktksHSG6pjj1CSvkPkcZq/KHD45LAkVP/oiHJkNBZWfpvlLmX8OtHDG8IuzsFlVRWo08w7Qxn0A==",
"dev": true,
"requires": {
"@ampproject/remapping": "^2.1.0",
"@babel/code-frame": "^7.18.6",
"@babel/generator": "^7.18.10",
"@babel/generator": "^7.18.13",
"@babel/helper-compilation-targets": "^7.18.9",
"@babel/helper-module-transforms": "^7.18.9",
"@babel/helpers": "^7.18.9",
"@babel/parser": "^7.18.10",
"@babel/parser": "^7.18.13",
"@babel/template": "^7.18.10",
"@babel/traverse": "^7.18.10",
"@babel/types": "^7.18.10",
"@babel/traverse": "^7.18.13",
"@babel/types": "^7.18.13",
"convert-source-map": "^1.7.0",
"debug": "^4.1.0",
"gensync": "^1.0.0-beta.2",
@@ -9705,12 +9721,12 @@
}
},
"@babel/generator": {
"version": "7.18.12",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.18.12.tgz",
"integrity": "sha512-dfQ8ebCN98SvyL7IxNMCUtZQSq5R7kxgN+r8qYTGDmmSion1hX2C0zq2yo1bsCDhXixokv1SAWTZUMYbO/V5zg==",
"version": "7.18.13",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.18.13.tgz",
"integrity": "sha512-CkPg8ySSPuHTYPJYo7IRALdqyjM9HCbt/3uOBEFbzyGVP6Mn8bwFPB0jX6982JVNBlYzM1nnPkfjuXSOPtQeEQ==",
"dev": true,
"requires": {
"@babel/types": "^7.18.10",
"@babel/types": "^7.18.13",
"@jridgewell/gen-mapping": "^0.3.2",
"jsesc": "^2.5.1"
},
@@ -9768,9 +9784,9 @@
}
},
"@babel/helper-create-class-features-plugin": {
"version": "7.18.9",
"resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.18.9.tgz",
"integrity": "sha512-WvypNAYaVh23QcjpMR24CwZY2Nz6hqdOcFdPbNpV56hL5H6KiFheO7Xm1aPdlLQ7d5emYZX7VZwPp9x3z+2opw==",
"version": "7.18.13",
"resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.18.13.tgz",
"integrity": "sha512-hDvXp+QYxSRL+23mpAlSGxHMDyIGChm0/AwTfTAAK5Ufe40nCsyNdaYCGuK91phn/fVu9kqayImRDkvNAgdrsA==",
"dev": true,
"requires": {
"@babel/helper-annotate-as-pure": "^7.18.6",
@@ -10054,9 +10070,9 @@
}
},
"@babel/parser": {
"version": "7.18.11",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.11.tgz",
"integrity": "sha512-9JKn5vN+hDt0Hdqn1PiJ2guflwP+B6Ga8qbDuoF0PzzVhrzsKIJo8yGqVk6CmMHiMei9w1C1Bp9IMJSIK+HPIQ==",
"version": "7.18.13",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.13.tgz",
"integrity": "sha512-dgXcIfMuQ0kgzLB2b9tRZs7TTFFaGM2AbtA4fJgUUYukzGH4jwsS7hzQHEGs67jdehpm22vkgKwvbU+aEflgwg==",
"dev": true
},
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": {
@@ -10437,9 +10453,9 @@
}
},
"@babel/plugin-transform-destructuring": {
"version": "7.18.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.18.9.tgz",
"integrity": "sha512-p5VCYNddPLkZTq4XymQIaIfZNJwT9YsjkPOhkVEqt6QIpQFZVM9IltqqYpOEkJoN1DPznmxUDyZ5CTZs/ZCuHA==",
"version": "7.18.13",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.18.13.tgz",
"integrity": "sha512-TodpQ29XekIsex2A+YJPj5ax2plkGa8YYY6mFjCohk/IG9IY42Rtuj1FuDeemfg2ipxIFLzPeA83SIBnlhSIow==",
"dev": true,
"requires": {
"@babel/helper-plugin-utils": "^7.18.9"
@@ -10836,27 +10852,27 @@
}
},
"@babel/traverse": {
"version": "7.18.11",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.18.11.tgz",
"integrity": "sha512-TG9PiM2R/cWCAy6BPJKeHzNbu4lPzOSZpeMfeNErskGpTJx6trEvFaVCbDvpcxwy49BKWmEPwiW8mrysNiDvIQ==",
"version": "7.18.13",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.18.13.tgz",
"integrity": "sha512-N6kt9X1jRMLPxxxPYWi7tgvJRH/rtoU+dbKAPDM44RFHiMH8igdsaSBgFeskhSl/kLWLDUvIh1RXCrTmg0/zvA==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.18.6",
"@babel/generator": "^7.18.10",
"@babel/generator": "^7.18.13",
"@babel/helper-environment-visitor": "^7.18.9",
"@babel/helper-function-name": "^7.18.9",
"@babel/helper-hoist-variables": "^7.18.6",
"@babel/helper-split-export-declaration": "^7.18.6",
"@babel/parser": "^7.18.11",
"@babel/types": "^7.18.10",
"@babel/parser": "^7.18.13",
"@babel/types": "^7.18.13",
"debug": "^4.1.0",
"globals": "^11.1.0"
}
},
"@babel/types": {
"version": "7.18.10",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.18.10.tgz",
"integrity": "sha512-MJvnbEiiNkpjo+LknnmRrqbY1GPUUggjv+wQVjetM/AONoupqRALB7I6jGqNUAZsKcRIEu2J6FRFvsczljjsaQ==",
"version": "7.18.13",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.18.13.tgz",
"integrity": "sha512-ePqfTihzW0W6XAU+aMw2ykilisStJfDnsejDCXRchCcMJ4O0+8DhPXf2YUbZ6wjBlsEmZwLK/sPweWtu8hcJYQ==",
"dev": true,
"requires": {
"@babel/helper-string-parser": "^7.18.10",
@@ -10947,9 +10963,9 @@
"dev": true
},
"@jridgewell/trace-mapping": {
"version": "0.3.14",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz",
"integrity": "sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==",
"version": "0.3.15",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.15.tgz",
"integrity": "sha512-oWZNOULl+UbhsgB51uuZzglikfIKSUBO/M9W2OfEjn7cmqoAiCgmv9lyACTUacZwBz0ITnJ2NqjU8Tx0DHL88g==",
"dev": true,
"requires": {
"@jridgewell/resolve-uri": "^3.0.3",
@@ -10989,9 +11005,9 @@
}
},
"@popperjs/core": {
"version": "2.11.5",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.5.tgz",
"integrity": "sha512-9X2obfABZuDVLCgPK9aX0a/x4jaOEweTTWE2+9sr0Qqqevj2Uv5XorvusThmc9XGYpS9yI+fhh8RTafBtGposw==",
"version": "2.11.6",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz",
"integrity": "sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==",
"dev": true
},
"@ryangjchandler/alpine-tooltip": {
@@ -11116,9 +11132,9 @@
}
},
"@types/eslint": {
"version": "8.4.5",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.5.tgz",
"integrity": "sha512-dhsC09y1gpJWnK+Ff4SGvCuSnk9DaU0BJZSzOwa6GVSg65XtTugLBITDAAzRU5duGBoXBHpdR/9jHGxJjNflJQ==",
"version": "8.4.6",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.6.tgz",
"integrity": "sha512-/fqTbjxyFUaYNO7VcW5g+4npmqVACz1bB7RTHYuLj+PRjw9hrCwrUXVQFpChUS0JsyEFvMZ7U/PfmvWgxJhI9g==",
"dev": true,
"requires": {
"@types/estree": "*",
@@ -11248,9 +11264,9 @@
"dev": true
},
"@types/node": {
"version": "18.6.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.6.5.tgz",
"integrity": "sha512-Xjt5ZGUa5WusGZJ4WJPbOT8QOqp6nDynVFRKcUt32bOgvXEoc6o085WNkYTMO7ifAj2isEfQQ2cseE+wT6jsRw==",
"version": "18.7.10",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.10.tgz",
"integrity": "sha512-SST7B//wF7xlGoLo1IEVB0cQ4e7BgXlDk5IaPgb5s0DlYLjb4if07h8+0zbQIvahfPNXs6e7zyT0EH1MqaS+5g==",
"dev": true
},
"@types/parse-json": {
@@ -12081,9 +12097,9 @@
}
},
"caniuse-lite": {
"version": "1.0.30001375",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001375.tgz",
"integrity": "sha512-kWIMkNzLYxSvnjy0hL8w1NOaWNr2rn39RTAVyIwcw8juu60bZDWiF1/loOYANzjtJmy6qPgNmn38ro5Pygagdw==",
"version": "1.0.30001381",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001381.tgz",
"integrity": "sha512-fEnkDOKpvp6qc+olg7+NzE1SqyfiyKf4uci7fAU38M3zxs0YOyKOxW/nMZ2l9sJbt7KZHcDIxUnbI0Iime7V4w==",
"dev": true
},
"chalk": {
@@ -12214,9 +12230,9 @@
"dev": true
},
"colord": {
"version": "2.9.2",
"resolved": "https://registry.npmjs.org/colord/-/colord-2.9.2.tgz",
"integrity": "sha512-Uqbg+J445nc1TKn4FoDPS6ZZqAvEDnwrH42yo8B40JSOgSLxMZ/gt3h4nmCtPLQeXhjJJkqBx7SCY35WnIixaQ==",
"version": "2.9.3",
"resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz",
"integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==",
"dev": true
},
"colorette": {
@@ -12572,9 +12588,9 @@
"dev": true
},
"cssnano": {
"version": "5.1.12",
"resolved": "https://registry.npmjs.org/cssnano/-/cssnano-5.1.12.tgz",
"integrity": "sha512-TgvArbEZu0lk/dvg2ja+B7kYoD7BBCmn3+k58xD0qjrGHsFzXY/wKTo9M5egcUCabPol05e/PVoIu79s2JN4WQ==",
"version": "5.1.13",
"resolved": "https://registry.npmjs.org/cssnano/-/cssnano-5.1.13.tgz",
"integrity": "sha512-S2SL2ekdEz6w6a2epXn4CmMKU4K3KpcyXLKfAYc9UQQqJRkD/2eLUG0vJ3Db/9OvO5GuAdgXw3pFbR6abqghDQ==",
"dev": true,
"requires": {
"cssnano-preset-default": "^5.2.12",
@@ -12636,9 +12652,9 @@
}
},
"dayjs": {
"version": "1.11.4",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.4.tgz",
"integrity": "sha512-Zj/lPM5hOvQ1Bf7uAvewDaUcsJoI6JmNqmHhHl3nyumwe0XHwt8sWdOVAPACJzCebL8gQCi+K49w7iKWnGwX9g==",
"version": "1.11.5",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.5.tgz",
"integrity": "sha512-CAdX5Q3YW3Gclyo5Vpqkgpj8fSdLQcRuzfX6mC6Phy0nfJ0eGYOeS7m4mt2plDWLAtA4TqTakvbboHvUxfe4iA==",
"dev": true
},
"debug": {
@@ -12881,9 +12897,9 @@
"dev": true
},
"electron-to-chromium": {
"version": "1.4.213",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.213.tgz",
"integrity": "sha512-+3DbGHGOCHTVB/Ms63bGqbyC1b8y7Fk86+7ltssB8NQrZtSCvZG6eooSl9U2Q0yw++fL2DpHKOdTU0NVEkFObg==",
"version": "1.4.225",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.225.tgz",
"integrity": "sha512-ICHvGaCIQR3P88uK8aRtx8gmejbVJyC6bB4LEC3anzBrIzdzC7aiZHY4iFfXhN4st6I7lMO0x4sgBHf/7kBvRw==",
"dev": true
},
"elliptic": {
@@ -14268,9 +14284,9 @@
}
},
"marked": {
"version": "4.0.18",
"resolved": "https://registry.npmjs.org/marked/-/marked-4.0.18.tgz",
"integrity": "sha512-wbLDJ7Zh0sqA0Vdg6aqlbT+yPxqLblpAZh1mK2+AO2twQkPywvvqQNfEPVwSSRjZ7dZcdeVBIAgiO7MMp3Dszw==",
"version": "4.0.19",
"resolved": "https://registry.npmjs.org/marked/-/marked-4.0.19.tgz",
"integrity": "sha512-rgQF/OxOiLcvgUAj1Q1tAf4Bgxn5h5JZTp04Fx4XUkVhs7B+7YA9JEWJhJpoO8eJt8MkZMwqLCNeNqj1bCREZQ==",
"dev": true
},
"md5": {
@@ -14620,9 +14636,9 @@
"dev": true
},
"object.assign": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.3.tgz",
"integrity": "sha512-ZFJnX3zltyjcYJL0RoCJuzb+11zWGyaDbjgxZbdV7rFEcHQuYxrZqhow67aA7xpes6LhojyFDaBKAFfogQrikA==",
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz",
"integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==",
"dev": true,
"requires": {
"call-bind": "^1.0.2",
@@ -15239,6 +15255,12 @@
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"dev": true
},
"prettier": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz",
"integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==",
"dev": true
},
"pretty-time": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/pretty-time/-/pretty-time-1.1.0.tgz",
@@ -15969,9 +15991,9 @@
"dev": true
},
"std-env": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.1.1.tgz",
"integrity": "sha512-/c645XdExBypL01TpFKiG/3RAa/Qmu+zRi0MwAmrdEkwHNuN0ebo8ccAXBBDa5Z0QOJgBskUIbuCK91x0sCVEw==",
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.2.1.tgz",
"integrity": "sha512-D/uYFWkI/31OrnKmXZqGAGK5GbQRPp/BWA1nuITcc6ICblhhuQUPHS5E2GSCVS7Hwhf4ciq8qsATwBUxv+lI6w==",
"dev": true
},
"stream-browserify": {
@@ -16183,16 +16205,16 @@
}
},
"terser-webpack-plugin": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.3.tgz",
"integrity": "sha512-Fx60G5HNYknNTNQnzQ1VePRuu89ZVYWfjRAeT5rITuCY/1b08s49e5kSQwHDirKZWuoKOBRFS98EUUoZ9kLEwQ==",
"version": "5.3.5",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.5.tgz",
"integrity": "sha512-AOEDLDxD2zylUGf/wxHxklEkOe2/r+seuyOWujejFrIxHf11brA1/dWQNIgXa1c6/Wkxgu7zvv0JhOWfc2ELEA==",
"dev": true,
"requires": {
"@jridgewell/trace-mapping": "^0.3.7",
"@jridgewell/trace-mapping": "^0.3.14",
"jest-worker": "^27.4.5",
"schema-utils": "^3.1.1",
"serialize-javascript": "^6.0.0",
"terser": "^5.7.2"
"terser": "^5.14.1"
},
"dependencies": {
"schema-utils": {
@@ -16615,9 +16637,9 @@
}
},
"webpack-dev-server": {
"version": "4.9.3",
"resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.9.3.tgz",
"integrity": "sha512-3qp/eoboZG5/6QgiZ3llN8TUzkSpYg1Ko9khWX1h40MIEUNS2mDoIa8aXsPfskER+GbTvs/IJZ1QTBBhhuetSw==",
"version": "4.10.0",
"resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.10.0.tgz",
"integrity": "sha512-7dezwAs+k6yXVFZ+MaL8VnE+APobiO3zvpp3rBHe/HmWQ+avwh0Q3d0xxacOiBybZZ3syTZw9HXzpa3YNbAZDQ==",
"dev": true,
"requires": {
"@types/bonjour": "^3.5.9",
+3 -1
View File
@@ -2,7 +2,8 @@
"name": "filament",
"scripts": {
"build": "node bin/build && mix --production",
"watch": "node bin/build && mix watch"
"watch": "node bin/build && mix watch",
"prettier": "npx prettier --write '**/*.{css,js,yml}' '!**/dist' '!**/vendor'"
},
"devDependencies": {
"@alpinejs/collapse": "^3.8.1",
@@ -37,6 +38,7 @@
"marked": "^4.0.10",
"mdhl": "^0.0.6",
"postcss": "^8.4.4",
"prettier": "^2.7.1",
"sortablejs": "^1.15.0",
"tailwindcss": "^3.0.0",
"tippy.js": "^6.3.7",
-1
View File
@@ -1 +0,0 @@
/vendor
+24
View File
@@ -11,14 +11,38 @@ namespace Livewire\Testing {
public function assertHasNoFormErrors(array $keys = []): static {}
public function mountPageAction(string $name): static {}
public function setPageActionData(array $data): static {}
public function assertPageActionDataSet(array $data): static {}
public function callPageAction(string $name, array $data = [], array $arguments = []): static {}
public function callMountedPageAction(array $arguments = []): static {}
public function assertPageActionExists(string $name): static {}
public function assertPageActionVisible(string $name): static {}
public function assertPageActionHidden(string $name): static {}
public function assertPageActionEnabled(string $name): static {}
public function assertPageActionDisabled(string $name): static {}
public function assertPageActionHasIcon(string $name, string $icon): static {}
public function assertPageActionDoesNotHaveIcon(string $name, string $icon): static {}
public function assertPageActionHasLabel(string $name, string $label): static {}
public function assertPageActionHasColor(string $name, string $color): static {}
public function assertPageActionDoesNotHaveColor(string $name, string $color): static {}
public function assertPageActionDoesNotHaveLabel(string $name, string $label): static {}
public function assertPageActionHeld(string $name): static {}
public function assertHasPageActionErrors(array $keys = []): static {}
-4783
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+53 -53
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -1,4 +1,4 @@
{
"/app.js": "/app.js?id=26af475756bce8ea9c5a2731dd4abc7a",
"/app.css": "/app.css?id=ebeff99b287f00b7cdc421f9e1d60220"
"/app.js": "/app.js?id=4fab5d479eb9dd235eb10ba839e10b36",
"/app.css": "/app.css?id=0df761cd71b4d35d8c79d66ce261e46d"
}
+8
View File
@@ -63,6 +63,14 @@ If you wish to translate the package, you may publish the language files using:
php artisan vendor:publish --tag=filament-translations
```
Since this package depends on other Filament packages, you may wish to translate those as well:
```bash
php artisan vendor:publish --tag=filament-forms-translations
php artisan vendor:publish --tag=filament-tables-translations
php artisan vendor:publish --tag=filament-support-translations
```
## Upgrading
To upgrade the package to the latest version, you must run:
@@ -58,6 +58,8 @@ When creating your resource, you may now use `--generate`:
php artisan make:filament-resource Customer --generate
```
> Note: If your table contains ENUM columns, `doctrine/dbal` is unable to scan your table and will crash. Hence Filament is unable to generate the schema for your resource if it contains an ENUM column. Read more about this issue [here](https://github.com/doctrine/dbal/issues/3819#issuecomment-573419808).
### Handling soft deletes
By default, you will not be able to interact with deleted records in the admin panel. If you'd like to add functionality to restore, force delete and filter trashed records in your resource, use the `--soft-deletes` flag when generating the resource:
@@ -403,6 +403,21 @@ public static function table(Table $table): Table
}
```
## Polling content
You may poll table content so that it refreshes at a set interval, using the `poll()` method:
```php
use Filament\Resources\Table;
public static function table(Table $table): Table
{
return $table
// ...
->poll('10s');
}
```
## Authorization
For authorization, Filament will observe any [model policies](https://laravel.com/docs/authorization#creating-policies) that are registered in your app.
@@ -64,6 +64,6 @@ Global search results will link to the [Edit page](editing-records) of your reso
```php
public static function getGlobalSearchResultUrl(Model $record): string
{
return route('users.edit', ['user' => $record]);
return UserResource::getUrl('edit', ['record' => $record]);
}
```
+26 -1
View File
@@ -138,6 +138,31 @@ Action::make('updateAuthor')
])
```
#### Filling default data
You may fill the form with default data, using the `mountUsing()` method:
```php
use App\Models\User;
use Filament\Forms;
use Filament\Pages\Actions\Action;
Action::make('updateAuthor')
->mountUsing(fn (Forms\ComponentContainer $form) => $form->fill([
'authorId' => $this->record->author->id,
]))
->action(function (array $data): void {
$this->record->author()->associate($data['authorId']);
$this->record->save();
})
->form([
Forms\Components\Select::make('authorId')
->label('Author')
->options(User::query()->pluck('name', 'id'))
->required(),
])
```
### Setting a modal heading, subheading, and button label
You may customize the heading, subheading and button label of the modal:
@@ -153,7 +178,7 @@ Action::make('delete')
->modalButton('Yes, delete them')
```
## Custom content
### Custom content
You may define custom content to be rendered inside your modal, which you can specify by passing a Blade view into the `modalContent()` method:
+1 -1
View File
@@ -166,7 +166,7 @@ use Filament\Navigation\NavigationGroup;
Filament::navigation(function (NavigationBuilder $builder): NavigationBuilder {
return $builder
->group([
->groups([
NavigationGroup::make('Website')
->items([
...PageResource::getNavigationItems(),
+25
View File
@@ -379,6 +379,31 @@ it('can validate invoice recipient email', function () {
});
```
To check if a page action is pre-filled with data, you can use the `assertPageActionDataSet()` method:
```php
use function Pest\Livewire\livewire;
it('can send invoices to the primary contact by default', function () {
$invoice = Invoice::factory()->create();
$recipientEmail = $invoice->company->primaryContact->email;
livewire(EditInvoice::class, [
'invoice' => $invoice,
])
->mountPageAction('send')
->assertPageActionDataSet([
'email' => $recipientEmail,
])
->callMountedPageAction()
->assertHasNoPageActionErrors();
expect($invoice->refresh())
->isSent()->toBeTrue()
->recipient_email->toBe($recipientEmail);
});
```
To check if a page action is hidden to a user, you can use the `assertPageActionHidden()` method:
```php
+4 -2
View File
@@ -40,7 +40,7 @@ Alpine.store('sidebar', {
toggleCollapsedGroup: function (group) {
this.collapsedGroups = this.collapsedGroups.includes(group)
? this.collapsedGroups.filter(
(collapsedGroup) => collapsedGroup !== group
(collapsedGroup) => collapsedGroup !== group,
)
: this.collapsedGroups.concat(group)
},
@@ -56,7 +56,9 @@ Alpine.store('sidebar', {
Alpine.store(
'theme',
window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light',
)
window.addEventListener('dark-mode-toggled', (event) => {
@@ -15,7 +15,7 @@ return [
],
'save' => [
'label' => 'Save',
'label' => 'Save changes',
],
],
@@ -19,7 +19,7 @@ return [
],
'create_another' => [
'label' => 'ساختن یکی دیگر',
'label' => 'ساختن و ساختن یکی دیگر',
],
],
@@ -2,16 +2,16 @@
return [
'title' => 'Tạo :label',
'title' => 'Tạo :label mới',
'breadcrumb' => 'Tạo',
'breadcrumb' => 'Tạo mới',
'form' => [
'actions' => [
'cancel' => [
'label' => 'Hủy',
'label' => 'Quay lại',
],
'create' => [
@@ -2,20 +2,20 @@
return [
'title' => 'Sửa :label',
'title' => 'Chỉnh sửa :label',
'breadcrumb' => 'Sửa',
'breadcrumb' => 'Chỉnh sửa',
'form' => [
'actions' => [
'cancel' => [
'label' => 'Hủy',
'label' => 'Quay lại',
],
'save' => [
'label' => 'Lưu',
'label' => 'Lưu lại',
],
],
@@ -18,31 +18,22 @@
@if (config('filament.layout.sidebar.is_collapsible_on_desktop'))
<div
x-data="{}"
class="flex-col flex-1 hidden w-screen h-full space-y-6 transition-all filament-main lg:pl-[var(--sidebar-width)] rtl:lg:pl-0 rtl:lg:pr-[var(--sidebar-width)]"
class="flex-col flex-1 hidden w-screen h-full gap-y-6 transition-all filament-main rtl:lg:pl-0"
x-bind:class="{
'lg:pl-[5.4rem] rtl:lg:pr-[5.4rem]': ! $store.sidebar.isOpen
'lg:pl-[5.4rem] rtl:lg:pr-[5.4rem]': ! $store.sidebar.isOpen,
'lg:pl-[var(--sidebar-width)] rtl:lg:pr-[var(--sidebar-width)]': $store.sidebar.isOpen,
}"
x-bind:style="'display: flex'" {{-- Mimics `x-cloak`, as using `x-cloak` causes visual issues with chart widgets --}}
>
@else
<div class="flex flex-col flex-1 w-screen space-y-6 lg:pl-[var(--sidebar-width)] rtl:lg:pl-0 rtl:lg:pr-[var(--sidebar-width)] filament-main">
<div class="flex flex-col flex-1 w-screen gap-y-6 lg:pl-[var(--sidebar-width)] rtl:lg:pl-0 rtl:lg:pr-[var(--sidebar-width)] filament-main">
@endif
<header @class([
'h-[4rem] shrink-0 w-full border-b flex items-center filament-main-topbar',
'dark:bg-gray-800 dark:border-gray-700' => config('filament.dark_mode'),
])>
<div @class([
'flex items-center w-full px-2 mx-auto sm:px-4 md:px-6 lg:px-8',
match ($maxContentWidth ?? config('filament.layout.max_content_width')) {
'xl' => 'max-w-xl',
'2xl' => 'max-w-2xl',
'3xl' => 'max-w-3xl',
'4xl' => 'max-w-4xl',
'5xl' => 'max-w-5xl',
'6xl' => 'max-w-6xl',
'full' => 'max-w-full',
default => 'max-w-7xl',
},
'flex items-center w-full px-2 sm:px-4 md:px-6 lg:px-8',
])>
<button
x-data="{}"
@@ -68,7 +59,8 @@
<div @class([
'flex-1 w-full px-4 mx-auto md:px-6 lg:px-8 filament-main-content',
match ($maxContentWidth ?? config('filament.layout.max_content_width')) {
match ($maxContentWidth ??= config('filament.layout.max_content_width')) {
null, '7xl', '' => 'max-w-7xl',
'xl' => 'max-w-xl',
'2xl' => 'max-w-2xl',
'3xl' => 'max-w-3xl',
@@ -76,7 +68,7 @@
'5xl' => 'max-w-5xl',
'6xl' => 'max-w-6xl',
'full' => 'max-w-full',
default => 'max-w-7xl',
default => $maxContentWidth,
},
])>
{{ \Filament\Facades\Filament::renderHook('content.start') }}
@@ -69,11 +69,11 @@
<div>
@if (config('filament.dark_mode'))
<x-filament::dropdown.item icon="heroicon-s-moon" x-show="theme === 'dark'" x-on:click="mode = 'manual'; theme = 'light'">
<x-filament::dropdown.item icon="heroicon-s-moon" x-show="theme === 'dark'" x-on:click="close(); mode = 'manual'; theme = 'light'">
{{ __('filament::layout.buttons.light_mode.label') }}
</x-filament::dropdown.item>
<x-filament::dropdown.item icon="heroicon-s-sun" x-show="theme === 'light'" x-on:click="mode = 'manual'; theme = 'dark'">
<x-filament::dropdown.item icon="heroicon-s-sun" x-show="theme === 'light'" x-on:click="close(); mode = 'manual'; theme = 'dark'">
{{ __('filament::layout.buttons.dark_mode.label') }}
</x-filament::dropdown.item>
@endif
@@ -35,7 +35,22 @@
$action = $this->getMountedAction();
@endphp
<x-filament::modal id="page-action" :visible="filled($action)" :width="$action?->getModalWidth()" display-classes="block">
<x-filament::modal
id="page-action"
:wire:key="$action ? $this->id . '.actions.' . $action->getName() . '.modal' : null"
x-init="
$watch('isOpen', () => {
if (isOpen) {
return
}
$wire.mountedAction = null
})
"
:visible="filled($action)"
:width="$action?->getModalWidth()"
display-classes="block"
>
@if ($action)
@if ($action->isModalCentered())
<x-slot name="heading">
@@ -52,6 +67,12 @@
<x-filament::modal.heading>
{{ $action->getModalHeading() }}
</x-filament::modal.heading>
@if ($subheading = $action->getModalSubheading())
<x-filament::modal.subheading>
{{ $subheading }}
</x-filament::modal.subheading>
@endif
</x-slot>
@endif
@@ -15,7 +15,10 @@
<x-filament::button
:form="$getForm()"
:type="$canSubmitForm() ? 'submit' : 'button'"
:tag="$action->getUrl() ? 'a' : 'button'"
:wire:click="$wireClickAction"
:href="$action->isEnabled() ? $action->getUrl() : null"
:target="$action->shouldOpenUrlInNewTab() ? '_blank' : null"
:x-on:click="$canCancelAction() ? 'isOpen = false' : null"
:color="$getColor()"
:outlined="$isOutlined()"
@@ -18,6 +18,7 @@ use Filament\Resources\Resource;
use Filament\Tables\Actions\Action as TableAction;
use Filament\Tables\Actions\ButtonAction;
use Filament\Tables\Actions\IconButtonAction;
use Filament\Testing\TestsPageActions;
use Filament\Testing\TestsPages;
use Filament\Widgets\Widget;
use Illuminate\Filesystem\Filesystem;
@@ -79,7 +80,9 @@ class FilamentServiceProvider extends PluginServiceProvider
{
parent::packageRegistered();
$this->registerComponents();
$this->app->booting(function () {
$this->registerComponents();
});
$this->app->scoped('filament', function (): FilamentManager {
return new FilamentManager();
@@ -99,6 +102,7 @@ class FilamentServiceProvider extends PluginServiceProvider
$this->bootTableActionConfiguration();
TestableLivewire::mixin(new TestsPageActions());
TestableLivewire::mixin(new TestsPages());
}
@@ -66,6 +66,8 @@ class CreateAction extends Action
}
$this->success();
$this->record(null);
});
}
@@ -62,8 +62,6 @@ trait HasActions
} finally {
$this->mountedAction = null;
$action->record(null);
$action->resetArguments();
$action->resetFormData();
@@ -51,7 +51,7 @@ class EditRecord extends Page implements HasFormActions
{
$this->callHook('beforeFill');
$data = $this->getRecord()->toArray();
$data = $this->getRecord()->attributesToArray();
$data = $this->mutateFormDataBeforeFill($data);
@@ -384,17 +384,61 @@ class ListRecords extends Page implements Tables\Contracts\HasTable
return filled($this->getTableReorderColumn()) && static::getResource()::canReorder();
}
protected function getTablePollingInterval(): ?string
{
return $this->getResourceTable()->getPollingInterval();
}
protected function getTableRecordUrlUsing(): ?Closure
{
return function (Model $record): ?string {
$resource = static::getResource();
foreach (['view', 'edit'] as $action) {
$action = $this->getCachedTableAction($action);
if ($resource::hasPage('view') && $resource::canView($record)) {
return $resource::getUrl('view', ['record' => $record]);
if (! $action) {
continue;
}
$action->record($record);
if ($action->isHidden()) {
continue;
}
$url = $action->getUrl();
if (! $url) {
continue;
}
return $url;
}
if ($resource::hasPage('edit') && $resource::canEdit($record)) {
return $resource::getUrl('edit', ['record' => $record]);
return null;
};
}
protected function getTableRecordActionUsing(): ?Closure
{
return function (Model $record): ?string {
foreach (['view', 'edit'] as $action) {
$action = $this->getCachedTableAction($action);
if (! $action) {
continue;
}
$action->record($record);
if ($action->isHidden()) {
continue;
}
if ($action->getUrl()) {
continue;
}
return $action->getName();
}
return null;
@@ -37,7 +37,7 @@ trait CanEditRecords
$this->callHook('beforeFill');
$this->callHook('beforeEditFill');
$data = $this->getMountedTableActionRecord()->toArray();
$data = $this->getMountedTableActionRecord()->attributesToArray();
$data = $this->mutateFormDataBeforeFill($data);
@@ -31,7 +31,7 @@ trait CanViewRecords
$this->callHook('beforeFill');
$this->callHook('beforeViewFill');
$data = $this->getMountedTableActionRecord()->toArray();
$data = $this->getMountedTableActionRecord()->attributesToArray();
$this->getMountedTableActionForm()->fill($data);
@@ -49,7 +49,7 @@ class ViewRecord extends Page
{
$this->callHook('beforeFill');
$data = $this->getRecord()->toArray();
$data = $this->getRecord()->attributesToArray();
$data = $this->mutateFormDataBeforeFill($data);
@@ -16,7 +16,7 @@ trait CanEditRecords
$this->callHook('beforeFill');
$this->callHook('beforeEditFill');
$data = $this->getMountedTableActionRecord()->toArray();
$data = $this->getMountedTableActionRecord()->attributesToArray();
$data = $this->mutateFormDataBeforeFill($data);
@@ -33,7 +33,7 @@ trait CanViewRecords
$this->callHook('beforeFill');
$this->callHook('beforeViewFill');
$data = $this->getMountedTableActionRecord()->toArray();
$data = $this->getMountedTableActionRecord()->attributesToArray();
$this->getMountedTableActionForm()->fill($data);
@@ -2,6 +2,7 @@
namespace Filament\Resources\RelationManagers;
use Closure;
use Filament\Facades\Filament;
use Filament\Http\Livewire\Concerns\CanNotify;
use function Filament\locale_has_pluralization;
@@ -418,6 +419,11 @@ class RelationManager extends Component implements Tables\Contracts\HasRelations
return filled($this->getTableReorderColumn()) && $this->canReorder();
}
protected function getTablePollingInterval(): ?string
{
return $this->getResourceTable()->getPollingInterval();
}
protected function getTableHeading(): ?string
{
return static::getTitle();
@@ -517,4 +523,60 @@ class RelationManager extends Component implements Tables\Contracts\HasRelations
{
return [];
}
protected function getTableRecordUrlUsing(): ?Closure
{
return function (Model $record): ?string {
foreach (['view', 'edit'] as $action) {
$action = $this->getCachedTableAction($action);
if (! $action) {
continue;
}
$action->record($record);
if ($action->isHidden()) {
continue;
}
$url = $action->getUrl();
if (! $url) {
continue;
}
return $url;
}
return null;
};
}
protected function getTableRecordActionUsing(): ?Closure
{
return function (Model $record): ?string {
foreach (['view', 'edit'] as $action) {
$action = $this->getCachedTableAction($action);
if (! $action) {
continue;
}
$action->record($record);
if ($action->isHidden()) {
continue;
}
if ($action->getUrl()) {
continue;
}
return $action->getName();
}
return null;
};
}
}
+14
View File
@@ -23,6 +23,8 @@ class Table
protected array $headerActions = [];
protected ?string $pollingInterval = null;
protected ?string $reorderColumn = null;
final public function __construct()
@@ -127,6 +129,13 @@ class Table
return $this;
}
public function poll(?string $interval = '10s'): static
{
$this->pollingInterval = $interval;
return $this;
}
/**
* @deprecated Use `appendActions()` instead.
*/
@@ -208,4 +217,9 @@ class Table
{
return $this->reorderColumn;
}
public function getPollingInterval(): ?string
{
return $this->pollingInterval;
}
}
@@ -0,0 +1,380 @@
<?php
namespace Filament\Testing;
use Closure;
use Filament\Pages\Actions\Action;
use Filament\Pages\Page;
use Filament\Support\Testing\TestsActions;
use Illuminate\Testing\Assert;
use Livewire\Testing\TestableLivewire;
/**
* @method Page instance()
*
* @mixin TestableLivewire
* @mixin TestsActions
*/
class TestsPageActions
{
public function mountPageAction(): Closure
{
return function (string $name): static {
$name = $this->parseActionName($name);
/** @phpstan-ignore-next-line */
$this->assertPageActionVisible($name);
$this->call('mountAction', $name);
$action = $this->instance()->getCachedAction($name);
if (! $action->shouldOpenModal()) {
$this->assertNotDispatchedBrowserEvent('open-modal');
$this->assertNotSet('mountedAction', $action->getName());
return $this;
}
$this->assertSet('mountedAction', $name);
$this->assertDispatchedBrowserEvent('open-modal', [
'id' => 'page-action',
]);
return $this;
};
}
public function setPageActionData(): Closure
{
return function (array $data): static {
$this->set('mountedActionData', $data);
return $this;
};
}
public function assertPageActionDataSet(): Closure
{
return function (array $data): static {
foreach ($data as $key => $value) {
$this->assertSet("mountedActionData.{$key}", $value);
}
return $this;
};
}
public function callPageAction(): Closure
{
return function (string $name, array $data = [], array $arguments = []): static {
/** @phpstan-ignore-next-line */
$this->mountPageAction($name);
if (! $this->instance()->getMountedAction()) {
return $this;
}
/** @phpstan-ignore-next-line */
$this->setPageActionData($data);
/** @phpstan-ignore-next-line */
$this->callMountedPageAction($arguments);
return $this;
};
}
public function callMountedPageAction(): Closure
{
return function (array $arguments = []): static {
$action = $this->instance()->getMountedAction();
if (! $action) {
return $this;
}
$this->call('callMountedAction', json_encode($arguments));
if ($this->get('mountedAction') !== $action->getName()) {
$this->assertDispatchedBrowserEvent('close-modal', [
'id' => 'page-action',
]);
}
return $this;
};
}
public function assertPageActionExists(): Closure
{
return function (string $name): static {
$livewire = $this->instance();
$livewireClass = $livewire::class;
$action = $livewire->getCachedAction($name);
Assert::assertInstanceOf(
Action::class,
$action,
message: "Failed asserting that an action with name [{$name}] exists on the [{$livewireClass}] page.",
);
return $this;
};
}
public function assertPageActionVisible(): Closure
{
return function (string $name): static {
/** @phpstan-ignore-next-line */
$this->assertPageActionExists($name);
$livewire = $this->instance();
$livewireClass = $livewire::class;
$action = $livewire->getCachedAction($name);
Assert::assertFalse(
$action->isHidden(),
message: "Failed asserting that an action with name [{$name}] is visible on the [{$livewireClass}] component.",
);
return $this;
};
}
public function assertPageActionHidden(): Closure
{
return function (string $name): static {
/** @phpstan-ignore-next-line */
$this->assertPageActionExists($name);
$livewire = $this->instance();
$livewireClass = $livewire::class;
$action = $livewire->getCachedAction($name);
Assert::assertTrue(
$action->isHidden(),
message: "Failed asserting that an action with name [{$name}] is hidden on the [{$livewireClass}] component.",
);
return $this;
};
}
public function assertPageActionEnabled(): Closure
{
return function (string $name): static {
/** @phpstan-ignore-next-line */
$this->assertPageActionExists($name);
$livewire = $this->instance();
$livewireClass = $livewire::class;
$action = $livewire->getCachedAction($name);
Assert::assertTrue(
$action->isEnabled(),
message: "Failed asserting that an action with name [{$name}] is enabled on the [{$livewireClass}] component.",
);
return $this;
};
}
public function assertPageActionDisabled(): Closure
{
return function (string $name): static {
/** @phpstan-ignore-next-line */
$this->assertPageActionExists($name);
$livewire = $this->instance();
$livewireClass = $livewire::class;
$action = $livewire->getCachedAction($name);
Assert::assertTrue(
$action->isDisabled(),
message: "Failed asserting that an action with name [{$name}] is disabled on the [{$livewireClass}] component.",
);
return $this;
};
}
public function assertPageActionHasIcon(): Closure
{
return function (string $name, string $icon, $record = null): static {
/** @phpstan-ignore-next-line */
$this->assertPageActionExists($name);
$livewire = $this->instance();
$livewireClass = $livewire::class;
$action = $livewire->getCachedAction($name);
Assert::assertTrue(
$action->getIcon() === $icon,
message: "Failed asserting that an action with name [{$name}] has icon [{$icon}] on the [{$livewireClass}] component.",
);
return $this;
};
}
public function assertPageActionDoesNotHaveIcon(): Closure
{
return function (string $name, string $icon, $record = null): static {
/** @phpstan-ignore-next-line */
$this->assertPageActionExists($name);
$livewire = $this->instance();
$livewireClass = $livewire::class;
$action = $livewire->getCachedAction($name);
Assert::assertFalse(
$action->getIcon() === $icon,
message: "Failed asserting that an action with name [{$name}] does not have icon [{$icon}] on the [{$livewireClass}] component.",
);
return $this;
};
}
public function assertPageActionHasLabel(): Closure
{
return function (string $name, string $label, $record = null): static {
/** @phpstan-ignore-next-line */
$this->assertPageActionExists($name);
$livewire = $this->instance();
$livewireClass = $livewire::class;
$action = $livewire->getCachedAction($name);
Assert::assertTrue(
$action->getLabel() === $label,
message: "Failed asserting that an action with name [{$name}] has label [{$label}] on the [{$livewireClass}] component.",
);
return $this;
};
}
public function assertPageActionDoesNotHaveLabel(): Closure
{
return function (string $name, string $label, $record = null): static {
/** @phpstan-ignore-next-line */
$this->assertPageActionExists($name);
$livewire = $this->instance();
$livewireClass = $livewire::class;
$action = $livewire->getCachedAction($name);
Assert::assertFalse(
$action->getLabel() === $label,
message: "Failed asserting that an action with name [{$name}] does not have label [{$label}] on the [{$livewireClass}] component.",
);
return $this;
};
}
public function assertPageActionHasColor(): Closure
{
return function (string $name, string $color, $record = null): static {
/** @phpstan-ignore-next-line */
$this->assertPageActionExists($name);
$livewire = $this->instance();
$livewireClass = $livewire::class;
$action = $livewire->getCachedAction($name);
Assert::assertTrue(
$action->getColor() === $color,
message: "Failed asserting that an action with name [{$name}] has color [{$color}] on the [{$livewireClass}] component.",
);
return $this;
};
}
public function assertPageActionDoesNotHaveColor(): Closure
{
return function (string $name, string $color, $record = null): static {
/** @phpstan-ignore-next-line */
$this->assertPageActionExists($name);
$livewire = $this->instance();
$livewireClass = $livewire::class;
$action = $livewire->getCachedAction($name);
Assert::assertFalse(
$action->getColor() === $color,
message: "Failed asserting that an action with name [{$name}] does not have color [{$color}] on the [{$livewireClass}] component.",
);
return $this;
};
}
public function assertPageActionHeld(): Closure
{
return function (string $name): static {
$name = $this->parseActionName($name);
/** @phpstan-ignore-next-line */
$this->assertPageActionExists($name);
$this->assertSet('mountedAction', $name);
return $this;
};
}
public function assertHasPageActionErrors(): Closure
{
return function (array $keys = []): static {
$this->assertHasErrors(
collect($keys)
->mapWithKeys(function ($value, $key): array {
if (is_int($key)) {
return [$key => "mountedActionData.{$value}"];
}
return ["mountedActionData.{$key}" => $value];
})
->all(),
);
return $this;
};
}
public function assertHasNoPageActionErrors(): Closure
{
return function (array $keys = []): static {
$this->assertHasNoErrors(
collect($keys)
->mapWithKeys(function ($value, $key): array {
if (is_int($key)) {
return [$key => "mountedActionData.{$value}"];
}
return ["mountedActionData.{$key}" => $value];
})
->all(),
);
return $this;
};
}
}
-152
View File
@@ -3,10 +3,8 @@
namespace Filament\Testing;
use Closure;
use Filament\Pages\Actions\Action;
use Filament\Pages\Page;
use Filament\Support\Testing\TestsActions;
use Illuminate\Testing\Assert;
use Livewire\Testing\TestableLivewire;
/**
@@ -74,154 +72,4 @@ class TestsPages
return $this;
};
}
public function callPageAction(): Closure
{
return function (string $name, array $data = [], array $arguments = []): static {
$name = $this->parseActionName($name);
/** @phpstan-ignore-next-line */
$this->assertPageActionVisible($name);
$this->call('mountAction', $name);
$action = $this->instance()->getCachedAction($name);
if (! $action->shouldOpenModal()) {
$this->assertNotDispatchedBrowserEvent('open-modal');
$this->assertNotSet('mountedAction', $name);
return $this;
}
$this->assertSet('mountedAction', $name);
$this->assertDispatchedBrowserEvent('open-modal', [
'id' => 'page-action',
]);
$this->set('mountedActionData', $data);
$this->call('callMountedAction', json_encode($arguments));
if ($this->get('mountedAction') !== $name) {
$this->assertDispatchedBrowserEvent('close-modal', [
'id' => 'page-action',
]);
}
return $this;
};
}
public function assertPageActionExists(): Closure
{
return function (string $name): static {
$livewire = $this->instance();
$livewireClass = $livewire::class;
$action = $livewire->getCachedAction($name);
Assert::assertInstanceOf(
Action::class,
$action,
message: "Failed asserting that an action with name [{$name}] exists on the [{$livewireClass}] page.",
);
return $this;
};
}
public function assertPageActionVisible(): Closure
{
return function (string $name): static {
/** @phpstan-ignore-next-line */
$this->assertPageActionExists($name);
$livewire = $this->instance();
$livewireClass = $livewire::class;
$action = $livewire->getCachedAction($name);
Assert::assertFalse(
$action->isHidden(),
message: "Failed asserting that an action with name [{$name}] is visible on the [{$livewireClass}] component.",
);
return $this;
};
}
public function assertPageActionHidden(): Closure
{
return function (string $name): static {
/** @phpstan-ignore-next-line */
$this->assertPageActionExists($name);
$livewire = $this->instance();
$livewireClass = $livewire::class;
$action = $livewire->getCachedAction($name);
Assert::assertTrue(
$action->isHidden(),
message: "Failed asserting that an action with name [{$name}] is hidden on the [{$livewireClass}] component.",
);
return $this;
};
}
public function assertPageActionHeld(): Closure
{
return function (string $name): static {
$name = $this->parseActionName($name);
/** @phpstan-ignore-next-line */
$this->assertPageActionExists($name);
$this->assertSet('mountedAction', $name);
return $this;
};
}
public function assertHasPageActionErrors(): Closure
{
return function (array $keys = []): static {
$this->assertHasErrors(
collect($keys)
->mapWithKeys(function ($value, $key): array {
if (is_int($key)) {
return [$key => "mountedActionData.{$value}"];
}
return ["mountedActionData.{$key}" => $value];
})
->all(),
);
return $this;
};
}
public function assertHasNoPageActionErrors(): Closure
{
return function (array $keys = []): static {
$this->assertHasNoErrors(
collect($keys)
->mapWithKeys(function ($value, $key): array {
if (is_int($key)) {
return [$key => "mountedActionData.{$value}"];
}
return ["mountedActionData.{$key}" => $value];
})
->all(),
);
return $this;
};
}
}
-1
View File
@@ -1 +0,0 @@
/vendor
-3665
View File
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -1607,7 +1607,7 @@ trix-editor .attachment__metadata .attachment__size {
/* packages/forms/resources/css/components/select.css */
.choices {
@apply relative overflow-hidden;
@apply relative;
}
.choices:focus {
@apply outline-none;
@@ -1643,14 +1643,14 @@ trix-editor .attachment__metadata .attachment__size {
.choices__inner {
@apply py-2 px-3 inline-block align-top w-full bg-white bg-no-repeat border border-gray-300 transition duration-75 rounded-lg shadow-sm;
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3E%3C/svg%3E");
background-position: right .5rem center;
background-position: right 0.5rem center;
background-size: 1.5em 1.5em;
}
.dark .choices__inner {
@apply border-gray-600 bg-gray-700 text-white;
}
[dir=rtl] .choices__inner {
background-position: left .5rem center;
background-position: left 0.5rem center;
}
.is-focused .choices__inner,
.is-open .choices__inner {
@@ -1791,7 +1791,7 @@ trix-editor .attachment__metadata .attachment__size {
border-color: rgb(75 85 99) !important;
}
.choices__input {
@apply inline-block border-0 max-w-full;
@apply inline-block max-w-full border-none;
border-color: transparent !important;
background-color: transparent !important;
padding: 0 !important;
+5925 -2410
View File
File diff suppressed because it is too large Load Diff
+6
View File
@@ -228,6 +228,12 @@ If you wish to translate the package, you may publish the language files using:
php artisan vendor:publish --tag=forms-translations
```
Since this package depends on other Filament packages, you may wish to translate those as well:
```bash
php artisan vendor:publish --tag=filament-support-translations
```
## Upgrading
To upgrade the package to the latest version, you must run:
+60 -5
View File
@@ -281,8 +281,6 @@ TextInput::make('name')
->maxLength(255)
```
> When using `minLength()` or `maxLength()` with `numeric()`, be aware that the validation will apply to the value of the input instead of its length. This is in line with the behaviour of Laravel's `min` and `max` validation rules.
You may also specify the exact length of the input by setting the `length()`. This method adds both frontend and backend validation:
```php
@@ -291,8 +289,6 @@ use Filament\Forms\Components\TextInput;
TextInput::make('code')->length(8)
```
Internally, this uses the `size` rule by default, and the `digits` rule for numeric inputs.
In addition, you may validate the minimum and maximum value of the input by setting the `minValue()` and `maxValue()` methods:
```php
@@ -568,7 +564,7 @@ Since HTML does not support nested `<form>` elements, you must also render the m
```blade
<form wire:submit.prevent="submit">
{{ $this->form }}
<button type="submit">
Submit
</button>
@@ -766,6 +762,16 @@ Toggle::make('is_admin')
->offIcon('heroicon-s-user')
```
You may also customize the color representing each state. These may be either `primary`, `secondary`, `success`, `warning` or `danger`:
```php
use Filament\Forms\Components\Toggle;
Toggle::make('is_admin')
->onColor('success')
->offColor('danger')
```
![](https://user-images.githubusercontent.com/41773797/147613184-9086c102-ad71-4c4e-9170-9a4201a80c66.png)
If you're saving the boolean value using Eloquent, you should be sure to add a `boolean` [cast](https://laravel.com/docs/eloquent-mutators#attribute-casting) to the model property:
@@ -1076,6 +1082,18 @@ FileUpload::make('attachment')
})
```
You can keep the randomly generated file names, while still storing the original file name, using the `storeFileNamesIn()` method:
```php
use Filament\Forms\Components\FileUpload;
FileUpload::make('attachments')
->multiple()
->storeFileNamesIn('attachment_file_names')
```
`attachment_file_names` will now store the original file name/s of your uploaded files.
You may restrict the types of files that may be uploaded using the `acceptedFileTypes()` method, and passing an array of MIME types. You may also use the `image()` method as shorthand to allow all image MIME types.
```php
@@ -1535,6 +1553,34 @@ Repeater::make('members')
Any fields that you use from `$state` should be `reactive()` or `lazy()` if you wish to see the item label update live as you use the form.
### Using `$get()` to access parent field values
All form components are able to [use `$get()` and `$set()`](advanced) to access another field's value. However, you might experience unexpected behaviour when using this inside the repeater's schema.
This is because `$get()` and `$set()`, by default, are scoped to the current repeater item. This means that you are able to interact with another field inside that repeater item easily without knowing which repeater item the current form component belongs to.
The consequence of this, is that you may be confused when you are unable to interact with a field outside the repeater. We use `../` syntax to solve this problem - `$get('../../parent_field_name')`.
Consider your form has this data structure:
```php
[
'client_id' => 1,
'repeater' => [
'item1' => [
'service_id' => 2,
],
],
]
```
You are trying to retrieve the value of `client_id` from inside the repeater item.
`$get()` is relative to the current repeater item, so `$get('client_id')` is looking for `$get('repeater.item1.client_id')`.
You can use `../` to go up a level in the data structure, so `$get('../client_id')` is $get('repeater.client_id') and `$get('../../client_id')` is `$get('client_id')`.
## Builder
Similar to a [repeater](#repeater), the builder component allows you to output a JSON array of repeated form components. Unlike the repeater, which only defines one form schema to repeat, the builder allows you to define different schema "blocks", which you can repeat in any order. This makes it useful for building more advanced array structures.
@@ -1786,6 +1832,15 @@ KeyValue::make('meta')
->disableEditingKeys()
```
You can allow the user to reorder rows within the table:
```php
use Filament\Forms\Components\KeyValue;
KeyValue::make('meta')
->reorderable()
```
You may also add placeholders for the key and value fields using the `keyPlaceholder()` and `valuePlaceholder()` methods:
```php
@@ -1,5 +1,20 @@
.filament-forms-color-picker-preview {
background-image: repeating-linear-gradient(45deg, #aaa 25%, transparent 25%, transparent 75%, #aaa 75%, #aaa), repeating-linear-gradient(45deg, #aaa 25%, #fff 25%, #fff 75%, #aaa 75%, #aaa);
background-image: repeating-linear-gradient(
45deg,
#aaa 25%,
transparent 25%,
transparent 75%,
#aaa 75%,
#aaa
),
repeating-linear-gradient(
45deg,
#aaa 25%,
#fff 25%,
#fff 75%,
#aaa 75%,
#aaa
);
background-position: 0 0, 4px 4px;
background-size: 8px 8px;
}
@@ -1,5 +1,5 @@
.choices {
@apply relative overflow-hidden;
@apply relative;
}
.choices:focus {
@@ -27,26 +27,26 @@
display: none !important;
}
.choices[data-type*=select-one] {
.choices[data-type*='select-one'] {
@apply cursor-pointer;
}
.choices[data-type*=select-one] .choices__input {
.choices[data-type*='select-one'] .choices__input {
@apply block w-full p-2 border-b m-0;
}
.dark .choices[data-type*=select-one] .choices__input {
.dark .choices[data-type*='select-one'] .choices__input {
@apply bg-gray-700;
}
.choices[data-type*=select-multiple] .choices__inner {
.choices[data-type*='select-multiple'] .choices__inner {
@apply cursor-text;
}
.choices__inner {
@apply py-2 px-3 inline-block align-top w-full bg-white bg-no-repeat border border-gray-300 transition duration-75 rounded-lg shadow-sm;
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3E%3C/svg%3E");
background-position: right .5rem center;
background-position: right 0.5rem center;
background-size: 1.5em 1.5em;
}
@@ -54,11 +54,12 @@
@apply border-gray-600 bg-gray-700 text-white;
}
[dir=rtl] .choices__inner {
background-position: left .5rem center;
[dir='rtl'] .choices__inner {
background-position: left 0.5rem center;
}
.is-focused .choices__inner, .is-open .choices__inner {
.is-focused .choices__inner,
.is-open .choices__inner {
@apply border-primary-600 ring-1 ring-inset ring-primary-600;
}
@@ -70,7 +71,7 @@
@apply pr-12 inline-block w-full;
}
[dir=rtl] .choices__list--single {
[dir='rtl'] .choices__list--single {
@apply pl-12 pr-0;
}
@@ -82,7 +83,7 @@
@apply pr-6 flex flex-wrap gap-1;
}
[dir=rtl] .choices__list--multiple {
[dir='rtl'] .choices__list--multiple {
@apply pl-6 pr-0;
}
@@ -98,40 +99,47 @@
@apply text-primary-500;
}
[dir=rtl] .choices__list--multiple .choices__item {
[dir='rtl'] .choices__list--multiple .choices__item {
@apply break-normal space-x-reverse;
}
.choices__list--dropdown, .choices__list[aria-expanded] {
.choices__list--dropdown,
.choices__list[aria-expanded] {
@apply mt-2 rounded-lg invisible z-[1] absolute w-full bg-white border border-gray-300 shadow-sm top-full overflow-hidden break-words will-change-[visibility];
}
.dark .choices__list--dropdown, .dark .choices__list[aria-expanded] {
.dark .choices__list--dropdown,
.dark .choices__list[aria-expanded] {
@apply bg-gray-700 border-gray-600;
}
.is-active.choices__list--dropdown, .is-active.choices__list[aria-expanded] {
.is-active.choices__list--dropdown,
.is-active.choices__list[aria-expanded] {
@apply visible z-10;
}
.choices__list--dropdown .choices__list, .choices__list[aria-expanded] .choices__list {
.choices__list--dropdown .choices__list,
.choices__list[aria-expanded] .choices__list {
@apply relative max-h-60 overflow-auto will-change-scroll;
}
.choices__list--dropdown .choices__item, .choices__list[aria-expanded] .choices__item {
.choices__list--dropdown .choices__item,
.choices__list[aria-expanded] .choices__item {
@apply relative px-3 py-2;
}
[dir=rtl] .choices__list--dropdown .choices__item,
[dir=rtl] .choices__list[aria-expanded] .choices__item {
[dir='rtl'] .choices__list--dropdown .choices__item,
[dir='rtl'] .choices__list[aria-expanded] .choices__item {
@apply text-right;
}
.choices__list--dropdown .choices__item--selectable.is-highlighted, .choices__list[aria-expanded] .choices__item--selectable.is-highlighted {
.choices__list--dropdown .choices__item--selectable.is-highlighted,
.choices__list[aria-expanded] .choices__item--selectable.is-highlighted {
@apply bg-primary-600 text-white;
}
.choices__list--dropdown .choices__item--selectable.is-highlighted::after, .choices__list[aria-expanded] .choices__item--selectable.is-highlighted::after {
.choices__list--dropdown .choices__item--selectable.is-highlighted::after,
.choices__list[aria-expanded] .choices__item--selectable.is-highlighted::after {
@apply opacity-70;
}
@@ -160,54 +168,56 @@
@apply outline-none;
}
.choices[data-type*=select-one] .choices__button {
.choices[data-type*='select-one'] .choices__button {
@apply p-0 absolute right-0 mr-9 h-4 w-4 opacity-60;
background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEiIGhlaWdodD0iMjEiIHZpZXdCb3g9IjAgMCAyMSAyMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48ZyBmaWxsPSIjMDAwIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPjxwYXRoIGQ9Ik0yLjU5Mi4wNDRsMTguMzY0IDE4LjM2NC0yLjU0OCAyLjU0OEwuMDQ0IDIuNTkyeiIvPjxwYXRoIGQ9Ik0wIDE4LjM2NEwxOC4zNjQgMGwyLjU0OCAyLjU0OEwyLjU0OCAyMC45MTJ6Ii8+PC9nPjwvc3ZnPg==");
background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEiIGhlaWdodD0iMjEiIHZpZXdCb3g9IjAgMCAyMSAyMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48ZyBmaWxsPSIjMDAwIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPjxwYXRoIGQ9Ik0yLjU5Mi4wNDRsMTguMzY0IDE4LjM2NC0yLjU0OCAyLjU0OEwuMDQ0IDIuNTkyeiIvPjxwYXRoIGQ9Ik0wIDE4LjM2NEwxOC4zNjQgMGwyLjU0OCAyLjU0OEwyLjU0OCAyMC45MTJ6Ii8+PC9nPjwvc3ZnPg==');
background-size: 0.7em 0.7em;
top: calc(50% - 0.55em);
}
.dark .choices[data-type*=select-one] .choices__button {
.dark .choices[data-type*='select-one'] .choices__button {
@apply opacity-30;
background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEiIGhlaWdodD0iMjEiIHZpZXdCb3g9IjAgMCAyMSAyMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48ZyBmaWxsPSIjZmZmIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPjxwYXRoIGQ9Ik0yLjU5Mi4wNDRsMTguMzY0IDE4LjM2NC0yLjU0OCAyLjU0OEwuMDQ0IDIuNTkyeiIvPjxwYXRoIGQ9Ik0wIDE4LjM2NEwxOC4zNjQgMGwyLjU0OCAyLjU0OEwyLjU0OCAyMC45MTJ6Ii8+PC9nPjwvc3ZnPg==");
background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEiIGhlaWdodD0iMjEiIHZpZXdCb3g9IjAgMCAyMSAyMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48ZyBmaWxsPSIjZmZmIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPjxwYXRoIGQ9Ik0yLjU5Mi4wNDRsMTguMzY0IDE4LjM2NC0yLjU0OCAyLjU0OEwuMDQ0IDIuNTkyeiIvPjxwYXRoIGQ9Ik0wIDE4LjM2NEwxOC4zNjQgMGwyLjU0OCAyLjU0OEwyLjU0OCAyMC45MTJ6Ii8+PC9nPjwvc3ZnPg==');
}
[dir=rtl] .choices[data-type*=select-one] .choices__button {
[dir='rtl'] .choices[data-type*='select-one'] .choices__button {
@apply left-0 right-auto ml-9 mr-0;
}
.choices[data-type*=select-one] .choices__button:hover,
.choices[data-type*=select-one] .choices__button:focus {
.choices[data-type*='select-one'] .choices__button:hover,
.choices[data-type*='select-one'] .choices__button:focus {
@apply opacity-75;
}
.dark .choices[data-type*=select-one] .choices__button:hover,
.dark .choices[data-type*=select-one] .choices__button:focus {
.dark .choices[data-type*='select-one'] .choices__button:hover,
.dark .choices[data-type*='select-one'] .choices__button:focus {
@apply opacity-60;
}
.choices[data-type*=select-one] .choices__item[data-value=""] .choices__button {
.choices[data-type*='select-one']
.choices__item[data-value='']
.choices__button {
@apply hidden;
}
.choices[data-type*=select-multiple] .choices__button {
.choices[data-type*='select-multiple'] .choices__button {
@apply text-primary-700 inline-block w-3 h-3 opacity-60;
background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEiIGhlaWdodD0iMjEiIHZpZXdCb3g9IjAgMCAyMSAyMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48ZyBmaWxsPSIjMDAwIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPjxwYXRoIGQ9Ik0yLjU5Mi4wNDRsMTguMzY0IDE4LjM2NC0yLjU0OCAyLjU0OEwuMDQ0IDIuNTkyeiIvPjxwYXRoIGQ9Ik0wIDE4LjM2NEwxOC4zNjQgMGwyLjU0OCAyLjU0OEwyLjU0OCAyMC45MTJ6Ii8+PC9nPjwvc3ZnPg==");
background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEiIGhlaWdodD0iMjEiIHZpZXdCb3g9IjAgMCAyMSAyMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48ZyBmaWxsPSIjMDAwIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPjxwYXRoIGQ9Ik0yLjU5Mi4wNDRsMTguMzY0IDE4LjM2NC0yLjU0OCAyLjU0OEwuMDQ0IDIuNTkyeiIvPjxwYXRoIGQ9Ik0wIDE4LjM2NEwxOC4zNjQgMGwyLjU0OCAyLjU0OEwyLjU0OCAyMC45MTJ6Ii8+PC9nPjwvc3ZnPg==');
background-size: 0.6em 0.6em;
}
.dark .choices[data-type*=select-multiple] .choices__button {
.dark .choices[data-type*='select-multiple'] .choices__button {
@apply opacity-75;
background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEiIGhlaWdodD0iMjEiIHZpZXdCb3g9IjAgMCAyMSAyMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48ZyBmaWxsPSIjZmZmIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPjxwYXRoIGQ9Ik0yLjU5Mi4wNDRsMTguMzY0IDE4LjM2NC0yLjU0OCAyLjU0OEwuMDQ0IDIuNTkyeiIvPjxwYXRoIGQ9Ik0wIDE4LjM2NEwxOC4zNjQgMGwyLjU0OCAyLjU0OEwyLjU0OCAyMC45MTJ6Ii8+PC9nPjwvc3ZnPg==");
background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEiIGhlaWdodD0iMjEiIHZpZXdCb3g9IjAgMCAyMSAyMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48ZyBmaWxsPSIjZmZmIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPjxwYXRoIGQ9Ik0yLjU5Mi4wNDRsMTguMzY0IDE4LjM2NC0yLjU0OCAyLjU0OEwuMDQ0IDIuNTkyeiIvPjxwYXRoIGQ9Ik0wIDE4LjM2NEwxOC4zNjQgMGwyLjU0OCAyLjU0OEwyLjU0OCAyMC45MTJ6Ii8+PC9nPjwvc3ZnPg==');
}
.choices[data-type*=select-multiple] .choices__button:hover,
.choices[data-type*=select-multiple] .choices__button:focus {
.choices[data-type*='select-multiple'] .choices__button:hover,
.choices[data-type*='select-multiple'] .choices__button:focus {
@apply opacity-75;
}
.dark .choices[data-type*=select-multiple] .choices__button:hover,
.dark .choices[data-type*=select-multiple] .choices__button:focus {
.dark .choices[data-type*='select-multiple'] .choices__button:hover,
.dark .choices[data-type*='select-multiple'] .choices__button:focus {
@apply opacity-100;
}
@@ -228,7 +238,7 @@
}
.choices__input {
@apply inline-block border-0 max-w-full;
@apply inline-block max-w-full border-none;
border-color: transparent !important;
background-color: transparent !important;
padding: 0 !important;
@@ -236,13 +246,18 @@
.choices__input:focus {
outline-color: transparent !important;
box-shadow: var(--tw-ring-inset) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color) !important;
box-shadow: var(--tw-ring-inset) 0 0 0
calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color) !important;
}
.choices__input::-webkit-search-decoration, .choices__input::-webkit-search-cancel-button, .choices__input::-webkit-search-results-button, .choices__input::-webkit-search-results-decoration {
.choices__input::-webkit-search-decoration,
.choices__input::-webkit-search-cancel-button,
.choices__input::-webkit-search-results-button,
.choices__input::-webkit-search-results-decoration {
@apply hidden;
}
.choices__input::-ms-clear, .choices__input::-ms-reveal {
.choices__input::-ms-clear,
.choices__input::-ms-reveal {
@apply hidden w-0 h-0;
}
@@ -30,7 +30,7 @@ export default (Alpine) => {
'color-changed',
(event) => {
this.setState(event.detail.value)
}
},
)
},
@@ -49,6 +49,6 @@ export default (Alpine) => {
this.$refs.panel.color = value
},
}
}
},
)
}
@@ -8,20 +8,6 @@ dayjs.extend(customParseFormat)
dayjs.extend(localeData)
dayjs.extend(timezone)
dayjs.extend(utc)
dayjs.extend((option, Dayjs, dayjs) => {
const listeners = []
dayjs.addLocaleListeners = (listener) => listeners.push(listener)
dayjs.onLocaleUpdated = () => {
listeners.forEach((listener) => listener())
}
dayjs.updateLocale = (locale) => {
dayjs.locale(locale)
// Emit the `localeUpdated` event that we can bind to later
dayjs.onLocaleUpdated()
}
})
window.dayjs = dayjs
@@ -33,6 +19,7 @@ export default (Alpine) => {
firstDayOfWeek,
isAutofocused,
isDisabled,
locale,
shouldCloseOnDateSelection,
state,
}) => {
@@ -66,6 +53,8 @@ export default (Alpine) => {
months: [],
init: function () {
dayjs.locale(locales[locale] ?? locales['en'])
this.focusedDate = dayjs().tz(timezone)
let date =
@@ -94,16 +83,10 @@ export default (Alpine) => {
if (isAutofocused) {
this.$nextTick(() =>
this.togglePanelVisibility(this.$refs.button)
this.togglePanelVisibility(this.$refs.button),
)
}
dayjs.addLocaleListeners(() => {
this.setDisplayText()
this.setMonths()
this.setDayLabels()
})
this.$watch('focusedMonth', () => {
this.focusedMonth = +this.focusedMonth
@@ -112,7 +95,7 @@ export default (Alpine) => {
}
this.focusedDate = this.focusedDate.month(
this.focusedMonth
this.focusedMonth,
)
})
@@ -432,14 +415,14 @@ export default (Alpine) => {
.date(8 - firstDayOfWeek)
.day(),
},
(_, i) => i + 1
(_, i) => i + 1,
)
this.daysInFocusedMonth = Array.from(
{
length: this.focusedDate.daysInMonth(),
},
(_, i) => i + 1
(_, i) => i + 1,
)
},
@@ -474,6 +457,41 @@ export default (Alpine) => {
return this.$refs.panel.style.display === 'block'
},
}
}
},
)
}
const locales = {
ar: require('dayjs/locale/ar'),
bs: require('dayjs/locale/bs'),
ca: require('dayjs/locale/ca'),
cs: require('dayjs/locale/cs'),
da: require('dayjs/locale/da'),
de: require('dayjs/locale/de'),
en: require('dayjs/locale/en'),
es: require('dayjs/locale/es'),
fa: require('dayjs/locale/fa'),
fr: require('dayjs/locale/fr'),
hi: require('dayjs/locale/hi'),
hu: require('dayjs/locale/hu'),
hy: require('dayjs/locale/hy-am'),
id: require('dayjs/locale/id'),
it: require('dayjs/locale/it'),
js: require('dayjs/locale/ja'),
ka: require('dayjs/locale/ka'),
ku: require('dayjs/locale/ku'),
ms: require('dayjs/locale/ms'),
my: require('dayjs/locale/my'),
nl: require('dayjs/locale/nl'),
pl: require('dayjs/locale/pl'),
pt_BR: require('dayjs/locale/pt-br'),
pt_PT: require('dayjs/locale/pt'),
ro: require('dayjs/locale/ro'),
ru: require('dayjs/locale/ru'),
sv: require('dayjs/locale/sv'),
tr: require('dayjs/locale/tr'),
uk: require('dayjs/locale/uk'),
vi: require('dayjs/locale/vi'),
zh_CN: require('dayjs/locale/zh-cn'),
zh_TW: require('dayjs/locale/zh-tw'),
}
@@ -10,7 +10,7 @@ import FilePondPluginMediaPreview from 'filepond-plugin-media-preview'
import 'filepond/dist/filepond.min.css'
import 'filepond-plugin-image-preview/dist/filepond-plugin-image-preview.css'
import 'filepond-plugin-media-preview/dist/filepond-plugin-media-preview.css';
import 'filepond-plugin-media-preview/dist/filepond-plugin-media-preview.css'
import '../../css/components/file-upload.css'
FilePond.registerPlugin(FilePondPluginFileValidateSize)
@@ -22,300 +22,404 @@ FilePond.registerPlugin(FilePondPluginImageResize)
FilePond.registerPlugin(FilePondPluginImageTransform)
FilePond.registerPlugin(FilePondPluginMediaPreview)
window.FilePond = FilePond;
window.FilePond = FilePond
export default (Alpine) => {
Alpine.data('fileUploadFormComponent', ({
acceptedFileTypes,
canDownload,
canOpen,
canPreview,
canReorder,
deleteUploadedFileUsing,
getUploadedFileUrlsUsing,
imageCropAspectRatio,
imagePreviewHeight,
imageResizeMode,
imageResizeTargetHeight,
imageResizeTargetWidth,
isAvatar,
loadingIndicatorPosition,
panelAspectRatio,
panelLayout,
placeholder,
maxSize,
minSize,
removeUploadedFileButtonPosition,
removeUploadedFileUsing,
reorderUploadedFilesUsing,
shouldAppendFiles,
shouldTransformImage,
state,
uploadButtonPosition,
uploadProgressIndicatorPosition,
uploadUsing,
}) => {
return {
fileKeyIndex: {},
pond: null,
shouldUpdateState: true,
Alpine.data(
'fileUploadFormComponent',
({
acceptedFileTypes,
canDownload,
canOpen,
canPreview,
canReorder,
deleteUploadedFileUsing,
getUploadedFileUrlsUsing,
imageCropAspectRatio,
imagePreviewHeight,
imageResizeMode,
imageResizeTargetHeight,
imageResizeTargetWidth,
isAvatar,
loadingIndicatorPosition,
locale,
panelAspectRatio,
panelLayout,
placeholder,
maxSize,
minSize,
removeUploadedFileButtonPosition,
removeUploadedFileUsing,
reorderUploadedFilesUsing,
shouldAppendFiles,
shouldTransformImage,
state,
uploadButtonPosition,
uploadProgressIndicatorPosition,
uploadUsing,
}) => {
return {
fileKeyIndex: {},
lastState: null,
pond: null,
uploadedFileUrlIndex: {},
shouldUpdateState: true,
init: async function () {
this.pond = FilePond.create(this.$refs.input, {
acceptedFileTypes,
allowPaste: false,
allowReorder: canReorder,
allowImagePreview: canPreview,
allowVideoPreview: canPreview,
allowAudioPreview: canPreview,
allowImageTransform: shouldTransformImage,
credits: false,
files: await this.getFiles(),
imageCropAspectRatio,
imagePreviewHeight,
imageResizeTargetHeight,
imageResizeTargetWidth,
imageResizeMode,
itemInsertLocation: shouldAppendFiles ? 'after' : 'before',
...(placeholder && {labelIdle: placeholder}),
maxFileSize: maxSize,
minFileSize: minSize,
styleButtonProcessItemPosition: uploadButtonPosition,
styleButtonRemoveItemPosition: removeUploadedFileButtonPosition,
styleLoadIndicatorPosition: loadingIndicatorPosition,
stylePanelAspectRatio: panelAspectRatio,
stylePanelLayout: panelLayout,
styleProgressIndicatorPosition: uploadProgressIndicatorPosition,
server: {
load: async (source, load) => {
let response = await fetch(source)
let blob = await response.blob()
state,
load(blob)
},
process: (fieldName, file, metadata, load, error, progress) => {
this.shouldUpdateState = false
lastState: null,
let fileKey = ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c =>
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
)
uploadedFileUrlIndex: {},
uploadUsing(fileKey, file, (fileKey) => {
this.shouldUpdateState = true
init: async function () {
FilePond.setOptions(locales[locale] ?? locales['en'])
load(fileKey)
}, error, progress)
},
remove: async (source, load) => {
let fileKey = this.uploadedFileUrlIndex[source] ?? null
this.pond = FilePond.create(this.$refs.input, {
acceptedFileTypes,
allowPaste: false,
allowReorder: canReorder,
allowImagePreview: canPreview,
allowVideoPreview: canPreview,
allowAudioPreview: canPreview,
allowImageTransform: shouldTransformImage,
credits: false,
files: await this.getFiles(),
imageCropAspectRatio,
imagePreviewHeight,
imageResizeTargetHeight,
imageResizeTargetWidth,
imageResizeMode,
itemInsertLocation: shouldAppendFiles
? 'after'
: 'before',
...(placeholder && { labelIdle: placeholder }),
maxFileSize: maxSize,
minFileSize: minSize,
styleButtonProcessItemPosition: uploadButtonPosition,
styleButtonRemoveItemPosition:
removeUploadedFileButtonPosition,
styleLoadIndicatorPosition: loadingIndicatorPosition,
stylePanelAspectRatio: panelAspectRatio,
stylePanelLayout: panelLayout,
styleProgressIndicatorPosition:
uploadProgressIndicatorPosition,
server: {
load: async (source, load) => {
let response = await fetch(source)
let blob = await response.blob()
if (! fileKey) {
return
}
load(blob)
},
process: (
fieldName,
file,
metadata,
load,
error,
progress,
) => {
this.shouldUpdateState = false
await deleteUploadedFileUsing(fileKey)
let fileKey = (
[1e7] +
-1e3 +
-4e3 +
-8e3 +
-1e11
).replace(/[018]/g, (c) =>
(
c ^
(crypto.getRandomValues(
new Uint8Array(1),
)[0] &
(15 >> (c / 4)))
).toString(16),
)
load()
},
revert: async (uniqueFileId, load) => {
await removeUploadedFileUsing(uniqueFileId)
uploadUsing(
fileKey,
file,
(fileKey) => {
this.shouldUpdateState = true
load()
},
},
})
load(fileKey)
},
error,
progress,
)
},
remove: async (source, load) => {
let fileKey =
this.uploadedFileUrlIndex[source] ?? null
this.$watch('state', async () => {
if (! this.shouldUpdateState) {
return;
}
if (!fileKey) {
return
}
// We don't want to overwrite the files that are already in the input, if they haven't been saved yet.
if (Object.values(this.state).filter((file) => file.startsWith('livewire-file:')).length) {
return
}
await deleteUploadedFileUsing(fileKey)
// Don't do anything if the state hasn't changed
if (JSON.stringify(this.state) === this.lastState) {
return
}
load()
},
revert: async (uniqueFileId, load) => {
await removeUploadedFileUsing(uniqueFileId)
this.lastState = JSON.stringify(this.state)
this.pond.files = await this.getFiles()
})
this.pond.on('reorderfiles', async (files) => {
const orderedFileKeys = files
.map(file => file.source instanceof File ? file.serverId : this.uploadedFileUrlIndex[file.source] ?? null) // file.serverId is null for a file that is not yet uploaded
.filter(fileKey => fileKey)
await reorderUploadedFilesUsing(shouldAppendFiles ? orderedFileKeys : orderedFileKeys.reverse())
})
this.pond.on('initfile', async (fileItem) => {
if (! canDownload) {
return
}
if (isAvatar) {
return
}
this.insertDownloadLink(fileItem)
})
this.pond.on('initfile', async (fileItem) => {
if (! canOpen) {
return
}
if (isAvatar) {
return
}
this.insertOpenLink(fileItem)
})
this.pond.on('processfilestart', async () => {
this.dispatchFormEvent('file-upload-started')
})
this.pond.on('processfileprogress', async () => {
this.dispatchFormEvent('file-upload-started')
})
this.pond.on('processfile', async () => {
this.dispatchFormEvent('file-upload-finished')
})
this.pond.on('processfiles', async () => {
this.dispatchFormEvent('file-upload-finished')
})
this.pond.on('processfileabort', async () => {
this.dispatchFormEvent('file-upload-finished')
})
this.pond.on('processfilerevert', async () => {
this.dispatchFormEvent('file-upload-finished')
})
},
dispatchFormEvent: function (name) {
this.$el.closest('form')?.dispatchEvent(
new CustomEvent(name, {
composed: true,
cancelable: true,
})
)
},
getUploadedFileUrls: async function () {
const uploadedFileUrls = await getUploadedFileUrlsUsing()
this.fileKeyIndex = uploadedFileUrls ?? {}
this.uploadedFileUrlIndex = Object.entries(this.fileKeyIndex)
.filter(value => value)
.reduce((obj, [key, value]) => {
obj[value] = key
return obj
}, {})
},
getFiles: async function () {
await this.getUploadedFileUrls()
let files = []
for (const uploadedFileUrl of Object.values(this.fileKeyIndex)) {
if (! uploadedFileUrl) {
continue
}
files.push({
source: uploadedFileUrl,
options: {
type: 'local',
load()
},
},
})
}
return shouldAppendFiles ? files : files.reverse()
},
this.$watch('state', async () => {
if (!this.shouldUpdateState) {
return
}
insertDownloadLink: function (file) {
if (file.origin !== FilePond.FileOrigin.LOCAL) {
return
}
// We don't want to overwrite the files that are already in the input, if they haven't been saved yet.
if (
Object.values(this.state).filter((file) =>
file.startsWith('livewire-file:'),
).length
) {
return
}
const anchor = this.getDownloadLink(file)
// Don't do anything if the state hasn't changed
if (JSON.stringify(this.state) === this.lastState) {
return
}
if (! anchor) {
return
}
this.lastState = JSON.stringify(this.state)
document.getElementById(`filepond--item-${file.id}`)
.querySelector('.filepond--file-info-main')
.prepend(anchor)
},
this.pond.files = await this.getFiles()
})
this.pond.on('reorderfiles', async (files) => {
const orderedFileKeys = files
.map((file) =>
file.source instanceof File
? file.serverId
: this.uploadedFileUrlIndex[file.source] ??
null,
) // file.serverId is null for a file that is not yet uploaded
.filter((fileKey) => fileKey)
insertOpenLink: function (file) {
if (file.origin !== FilePond.FileOrigin.LOCAL) {
return
}
await reorderUploadedFilesUsing(
shouldAppendFiles
? orderedFileKeys
: orderedFileKeys.reverse(),
)
})
const anchor = this.getOpenLink(file)
this.pond.on('initfile', async (fileItem) => {
if (!canDownload) {
return
}
if (! anchor) {
return
}
if (isAvatar) {
return
}
document.getElementById(`filepond--item-${file.id}`)
.querySelector('.filepond--file-info-main')
.prepend(anchor)
},
this.insertDownloadLink(fileItem)
})
getDownloadLink: function (file) {
let fileSource = file.source
this.pond.on('initfile', async (fileItem) => {
if (!canOpen) {
return
}
if (! fileSource) {
return
}
if (isAvatar) {
return
}
const anchor = document.createElement('a')
anchor.className = 'filepond--download-icon'
anchor.href = fileSource
anchor.download = file.file.name
this.insertOpenLink(fileItem)
})
return anchor
},
this.pond.on('processfilestart', async () => {
this.dispatchFormEvent('file-upload-started')
})
getOpenLink: function (file) {
let fileSource = file.source
this.pond.on('processfileprogress', async () => {
this.dispatchFormEvent('file-upload-started')
})
if (! fileSource) {
return
}
this.pond.on('processfile', async () => {
this.dispatchFormEvent('file-upload-finished')
})
const anchor = document.createElement('a')
anchor.className = 'filepond--open-icon'
anchor.href = fileSource
anchor.target = '_blank'
this.pond.on('processfiles', async () => {
this.dispatchFormEvent('file-upload-finished')
})
return anchor
this.pond.on('processfileabort', async () => {
this.dispatchFormEvent('file-upload-finished')
})
this.pond.on('processfilerevert', async () => {
this.dispatchFormEvent('file-upload-finished')
})
},
dispatchFormEvent: function (name) {
this.$el.closest('form')?.dispatchEvent(
new CustomEvent(name, {
composed: true,
cancelable: true,
}),
)
},
getUploadedFileUrls: async function () {
const uploadedFileUrls = await getUploadedFileUrlsUsing()
this.fileKeyIndex = uploadedFileUrls ?? {}
this.uploadedFileUrlIndex = Object.entries(
this.fileKeyIndex,
)
.filter((value) => value)
.reduce((obj, [key, value]) => {
obj[value] = key
return obj
}, {})
},
getFiles: async function () {
await this.getUploadedFileUrls()
let files = []
for (const uploadedFileUrl of Object.values(
this.fileKeyIndex,
)) {
if (!uploadedFileUrl) {
continue
}
files.push({
source: uploadedFileUrl,
options: {
type: 'local',
},
})
}
return shouldAppendFiles ? files : files.reverse()
},
insertDownloadLink: function (file) {
if (file.origin !== FilePond.FileOrigin.LOCAL) {
return
}
const anchor = this.getDownloadLink(file)
if (!anchor) {
return
}
document
.getElementById(`filepond--item-${file.id}`)
.querySelector('.filepond--file-info-main')
.prepend(anchor)
},
insertOpenLink: function (file) {
if (file.origin !== FilePond.FileOrigin.LOCAL) {
return
}
const anchor = this.getOpenLink(file)
if (!anchor) {
return
}
document
.getElementById(`filepond--item-${file.id}`)
.querySelector('.filepond--file-info-main')
.prepend(anchor)
},
getDownloadLink: function (file) {
let fileSource = file.source
if (!fileSource) {
return
}
const anchor = document.createElement('a')
anchor.className = 'filepond--download-icon'
anchor.href = fileSource
anchor.download = file.file.name
return anchor
},
getOpenLink: function (file) {
let fileSource = file.source
if (!fileSource) {
return
}
const anchor = document.createElement('a')
anchor.className = 'filepond--open-icon'
anchor.href = fileSource
anchor.target = '_blank'
return anchor
},
}
}
})
},
)
}
import ar from 'filepond/locale/ar-ar'
import cs from 'filepond/locale/cs-cz'
import da from 'filepond/locale/da-dk'
import de from 'filepond/locale/de-de'
import en from 'filepond/locale/en-en'
import es from 'filepond/locale/es-es'
import fa from 'filepond/locale/fa_ir'
import fr from 'filepond/locale/fr-fr'
import hu from 'filepond/locale/hu-hu'
import id from 'filepond/locale/id-id'
import it from 'filepond/locale/it-it'
import nl from 'filepond/locale/nl-nl'
import pl from 'filepond/locale/pl-pl'
import pt_BR from 'filepond/locale/pt-br'
import pt_PT from 'filepond/locale/pt-br'
import ro from 'filepond/locale/ro-ro'
import ru from 'filepond/locale/ru-ru'
import sv from 'filepond/locale/sv_se'
import tr from 'filepond/locale/tr-tr'
import uk from 'filepond/locale/uk-ua'
import vi from 'filepond/locale/vi-vi'
import zh_CN from 'filepond/locale/zh-cn'
import zh_TW from 'filepond/locale/zh-tw'
const locales = {
ar,
cs,
da,
de,
en,
es,
fa,
fr,
hu,
id,
it,
nl,
pl,
pt_BR,
pt_PT,
ro,
ru,
sv,
tr,
uk,
vi,
zh_CN,
zh_TW,
}
@@ -1,7 +1,5 @@
export default (Alpine) => {
Alpine.data('keyValueFormComponent', ({
state,
}) => ({
Alpine.data('keyValueFormComponent', ({ state }) => ({
state,
rows: [],
@@ -18,7 +16,7 @@ export default (Alpine) => {
this.shouldUpdateRows = true
this.$watch('state', () => {
if (! this.shouldUpdateRows) {
if (!this.shouldUpdateRows) {
this.shouldUpdateRows = true
return
@@ -46,6 +44,17 @@ export default (Alpine) => {
this.shouldUpdateRows = true
},
reorderRows: function (event) {
const rows = Alpine.raw(this.rows)
const reorderedRow = rows.splice(event.oldIndex, 1)[0]
rows.splice(event.newIndex, 0, reorderedRow)
this.rows = rows
this.updateState()
},
updateRows: function () {
let rows = []
@@ -5,10 +5,7 @@ import { marked } from 'marked'
import { highlight } from 'mdhl'
export default (Alpine) => {
Alpine.data('markdownEditorFormComponent', ({
state,
tab,
}) => {
Alpine.data('markdownEditorFormComponent', ({ state, tab }) => {
return {
attachment: null,
@@ -35,7 +32,8 @@ export default (Alpine) => {
render: function () {
if (this.$refs.textarea.scrollHeight > 0) {
this.$refs.overlay.style.height = '150px'
this.$refs.overlay.style.height = this.$refs.textarea.scrollHeight + 'px'
this.$refs.overlay.style.height =
this.$refs.textarea.scrollHeight + 'px'
}
this.overlay = null
@@ -46,15 +44,15 @@ export default (Alpine) => {
},
checkForAutoInsertion($event) {
const lines = this.$refs.textarea.value.split("\n")
const lines = this.$refs.textarea.value.split('\n')
const currentLine = this.$refs.textarea.value.substring(
0, this.$refs.textarea.value.selectionStart
).split("\n").length
const currentLine = this.$refs.textarea.value
.substring(0, this.$refs.textarea.value.selectionStart)
.split('\n').length
const previousLine = lines[currentLine - 2]
if (! previousLine.match(/^(\*\s|-\s)|^(\d)+\./)) {
return;
if (!previousLine.match(/^(\*\s|-\s)|^(\d)+\./)) {
return
}
if (previousLine.match(/^(\*\s)/)) {
@@ -73,14 +71,14 @@ export default (Alpine) => {
const matches = previousLine.match(/^(\d)+/)
const number = matches[0]
if (previousLine.trim().length > (number.length + 2)) {
if (previousLine.trim().length > number.length + 2) {
lines[currentLine - 1] = `${parseInt(number) + 1}. `
} else {
delete lines[currentLine - 2]
}
}
this.state = lines.join("\n")
this.state = lines.join('\n')
this.render()
},
@@ -11,35 +11,39 @@ Trix.config.blockAttributes.heading = {
tagName: 'h2',
terminal: true,
breakOnReturn: true,
group: false
group: false,
}
Trix.config.blockAttributes.subHeading = {
tagName: 'h3',
terminal: true,
breakOnReturn: true,
group: false
group: false,
}
Trix.Block.prototype.breaksOnReturn = function () {
const lastAttribute = this.getLastAttribute()
const blockConfig = Trix.getBlockConfig(lastAttribute ? lastAttribute : 'default')
const blockConfig = Trix.getBlockConfig(
lastAttribute ? lastAttribute : 'default',
)
return blockConfig?.breakOnReturn ?? false
}
Trix.LineBreakInsertion.prototype.shouldInsertBlockBreak = function () {
if (this.block.hasAttributes() && this.block.isListItem() && (! this.block.isEmpty())) {
if (
this.block.hasAttributes() &&
this.block.isListItem() &&
!this.block.isEmpty()
) {
return this.startLocation.offset > 0
} else {
return (! this.shouldBreakFormattedBlock()) ? this.breaksOnReturn : false
return !this.shouldBreakFormattedBlock() ? this.breaksOnReturn : false
}
}
export default (Alpine) => {
Alpine.data('richEditorFormComponent', ({
state,
}) => {
Alpine.data('richEditorFormComponent', ({ state }) => {
return {
state,
@@ -53,7 +57,7 @@ export default (Alpine) => {
this.$refs.trix?.editor?.loadHTML(this.state)
})
}
},
}
})
}
+199 -175
View File
@@ -3,224 +3,248 @@ import Choices from 'choices.js'
import '../../css/components/select.css'
export default (Alpine) => {
Alpine.data('selectFormComponent', ({
isHtmlAllowed,
getOptionLabelUsing,
getOptionLabelsUsing,
getOptionsUsing,
getSearchResultsUsing,
isAutofocused,
isMultiple,
hasDynamicOptions,
hasDynamicSearchResults,
loadingMessage,
maxItems,
noSearchResultsMessage,
options,
optionsLimit,
placeholder,
searchingMessage,
searchPrompt,
state,
}) => {
return {
isSearching: false,
select: null,
selectedOptions: [],
isStateBeingUpdated: false,
Alpine.data(
'selectFormComponent',
({
isHtmlAllowed,
getOptionLabelUsing,
getOptionLabelsUsing,
getOptionsUsing,
getSearchResultsUsing,
isAutofocused,
isMultiple,
hasDynamicOptions,
hasDynamicSearchResults,
loadingMessage,
maxItems,
noSearchResultsMessage,
options,
optionsLimit,
placeholder,
searchingMessage,
searchPrompt,
state,
}) => {
return {
isSearching: false,
init: async function () {
this.select = new Choices(this.$refs.input, {
allowHTML: isHtmlAllowed,
duplicateItemsAllowed: false,
itemSelectText: '',
loadingText: loadingMessage,
maxItemCount: maxItems ?? -1,
noChoicesText: searchPrompt,
noResultsText: noSearchResultsMessage,
placeholderValue: placeholder,
removeItemButton: true,
renderChoiceLimit: optionsLimit,
searchFields: ['label'],
searchResultLimit: optionsLimit,
shouldSort: false,
})
select: null,
await this.refreshChoices({ withInitialOptions: true })
selectedOptions: [],
if (! [null, undefined, ''].includes(this.state)) {
this.select.setChoiceByValue(this.formatState(this.state))
}
isStateBeingUpdated: false,
this.refreshPlaceholder()
state,
if (isAutofocused) {
this.select.showDropdown()
}
init: async function () {
this.select = new Choices(this.$refs.input, {
allowHTML: isHtmlAllowed,
duplicateItemsAllowed: false,
itemSelectText: '',
loadingText: loadingMessage,
maxItemCount: maxItems ?? -1,
noChoicesText: searchPrompt,
noResultsText: noSearchResultsMessage,
placeholderValue: placeholder,
removeItemButton: true,
renderChoiceLimit: optionsLimit,
searchFields: ['label'],
searchResultLimit: optionsLimit,
shouldSort: false,
})
this.$refs.input.addEventListener('change', () => {
this.refreshPlaceholder()
await this.refreshChoices({ withInitialOptions: true })
if (this.isStateBeingUpdated) {
return
if (![null, undefined, ''].includes(this.state)) {
this.select.setChoiceByValue(
this.formatState(this.state),
)
}
this.isStateBeingUpdated = true
this.state = this.select.getValue(true) ?? null
this.$nextTick(() => this.isStateBeingUpdated = false)
})
this.refreshPlaceholder()
if (hasDynamicOptions) {
this.$refs.input.addEventListener('showDropdown', async () => {
this.select.clearChoices()
await this.select.setChoices(
[{ value: '', label: loadingMessage, disabled: true }],
)
if (isAutofocused) {
this.select.showDropdown()
}
await this.refreshChoices()
})
}
this.$refs.input.addEventListener('change', () => {
this.refreshPlaceholder()
if (hasDynamicSearchResults) {
this.$refs.input.addEventListener('search', async (event) => {
let search = event.detail.value?.trim()
if ([null, undefined, ''].includes(search)) {
if (this.isStateBeingUpdated) {
return
}
this.isSearching = true
this.select.clearChoices()
await this.select.setChoices(
[{ value: '', label: searchingMessage, disabled: true }],
)
this.isStateBeingUpdated = true
this.state = this.select.getValue(true) ?? null
this.$nextTick(() => (this.isStateBeingUpdated = false))
})
this.$refs.input.addEventListener('search', Alpine.debounce(async (event) => {
await this.refreshChoices({
search: event.detail.value?.trim(),
if (hasDynamicOptions) {
this.$refs.input.addEventListener(
'showDropdown',
async () => {
this.select.clearChoices()
await this.select.setChoices([
{
value: '',
label: loadingMessage,
disabled: true,
},
])
await this.refreshChoices()
},
)
}
if (hasDynamicSearchResults) {
this.$refs.input.addEventListener(
'search',
async (event) => {
let search = event.detail.value?.trim()
if ([null, undefined, ''].includes(search)) {
return
}
this.isSearching = true
this.select.clearChoices()
await this.select.setChoices([
{
value: '',
label: searchingMessage,
disabled: true,
},
])
},
)
this.$refs.input.addEventListener(
'search',
Alpine.debounce(async (event) => {
await this.refreshChoices({
search: event.detail.value?.trim(),
})
this.isSearching = false
}, 1000),
)
}
this.$watch('state', async () => {
this.refreshPlaceholder()
if (this.isStateBeingUpdated) {
return
}
const choices = await this.getChoices({
withInitialOptions: !hasDynamicOptions,
})
this.isSearching = false
}, 1000))
}
this.select.clearStore()
this.$watch('state', async () => {
this.refreshPlaceholder()
this.setChoices(choices)
if (this.isStateBeingUpdated) {
return
}
const choices = await this.getChoices({
withInitialOptions: ! hasDynamicOptions,
if (![null, undefined, ''].includes(this.state)) {
this.select.setChoiceByValue(
this.formatState(this.state),
)
}
})
},
this.select.clearStore()
refreshChoices: async function (config = {}) {
this.setChoices(await this.getChoices(config))
},
this.setChoices(choices)
setChoices: function (choices) {
this.select.setChoices(choices, 'value', 'label', true)
},
if (! [null, undefined, ''].includes(this.state)) {
this.select.setChoiceByValue(this.formatState(this.state))
getChoices: async function (config = {}) {
const options = await this.getOptions(config)
return this.transformOptionsIntoChoices({
...options,
...(await this.getMissingOptions(options)),
})
},
getOptions: async function ({ search, withInitialOptions }) {
if (withInitialOptions) {
return options
}
})
},
refreshChoices: async function (config = {}) {
this.setChoices(await this.getChoices(config))
},
if (
search !== '' &&
search !== null &&
search !== undefined
) {
return await getSearchResultsUsing(search)
}
setChoices: function (choices) {
this.select.setChoices(
choices,
'value',
'label',
true,
)
},
return await getOptionsUsing()
},
getChoices: async function (config = {}) {
const options = await this.getOptions(config)
return this.transformOptionsIntoChoices({
...options,
...await this.getMissingOptions(options),
})
},
getOptions: async function ({ search, withInitialOptions }) {
if (withInitialOptions) {
return options
}
if ((search !== '') && (search !== null) && (search !== undefined)) {
return await getSearchResultsUsing(search)
}
return await getOptionsUsing()
},
transformOptionsIntoChoices: function (options) {
return Object.entries(options)
.map(([value, label]) => ({
transformOptionsIntoChoices: function (options) {
return Object.entries(options).map(([value, label]) => ({
label,
value,
}))
},
},
refreshPlaceholder: function () {
if (isMultiple) {
return
}
refreshPlaceholder: function () {
if (isMultiple) {
return
}
this.select._renderItems()
this.select._renderItems()
if (! [null, undefined, ''].includes(this.state)) {
return
}
if (![null, undefined, ''].includes(this.state)) {
return
}
this.$el.querySelector('.choices__list--single').innerHTML = `<div class="choices__placeholder choices__item">${placeholder}</div>`
},
this.$el.querySelector(
'.choices__list--single',
).innerHTML = `<div class="choices__placeholder choices__item">${placeholder}</div>`
},
formatState: function (state) {
if (isMultiple) {
return (state ?? []).map((item) => item?.toString())
}
formatState: function (state) {
if (isMultiple) {
return (state ?? []).map((item) => item?.toString())
}
return state?.toString()
},
return state?.toString()
},
getMissingOptions: async function (options) {
if ([null, undefined, '', [], {}].includes(this.state)) {
return {}
}
if (! options.length) {
options = {}
}
if (isMultiple) {
if (this.state.every((value) => value in options)) {
getMissingOptions: async function (options) {
if ([null, undefined, '', [], {}].includes(this.state)) {
return {}
}
return await getOptionLabelsUsing()
}
if (!options.length) {
options = {}
}
if (this.state in options) {
return options
}
if (isMultiple) {
if (this.state.every((value) => value in options)) {
return {}
}
let missingOptions = {}
missingOptions[this.state] = await getOptionLabelUsing()
return missingOptions
},
}
})
return await getOptionLabelsUsing()
}
if (this.state in options) {
return options
}
let missingOptions = {}
missingOptions[this.state] = await getOptionLabelUsing()
return missingOptions
},
}
},
)
}
@@ -1,9 +1,7 @@
import '../../css/components/tags-input.css'
export default (Alpine) => {
Alpine.data('tagsInputFormComponent', ({
state,
}) => {
Alpine.data('tagsInputFormComponent', ({ state }) => {
return {
newTag: '',
@@ -1,17 +1,16 @@
import IMask from 'imask'
export default (Alpine) => {
Alpine.data('textInputFormComponent', ({
getMaskOptionsUsing,
state,
}) => {
Alpine.data('textInputFormComponent', ({ getMaskOptionsUsing, state }) => {
return {
isStateBeingUpdated: false,
mask: null,
state,
init: function () {
if (! getMaskOptionsUsing) {
if (!getMaskOptionsUsing) {
return
}
@@ -19,14 +18,25 @@ export default (Alpine) => {
this.$el.value = this.state?.valueOf()
}
this.mask = IMask(this.$el, getMaskOptionsUsing(IMask)).on('accept', () => {
this.state = this.mask.unmaskedValue
})
this.mask = IMask(this.$el, getMaskOptionsUsing(IMask)).on(
'accept',
() => {
this.isStateBeingUpdated = true
this.state = this.mask.unmaskedValue
this.$nextTick(() => (this.isStateBeingUpdated = false))
},
)
this.$watch('state', () => {
if (this.isStateBeingUpdated) {
return
}
this.mask.unmaskedValue = this.state?.valueOf() ?? ''
})
}
},
}
})
}
@@ -9,7 +9,7 @@ export default (Alpine) => {
render: function () {
if (this.$el.scrollHeight > 0) {
this.$el.style.height = '150px'
this.$el.style.height = (this.$el.scrollHeight + 2) + 'px'
this.$el.style.height = this.$el.scrollHeight + 2 + 'px'
}
},
}))
+9 -4
View File
@@ -1,4 +1,3 @@
import './sortable'
import ColorPickerFormComponentAlpinePlugin from './components/color-picker'
import DateTimePickerFormComponentAlpinePlugin from './components/date-time-picker'
import FileUploadFormComponentAlpinePlugin from './components/file-upload'
@@ -7,8 +6,10 @@ import MarkdownEditorFormComponentAlpinePlugin from './components/markdown-edito
import RichEditorFormComponentAlpinePlugin from './components/rich-editor'
import SelectFormComponentAlpinePlugin from './components/select'
import TagsInputFormComponentAlpinePlugin from './components/tags-input'
import TextareaFormComponentAlpinePlugin from './components/textarea'
import TextInputFormComponentAlpinePlugin from './components/text-input'
import TextareaFormComponentAlpinePlugin from './components/textarea'
import SortableAlpinePlugin from './sortable'
import AlpineFloatingUI from '@awcodes/alpine-floating-ui'
export default (Alpine) => {
Alpine.plugin(ColorPickerFormComponentAlpinePlugin)
@@ -19,8 +20,10 @@ export default (Alpine) => {
Alpine.plugin(RichEditorFormComponentAlpinePlugin)
Alpine.plugin(SelectFormComponentAlpinePlugin)
Alpine.plugin(TagsInputFormComponentAlpinePlugin)
Alpine.plugin(TextareaFormComponentAlpinePlugin)
Alpine.plugin(TextInputFormComponentAlpinePlugin)
Alpine.plugin(TextareaFormComponentAlpinePlugin)
Alpine.plugin(SortableAlpinePlugin)
Alpine.plugin(AlpineFloatingUI)
}
export {
@@ -32,6 +35,8 @@ export {
RichEditorFormComponentAlpinePlugin,
SelectFormComponentAlpinePlugin,
TagsInputFormComponentAlpinePlugin,
TextareaFormComponentAlpinePlugin,
TextInputFormComponentAlpinePlugin,
TextareaFormComponentAlpinePlugin,
SortableAlpinePlugin,
AlpineFloatingUI,
}
+14 -4
View File
@@ -1,11 +1,21 @@
import Sortable from 'sortablejs';
import Sortable from 'sortablejs'
window.Sortable = Sortable;
window.Sortable = Sortable
window.Livewire.directive('sortable', (el) => {
el.sortable = Sortable.create(el, {
draggable: '[wire\\:sortable\\.item]',
handle: '[wire\\:sortable\\.handle]',
dataIdAttr: 'wire:sortable.item',
});
});
})
})
export default (Alpine) => {
Alpine.directive('sortable', (el) => {
el.sortable = Sortable.create(el, {
draggable: '[x-sortable-item]',
handle: '[x-sortable-handle]',
dataIdAttr: 'x-sortable-item',
})
})
}
@@ -2,10 +2,6 @@
return [
'file_upload' => [
'filepond_locale' => 'da-dk',
],
'rich_editor' => [
'dialogs' => [
@@ -103,6 +103,10 @@ return [
'label' => 'Borrar',
],
'clone_item' => [
'label' => 'Clonar',
],
'move_item_down' => [
'label' => 'Bajar',
],
@@ -2,10 +2,6 @@
return [
'file_upload' => [
'filepond_locale' => 'fa_ir',
],
'builder' => [
'collapsed' => 'محتوا جمع شد',
@@ -4,16 +4,14 @@ return [
'builder' => [
'collapsed' => 'Kandungan ditutup',
'buttons' => [
'create_item' => [
'label' => 'Tambah ke :label',
],
'create_item_between' => [
'label' => 'Tambah antara',
],
'delete_item' => [
'label' => 'Padam',
],
@@ -26,6 +24,22 @@ return [
'label' => 'Pindah ke atas',
],
'collapse_item' => [
'label' => 'Tutup',
],
'expand_item' => [
'label' => 'Buka',
],
'collapse_all' => [
'label' => 'Tutup semua',
],
'expand_all' => [
'label' => 'Buka semua',
],
],
],
@@ -77,6 +91,8 @@ return [
'repeater' => [
'collapsed' => 'Kandungan ditutup',
'buttons' => [
'create_item' => [
@@ -87,6 +103,10 @@ return [
'label' => 'Padam',
],
'clone_item' => [
'label' => 'Klon',
],
'move_item_down' => [
'label' => 'Pindah ke atas',
],
@@ -95,6 +115,22 @@ return [
'label' => 'Pindah ke bawah',
],
'collapse_item' => [
'label' => 'Tutup',
],
'expand_item' => [
'label' => 'Buka',
],
'collapse_all' => [
'label' => 'Tutup semua',
],
'expand_all' => [
'label' => 'Buka semua',
],
],
],
@@ -138,13 +174,64 @@ return [
],
'select' => [
'actions' => [
'create_option' => [
'modal' => [
'heading' => 'Cipta',
'actions' => [
'create' => [
'label' => 'Cipta',
],
],
],
],
],
'boolean' => [
'true' => 'Ya',
'false' => 'Tidak',
],
'loading_message' => 'Memuatkan...',
'no_search_results_message' => 'Tiada pilihan yang sepadan dengan carian anda.',
'placeholder' => 'Pilih satu pilihan',
'searching_message' => 'Mencari...',
'search_prompt' => 'Mula menaip untuk mencari...',
],
'tags_input' => [
'placeholder' => 'Tag baru',
],
'wizard' => [
'buttons' => [
'previous_step' => [
'label' => 'Sebelum',
],
'next_step' => [
'label' => 'Seterus',
],
],
],
];
@@ -61,7 +61,7 @@ return [
'fields' => [
'key' => [
'label' => 'Key',
'label' => 'Khóa',
],
'value' => [
@@ -79,7 +79,7 @@ return [
'bold' => 'In đậm',
'bullet_list' => 'Danh sách dùng dấu chấm',
'code_block' => 'Code block',
'edit' => 'Xóa',
'edit' => 'Sửa',
'italic' => 'In nghiêng',
'link' => 'Liên kết',
'ordered_list' => 'Danh sách đánh số',
@@ -151,7 +151,7 @@ return [
],
'toolbar_buttons' => [
'attach_files' => 'Đính kèm file',
'attach_files' => 'Đính kèm tệp',
'blockquote' => 'Trích dẫn',
'bold' => 'In đậm',
'bullet_list' => 'Danh sách dùng dấu chấm',
@@ -162,8 +162,8 @@ return [
'italic' => 'In nghiêng',
'link' => 'Liên kết',
'ordered_list' => 'Danh sách đánh số',
'redo' => 'Redo',
'strike' => 'Strikethrough',
'redo' => 'Làm lại',
'strike' => 'Gạch ngang',
'undo' => 'Hoàn tác',
],
@@ -15,7 +15,10 @@
<x-forms::button
:form="$getForm()"
:type="$canSubmitForm() ? 'submit' : 'button'"
:tag="$action->getUrl() ? 'a' : 'button'"
:wire:click="$wireClickAction"
:href="$action->isEnabled() ? $action->getUrl() : null"
:target="$action->shouldOpenUrlInNewTab() ? '_blank' : null"
:x-on:click="$canCancelAction() ? 'isOpen = false' : null"
:color="$getColor()"
:outlined="$isOutlined()"
@@ -3,7 +3,22 @@
@endphp
<form wire:submit.prevent="callMountedFormComponentAction">
<x-forms::modal :id="\Illuminate\Support\Str::of(static::class)->replace('\\', '\\\\') . '-form-component-action'" :visible="filled($action)" :width="$action?->getModalWidth()" display-classes="block">
<x-forms::modal
:id="$this->id . '-form-component-action'"
:wire:key="$action ? $this->id . '.' . $action->getComponent()->getStatePath() . '.actions.' . $action->getName() . '.modal' : null"
x-init="
$watch('isOpen', () => {
if (isOpen) {
return
}
$wire.mountedFormComponentAction = null
})
"
:visible="filled($action)"
:width="$action?->getModalWidth()"
display-classes="block"
>
@if ($action)
@if ($action->isModalCentered())
<x-slot name="heading">
@@ -20,6 +35,12 @@
<x-forms::modal.heading>
{{ $action->getModalHeading() }}
</x-forms::modal.heading>
@if ($subheading = $action->getModalSubheading())
<x-forms::modal.subheading>
{{ $subheading }}
</x-forms::modal.subheading>
@endif
</x-slot>
@endif
@@ -1,13 +1,3 @@
@once
@push('scripts')
@php
$locale = strtolower(str_replace('_', '-', app()->getLocale()));
@endphp
<script defer src="//unpkg.com/dayjs@1.10.4/locale/{{ $locale }}.js" onload="dayjs.updateLocale('{{ $locale }}')"></script>
@endpush
@endonce
<x-dynamic-component
:component="$getFieldWrapperView()"
:id="$getId()"
@@ -25,6 +15,7 @@
firstDayOfWeek: {{ $getFirstDayOfWeek() }},
isAutofocused: @js($isAutofocused()),
isDisabled: @js($isDisabled()),
locale: @js(app()->getLocale()),
shouldCloseOnDateSelection: @js($shouldCloseOnDateSelection()),
state: $wire.{{ $applyStateBindingModifiers('entangle(\'' . $getStatePath() . '\')') }},
})"
@@ -1,27 +1,3 @@
@once
@push('scripts')
@php
if (\Illuminate\Support\Facades\Lang::has($localeString = 'forms::components.file_upload.filepond_locale')) {
$locale = __($localeString);
} else {
$locale = strtolower(str_replace('_', '-', app()->getLocale()));
if (! str_contains($locale, '-')) {
$locale .= '-' . $locale;
}
}
$defaultLocaleData = ($placeholder = $getPlaceholder()) ? "{ labelIdle: '{$placeholder}' }" : '{}';
@endphp
<script type="module">
import localeData from 'https://cdn.skypack.dev/filepond/locale/{{$locale}}.js'
window.FilePond && window.FilePond.setOptions({ ...localeData, ...{!! $defaultLocaleData !!} })
</script>
@endpush
@endonce
<x-dynamic-component
:component="$getFieldWrapperView()"
:id="$getId()"
@@ -60,9 +36,10 @@
imageResizeTargetWidth: {{ $imageResizeTargetWidth ? "'{$imageResizeTargetWidth}'" : 'null' }},
isAvatar: {{ $isAvatar() ? 'true' : 'false' }},
loadingIndicatorPosition: '{{ $getLoadingIndicatorPosition() }}',
locale: @js(app()->getLocale()),
panelAspectRatio: {{ ($aspectRatio = $getPanelAspectRatio()) ? "'{$aspectRatio}'" : 'null' }},
panelLayout: {{ ($layout = $getPanelLayout()) ? "'{$layout}'" : 'null' }},
placeholder: {{ ($placeholder = $getPlaceholder()) ? "'{$placeholder}'" : 'null' }},
placeholder: @js($getPlaceholder()),
maxSize: {{ ($size = $getMaxSize()) ? "'{$size} KB'" : 'null' }},
minSize: {{ ($size = $getMinSize()) ? "'{$size} KB'" : 'null' }},
removeUploadedFileUsing: async (fileKey) => {
@@ -1 +1,3 @@
{{ $getChildComponentContainer() }}
<div {{ $attributes->merge($getExtraAttributes()) }}>
{{ $getChildComponentContainer() }}
</div>
@@ -1 +1,3 @@
{{ $getChildComponentContainer() }}
<div {{ $attributes->merge($getExtraAttributes()) }}>
{{ $getChildComponentContainer() }}
</div>
@@ -43,17 +43,23 @@
{{ $getValueLabel() }}
</th>
@if ($canDeleteRows() && $getDeleteButtonLabel() && $isEnabled())
<th class="w-12" scope="col" x-show="rows.length > 1">
<span class="sr-only">
{{ $getDeleteButtonLabel() }}
</span>
@if (($canDeleteRows() || $isReorderable()) && $isEnabled())
<th
scope="col"
x-show="rows.length > 1"
class="{{ ($canDeleteRows() && $isReorderable()) ? 'w-16' : 'w-12' }}"
>
<span class="sr-only"></span>
</th>
@endif
</tr>
</thead>
<tbody
@if ($isReorderable())
x-sortable
x-on:end="reorderRows($event)"
@endif
x-ref="tableBody"
@class([
'divide-y whitespace-nowrap',
@@ -61,10 +67,15 @@
])
>
<template x-for="(row, index) in rows" x-bind:key="index" x-ref="rowTemplate">
<tr @class([
'divide-x',
'dark:divide-gray-600' => config('forms.dark_mode'),
])>
<tr
@if ($isReorderable())
x-bind:x-sortable-item="row.key"
@endif
@class([
'divide-x',
'dark:divide-gray-600' => config('forms.dark_mode'),
])
>
<td>
<input
type="text"
@@ -91,16 +102,36 @@
>
</td>
@if ($canDeleteRows() && $isEnabled())
@if (($canDeleteRows() || $isReorderable()) && $isEnabled())
<td x-show="rows.length > 1" class="whitespace-nowrap">
<div class="flex items-center justify-center">
<button
x-on:click="deleteRow(index)"
type="button"
class="text-danger-600 hover:text-danger-700"
>
<x-heroicon-o-trash class="w-4 h-4" />
</button>
<div class="flex items-center justify-center gap-2">
@if ($isReorderable())
<button
x-sortable-handle
type="button"
class="text-gray-600 hover:text-gray-700"
>
<x-heroicon-o-switch-vertical class="w-4 h-4" />
<span class="sr-only">
{{ $getDeleteButtonLabel() }}
</span>
</button>
@endif
@if ($canDeleteRows())
<button
x-on:click="deleteRow(index)"
type="button"
class="text-danger-600 hover:text-danger-700"
>
<x-heroicon-o-trash class="w-4 h-4" />
<span class="sr-only">
{{ $getDeleteButtonLabel() }}
</span>
</button>
@endif
</div>
</td>
@endif
@@ -217,7 +217,7 @@
{!! $isRequired() ? 'required' : null !!}
@endif
@class([
'tracking-normal whitespace-pre-wrap overflow-y-hidden font-mono block absolute bg-transparent top-0 text-sm left-0 block z-1 w-full h-full min-h-full resize-none transition duration-75 rounded-lg shadow-sm focus:border-primary-600 focus:ring-1 focus:ring-inset focus:ring-primary-600 caret-black',
'tracking-normal overflow-y-hidden font-mono block absolute bg-transparent top-0 text-sm left-0 block z-1 w-full h-full min-h-full resize-none transition duration-75 rounded-lg shadow-sm focus:border-primary-600 focus:ring-1 focus:ring-inset focus:ring-primary-600 caret-black',
'dark:caret-white dark:focus:border-primary-600' => config('forms.dark_mode'),
'border-gray-300' => ! $errors->has($getStatePath()),
'dark:border-gray-600' => (! $errors->has($getStatePath())) && config('forms.dark_mode'),
@@ -231,14 +231,14 @@
x-html="overlay"
style="min-height: 150px;"
@class([
'w-full h-full rounded-lg px-3 py-2 border border-transparent font-mono tracking-normal bg-white text-sm text-gray-900 break-words whitespace-pre-wrap',
'w-full h-full rounded-lg px-3 py-2 border border-transparent font-mono tracking-normal bg-white text-sm text-gray-900 break-words',
'dark:bg-gray-700 dark:border-gray-600 dark:text-white' => config('forms.dark_mode'),
])
></div>
</div>
<div @class([
'prose max-w-none block w-full h-full min-h-full px-3 py-2 bg-white border border-gray-300 rounded-lg shadow-sm focus:border-primary-300',
'prose max-w-none block w-full h-full min-h-full px-3 py-2 bg-white border border-gray-300 rounded-lg shadow-sm break-words focus:border-primary-300',
'dark:prose-invert dark:border-gray-600 dark:bg-gray-700' => config('forms.dark_mode'),
]) x-show="tab === 'preview'" x-html="preview" x-cloak style="min-height: 150px;"></div>
</div>
@@ -40,6 +40,15 @@
x-on:keydown.enter.stop.prevent="createTag()"
x-on:keydown.,.stop.prevent="createTag()"
x-on:blur="createTag()"
x-on:paste="$nextTick(() => {
if (newTag.includes(',')) {
newTag.split(',').forEach((tag) => {
newTag = tag
createTag()
})
}
})"
x-model="newTag"
{{ $getExtraInputAttributeBag()->class([
'webkit-calendar-picker-indicator:opacity-0 block w-full border-0',
@@ -42,11 +42,7 @@
@else
x-data="textInputFormComponent({
{{ $hasMask() ? "getMaskOptionsUsing: (IMask) => ({$getJsonMaskConfiguration()})," : null }}
state: $wire.{{
$isLazy() ?
'entangle(\'' . $getStatePath() . '\').defer' :
$applyStateBindingModifiers('entangle(\'' . $getStatePath() . '\')')
}},
state: $wire.{{ $applyStateBindingModifiers('entangle(\'' . $getStatePath() . '\')') }},
})"
type="text"
wire:ignore
@@ -19,8 +19,20 @@
x-bind:aria-checked="state.toString()"
x-on:click="state = ! state"
x-bind:class="{
'bg-primary-600': state,
'bg-gray-200 @if (config('forms.dark_mode')) dark:bg-white/10 @endif': ! state,
'{{ match ($getOnColor()) {
'danger' => 'bg-danger-500',
'secondary' => 'bg-gray-500',
'success' => 'bg-success-500',
'warning' => 'bg-warning-500',
default => 'bg-primary-600',
} }}': state,
'{{ match ($getOffColor()) {
'danger' => 'bg-danger-500',
'primary' => 'bg-primary-500',
'success' => 'bg-success-500',
'warning' => 'bg-warning-500',
default => 'bg-gray-200',
} }} @if (config('forms.dark_mode')) dark:bg-white/10 @endif': ! state,
}"
{!! $isAutofocused() ? 'autofocus' : null !!}
{!! $isDisabled() ? 'disabled' : null !!}
@@ -41,6 +41,8 @@ class BaseFileUpload extends Field
protected bool | Closure $shouldPreserveFilenames = false;
protected string | Closure | null $fileNamesStatePath = null;
protected string | Closure $visibility = 'public';
protected ?Closure $deleteUploadedFileUsing = null;
@@ -73,6 +75,10 @@ class BaseFileUpload extends Field
});
$this->afterStateUpdated(static function (BaseFileUpload $component, $state) {
if ($state instanceof TemporaryUploadedFile) {
return;
}
if (blank($state)) {
return;
}
@@ -203,6 +209,13 @@ class BaseFileUpload extends Field
return $this;
}
public function storeFileNamesIn(string | Closure | null $statePath): static
{
$this->fileNamesStatePath = $statePath;
return $this;
}
public function preserveFilenames(bool | Closure $condition = true): static
{
$this->shouldPreserveFilenames = $condition;
@@ -358,6 +371,15 @@ class BaseFileUpload extends Field
return $this->evaluate($this->shouldPreserveFilenames);
}
public function getFileNamesStatePath(): ?string
{
if (! $this->fileNamesStatePath) {
return null;
}
return $this->generateRelativeStatePath($this->fileNamesStatePath);
}
public function getValidationRules(): array
{
$rules = [
@@ -424,7 +446,9 @@ class BaseFileUpload extends Field
return null;
}
if ($file instanceof TemporaryUploadedFile) {
if (is_string($file)) {
$this->removeStoredFileName($file);
} elseif ($file instanceof TemporaryUploadedFile) {
$file->delete();
}
@@ -435,6 +459,31 @@ class BaseFileUpload extends Field
return $file;
}
public function removeStoredFileName(string $file): void
{
$statePath = $this->fileNamesStatePath;
if (blank($statePath)) {
return;
}
$this->evaluate(function (BaseFileUpload $component, Closure $get, Closure $set) use ($file, $statePath) {
if (! $component->isMultiple()) {
$set($statePath, null);
return;
}
$fileNames = $get($statePath) ?? [];
if (array_key_exists($file, $fileNames)) {
unset($fileNames[$file]);
}
$set($statePath, $fileNames);
});
}
public function reorderUploadedFiles(array $fileKeys): void
{
if (! $this->canReorder) {
@@ -483,10 +532,6 @@ class BaseFileUpload extends Field
return;
}
if (! is_array($this->getState())) {
$this->state([$this->getState()]);
}
$state = array_map(function (TemporaryUploadedFile | string $file) {
if (! $file instanceof TemporaryUploadedFile) {
return $file;
@@ -504,10 +549,12 @@ class BaseFileUpload extends Field
'file' => $file,
]);
$this->storeFileName($storedFile, $file->getClientOriginalName());
$file->delete();
return $storedFile;
}, $this->getState());
}, Arr::wrap($this->getState()));
if ($this->canReorder && ($callback = $this->reorderUploadedFilesUsing)) {
$state = $this->evaluate($callback, [
@@ -518,6 +565,44 @@ class BaseFileUpload extends Field
$this->state($state);
}
public function storeFileName(string $file, string $fileName): void
{
$statePath = $this->fileNamesStatePath;
if (blank($statePath)) {
return;
}
$this->evaluate(function (BaseFileUpload $component, Closure $get, Closure $set) use ($file, $fileName, $statePath) {
if (! $component->isMultiple()) {
$set($statePath, $fileName);
return;
}
$fileNames = $get($statePath) ?? [];
$fileNames[$file] = $fileName;
$set($statePath, $fileNames);
});
}
public function getStoredFileNames(): string | array | null
{
$state = null;
$statePath = $this->fileNamesStatePath;
if (filled($statePath)) {
$state = $this->evaluate(fn (Closure $get) => $get($statePath));
}
if (blank($state) && $this->isMultiple()) {
return [];
}
return $state;
}
public function isMultiple(): bool
{
return $this->evaluate($this->isMultiple);
@@ -536,4 +621,26 @@ class BaseFileUpload extends Field
'file' => $file,
]);
}
public function getStateToDehydrate(): array
{
$state = parent::getStateToDehydrate();
if ($fileNamesStatePath = $this->getFileNamesStatePath()) {
$state = array_merge($state, [
$fileNamesStatePath => $this->getStoredFileNames(),
]);
}
return $state;
}
public function dehydrateValidationRules(array &$rules): void
{
parent::dehydrateValidationRules($rules);
if ($fileNamesStatePath = $this->getFileNamesStatePath()) {
$rules[$fileNamesStatePath] = ['nullable'];
}
}
}
@@ -31,7 +31,7 @@ trait BelongsToModel
$this->evaluate($callback);
}
public function loadStateFromRelationships(): void
public function loadStateFromRelationships(bool $andHydrate = false): void
{
$callback = $this->loadStateFromRelationshipsUsing;
@@ -39,7 +39,21 @@ trait BelongsToModel
return;
}
if (! $this->getRecord()?->exists) {
return;
}
$this->evaluate($callback);
if ($andHydrate) {
$this->callAfterStateHydrated();
foreach ($this->getChildComponentContainers() as $container) {
$container->callAfterStateHydrated();
}
$this->fillStateWithNull();
}
}
public function saveRelationshipsUsing(?Closure $callback): static
@@ -28,7 +28,7 @@ trait CanBeLengthConstrained
}
return "size:{$length}";
});
}, static fn (Contracts\CanBeLengthConstrained $component): bool => filled($component->getLength()));
return $this;
}
@@ -40,8 +40,12 @@ trait CanBeLengthConstrained
$this->rule(static function (Contracts\CanBeLengthConstrained $component): string {
$length = $component->getMaxLength();
if ($component instanceof Contracts\CanHaveNumericState && $component->isNumeric()) {
return "max_digits:{$length}";
}
return "max:{$length}";
});
}, static fn (Contracts\CanBeLengthConstrained $component): bool => filled($component->getMaxLength()));
return $this;
}
@@ -53,8 +57,12 @@ trait CanBeLengthConstrained
$this->rule(static function (Contracts\CanBeLengthConstrained $component): string {
$length = $component->getMinLength();
if ($component instanceof Contracts\CanHaveNumericState && $component->isNumeric()) {
return "min_digits:{$length}";
}
return "min:{$length}";
});
}, static fn (Contracts\CanBeLengthConstrained $component): bool => filled($component->getMinLength()));
return $this;
}
@@ -208,6 +208,18 @@ trait CanBeValidated
return $rules;
}
public function dehydrateValidationRules(array &$rules): void
{
if (count($componentRules = $this->getValidationRules())) {
$rules[$this->getStatePath()] = $componentRules;
}
}
public function dehydrateValidationAttributes(array &$attributes): void
{
$attributes[$this->getStatePath()] = $this->getValidationAttribute();
}
public function isRequired(): bool
{
return (bool) $this->evaluate($this->isRequired);
@@ -55,6 +55,7 @@ trait EntanglesStateWithSingularRelationship
if ($relationship instanceof BelongsTo) {
$relationship->associate($record->create($state));
$relationship->getParent()->save();
} else {
$record->fill($state);
$relationship->save($record);
@@ -83,7 +84,7 @@ trait EntanglesStateWithSingularRelationship
protected function getStateFromRelatedRecord(Model $record): array
{
$state = $record->toArray();
$state = $record->attributesToArray();
if (
($activeLocale = $this->getLivewire()->getActiveFormLocale()) &&
@@ -4,6 +4,8 @@ namespace Filament\Forms\Components\Concerns;
use Closure;
use Filament\Forms\Components\Component;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Livewire\Livewire;
@@ -92,13 +94,36 @@ trait HasState
return $this;
}
public function dehydrateState()
public function getStateToDehydrate(): array
{
if ($callback = $this->dehydrateStateUsing) {
return $this->evaluate($callback);
return [$this->getStatePath() => $this->evaluate($callback)];
}
return $this->getState();
return [$this->getStatePath() => $this->getState()];
}
public function dehydrateState(array &$state): void
{
if (! $this->isDehydrated()) {
Arr::forget($state, $this->getStatePath());
return;
}
if ($this->getStatePath(isAbsolute: false)) {
foreach ($this->getStateToDehydrate() as $key => $value) {
Arr::set($state, $key, $value);
}
}
foreach ($this->getChildComponentContainers() as $container) {
if ($container->isHidden()) {
continue;
}
$container->dehydrateState($state);
}
}
public function dehydrateStateUsing(?Closure $callback): static
@@ -108,29 +133,68 @@ trait HasState
return $this;
}
public function hydrateDefaultState(): static
public function hydrateState(?array &$hydratedDefaultState): void
{
if (! $this->hasDefaultState()) {
return $this;
$this->hydrateDefaultState($hydratedDefaultState);
foreach ($this->getChildComponentContainers(withHidden: true) as $container) {
$container->hydrateState($hydratedDefaultState);
}
$this->callAfterStateHydrated();
}
public function fill(): void
{
$defaults = [];
$this->hydrateDefaultState($defaults);
}
public function hydrateDefaultState(?array &$hydratedDefaultState): void
{
if ($hydratedDefaultState === null) {
$this->loadStateFromRelationships();
$state = $this->getState();
// Hydrate all arrayable state objects as arrays by converting
// them to collections, then using `toArray()`.
if (is_array($state) || $state instanceof Arrayable) {
$this->state(collect($state)->toArray());
}
return;
}
$statePath = $this->getStatePath();
if (Arr::has($hydratedDefaultState, $statePath)) {
return;
}
if (! $this->hasDefaultState()) {
$this->state(null);
return;
}
$defaultState = $this->getDefaultState();
$this->state($this->getDefaultState());
return $this;
Arr::set($hydratedDefaultState, $statePath, $defaultState);
}
public function fillStateWithNull(bool $shouldOverwrite = true): static
public function fillStateWithNull(): void
{
$livewire = $this->getLivewire();
if (! Arr::has((array) $this->getLivewire(), $this->getStatePath())) {
$this->state(null);
}
data_set(
$livewire,
$this->getStatePath(),
null,
$shouldOverwrite,
);
return $this;
foreach ($this->getChildComponentContainers(withHidden: true) as $container) {
$container->fillStateWithNull();
}
}
public function mutateDehydratedState($state)
@@ -236,7 +300,7 @@ trait HasState
return data_get(
$livewire,
$this->generateStatePathForCallback($path, $isAbsolute)
$this->generateRelativeStatePath($path, $isAbsolute)
);
};
}
@@ -248,7 +312,7 @@ trait HasState
data_set(
$livewire,
$this->generateStatePathForCallback($path, $isAbsolute),
$this->generateRelativeStatePath($path, $isAbsolute),
$this->evaluate($state),
);
@@ -256,7 +320,7 @@ trait HasState
};
}
protected function generateStatePathForCallback(string | Component $path, bool $isAbsolute = false): string
protected function generateRelativeStatePath(string | Component $path, bool $isAbsolute = false): string
{
if ($path instanceof Component) {
return $path->getStatePath();
@@ -4,9 +4,7 @@ namespace Filament\Forms\Components\Contracts;
interface HasValidationRules
{
public function getStatePath(): string;
public function dehydrateValidationAttributes(array &$attributes): void;
public function getValidationAttribute(): string;
public function getValidationRules(): array;
public function dehydrateValidationRules(array &$rules): void;
}
@@ -31,6 +31,8 @@ class KeyValue extends Field
protected string | Closure | null $valuePlaceholder = null;
protected bool | Closure $isReorderable = false;
protected function setUp(): void
{
parent::setUp();
@@ -123,6 +125,13 @@ class KeyValue extends Field
return $this;
}
public function reorderable(bool | Closure $condition = true): static
{
$this->isReorderable = $condition;
return $this;
}
public function canAddRows(): bool
{
return ! $this->evaluate($this->shouldDisableAddingRows);
@@ -172,4 +181,9 @@ class KeyValue extends Field
{
return $this->evaluate($this->valuePlaceholder);
}
public function isReorderable(): bool
{
return $this->evaluate($this->isReorderable);
}
}
+1 -1
View File
@@ -406,7 +406,7 @@ class Repeater extends Field
return $records
->map(function (Model $record) use ($activeLocale): array {
$state = $record->toArray();
$state = $record->attributesToArray();
if ($activeLocale && method_exists($record, 'getTranslatableAttributes') && method_exists($record, 'getTranslation')) {
foreach ($record->getTranslatableAttributes() as $attribute) {
+3 -2
View File
@@ -20,6 +20,7 @@ use Illuminate\Validation\Rules\Exists;
class Select extends Field
{
use Concerns\HasAffixes {
getActions as getBaseActions;
getSuffixAction as getBaseSuffixAction;
}
use Concerns\HasExtraInputAttributes;
@@ -170,7 +171,7 @@ class Select extends Field
public function getActions(): array
{
$actions = parent::getActions();
$actions = $this->getBaseActions();
$createOptionActionName = $this->getCreateOptionActionName();
@@ -254,7 +255,7 @@ class Select extends Field
if ($this->modifyCreateOptionActionUsing) {
$action = $this->evaluate($this->modifyCreateOptionActionUsing, [
'action' => $action,
]);
]) ?? $action;
}
return $action;
+2 -2
View File
@@ -98,7 +98,7 @@ class TextInput extends Field implements Contracts\CanBeLengthConstrained, CanHa
$value = $component->getMaxValue();
return "max:{$value}";
});
}, static fn (TextInput $component): bool => filled($component->getMaxValue()));
return $this;
}
@@ -111,7 +111,7 @@ class TextInput extends Field implements Contracts\CanBeLengthConstrained, CanHa
$value = $component->getMinValue();
return "min:{$value}";
});
}, static fn (TextInput $component): bool => filled($component->getMinValue()));
return $this;
}

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