mirror of
https://github.com/symbiote/silverstripe-gridfieldextensions.git
synced 2024-10-22 17:05:39 +02:00
Compare commits
268 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
d851ce562c | ||
|
018ab87515 | ||
|
d34c92dba7 | ||
|
c84d8afa7a | ||
|
78fa8ba747 | ||
|
62734535a0 | ||
|
5ed8c95fda | ||
|
377dc1ef93 | ||
|
1637e78ea1 | ||
|
9c70b0175b | ||
|
e7d7178719 | ||
|
1e5a1e8056 | ||
|
47171ee4c2 | ||
|
a2ea473369 | ||
|
829cacff0b | ||
|
1284046e10 | ||
|
665d23170b | ||
|
f2adec35d3 | ||
|
504b3bd822 | ||
|
89a7301b67 | ||
|
6d46483e32 | ||
|
bb6c0396f1 | ||
|
bd783a4a24 | ||
|
17f92d08ba | ||
|
49a607d747 | ||
|
5566b05c9d | ||
|
b0addcb5bd | ||
|
d5d438c5cc | ||
|
e9f202f003 | ||
|
aa6ac7a1a2 | ||
|
23a5154c97 | ||
|
7a8f244df0 | ||
|
378b3af799 | ||
|
77e811459e | ||
|
aec306bae2 | ||
|
4d99f83bbf | ||
|
2cfe6db9a7 | ||
|
f56bf67e40 | ||
|
acacac2563 | ||
|
dc99a8e04d | ||
|
7f90426987 | ||
|
c6dfea0598 | ||
|
cfc764e3b5 | ||
|
1574be36db | ||
|
0405d919fc | ||
|
547ec8aa50 | ||
|
71d60b0734 | ||
|
93e4379c3d | ||
|
ec93c994f8 | ||
|
8c4e924bfa | ||
|
2928504b3c | ||
|
8e77095de0 | ||
|
07c97e45b2 | ||
|
8e9ee0bace | ||
|
78da0c31fe | ||
|
607a9bcdf9 | ||
|
6d6cf4a225 | ||
|
baa6a8d147 | ||
|
5e3037bd2f | ||
|
b52fd75f0e | ||
|
7738a0b89b | ||
|
719c0ed547 | ||
|
3d4986432c | ||
|
74126ed6a7 | ||
|
6ee8d6ccb2 | ||
|
acd1a37122 | ||
|
700e5adf53 | ||
|
d6357ec187 | ||
|
3a9fd3c928 | ||
|
6becfc2f89 | ||
|
229a23a2f4 | ||
|
08f89ea4b5 | ||
|
c77d9d1de3 | ||
|
4105d6330f | ||
|
d671788b19 | ||
|
2fc085bbd0 | ||
|
306ad52a28 | ||
|
3aaf4479ce | ||
|
5024b93f1d | ||
|
f8439f1b67 | ||
|
66d5e043ed | ||
|
9944b67632 | ||
|
4d94b2748d | ||
|
4afe5dcfd3 | ||
|
04c42273cf | ||
|
8e37021317 | ||
|
a710c81941 | ||
|
d0cf736174 | ||
|
0dc0c5614d | ||
|
0b37e97b42 | ||
|
d357479421 | ||
|
668b297a30 | ||
|
f22531bf6a | ||
|
6a26d76c6f | ||
|
600a39e428 | ||
|
d97bbbddce | ||
|
088e7f16a8 | ||
|
76541b27f2 | ||
|
56d1adf64a | ||
|
3876527913 | ||
|
4263c9d970 | ||
|
6e922fcec0 | ||
|
968b807918 | ||
|
78c63c6725 | ||
|
b221134ce1 | ||
|
a88ac60033 | ||
|
a034bd5973 | ||
|
7355a4a816 | ||
|
bba8547054 | ||
|
95f0acb0f4 | ||
|
a4d752c911 | ||
|
7fdfe234fd | ||
|
838e8d765e | ||
|
b6130c4e11 | ||
|
688c29ba04 | ||
|
5b14abb221 | ||
|
939415a444 | ||
|
19b4c6a7fa | ||
|
41f6267c23 | ||
|
9fa9ef8903 | ||
|
e898711add | ||
|
ba0d23ab5e | ||
|
6b47b6d63a | ||
|
e15f1766bf | ||
|
b93a8f28ac | ||
|
9009a83ae1 | ||
|
8e24be2660 | ||
|
e7825cd0a3 | ||
|
cbffc5b935 | ||
|
8c82965e7e | ||
|
96af48a2fe | ||
|
0974e36d57 | ||
|
51dd4cab65 | ||
|
30ae7e4037 | ||
|
5945ce105f | ||
|
e20ead2624 | ||
|
e2bfcd1d9f | ||
|
b192551f30 | ||
|
ed51125b91 | ||
|
6d8b41ee5a | ||
|
37a206cf46 | ||
|
9d4c87f5c7 | ||
|
d1021ace51 | ||
|
509abf1532 | ||
|
64b6fd04f3 | ||
|
4955b9c4b9 | ||
|
6ef71280d1 | ||
|
cd1fdada5d | ||
|
24db0da047 | ||
|
b29db7a4e7 | ||
|
6bf1a05975 | ||
|
8e8c1db7bf | ||
|
e67065a780 | ||
|
af0f342c3b | ||
|
978fda31d4 | ||
|
f540f247bb | ||
|
eb9093a3ac | ||
|
0a16566471 | ||
|
8ba5c3435e | ||
|
14c0080853 | ||
|
eee01960e8 | ||
|
692b9df70c | ||
|
38c1168bea | ||
|
84241738b6 | ||
|
30a4f34fbf | ||
|
735cc01270 | ||
|
dc34dbe781 | ||
|
6bcb983605 | ||
|
6096a5ae53 | ||
|
102768763a | ||
|
18b7032211 | ||
|
ade9bb50f4 | ||
|
0203e33117 | ||
|
fdbac60300 | ||
|
4580d0b913 | ||
|
da67666c22 | ||
|
d5960b5efb | ||
|
40ed8c7f49 | ||
|
a7c5283bdd | ||
|
834547e5c0 | ||
|
931e014a0e | ||
|
091954d987 | ||
|
efdf9dcc13 | ||
|
c120dc23ce | ||
|
e98226fe3d | ||
|
ab28212d86 | ||
|
ee9bd6b234 | ||
|
ab6dfabb8a | ||
|
23c0a58e48 | ||
|
b6f03225d0 | ||
|
9d4ac960a7 | ||
|
f764303ed0 | ||
|
cb94bbe5e0 | ||
|
0ad1fc367e | ||
|
3ee4ff4cb7 | ||
|
9e26e38d75 | ||
|
fbc7278378 | ||
|
09f6cdf299 | ||
|
39dddc4c91 | ||
|
b7b3678b26 | ||
|
46b792b27f | ||
|
37b35f6a46 | ||
|
9421e85483 | ||
|
b07079d758 | ||
|
99f59c4efe | ||
|
0404b305c7 | ||
|
973f524b41 | ||
|
db29e863f7 | ||
|
04a07d505f | ||
|
da2e75f3c4 | ||
|
9ccd300629 | ||
|
730f7a13ea | ||
|
58200f847f | ||
|
a34d6242a5 | ||
|
a36c96e692 | ||
|
bb4bb62be3 | ||
|
320a6198e5 | ||
|
e14ed0b47a | ||
|
1e84c9af06 | ||
|
58604a6234 | ||
|
f80fc03bbb | ||
|
aa9bbbafdd | ||
|
719b906460 | ||
|
d6f2df0700 | ||
|
36d9711a47 | ||
|
403900f1fd | ||
|
f0d2ec7733 | ||
|
e95c92a168 | ||
|
ef354511d7 | ||
|
aaf1130569 | ||
|
57acb83546 | ||
|
08ccb15bdb | ||
|
315cb0b1c1 | ||
|
87cd8ffc6d | ||
|
997a5614fc | ||
|
7d3f5acbd7 | ||
|
5cb337df00 | ||
|
2fc4490af4 | ||
|
3ebf09a572 | ||
|
ef48ea9cdd | ||
|
1b0abe0ca6 | ||
|
ddec10c141 | ||
|
658874419a | ||
|
90b1a466c1 | ||
|
3f6036b1ae | ||
|
cc8276482f | ||
|
0795113cf4 | ||
|
2c73b802d6 | ||
|
0907b2f493 | ||
|
24057fe79a | ||
|
cbcb6e57e0 | ||
|
1b5e83b7f6 | ||
|
03b83e538a | ||
|
d81d701bab | ||
|
2acf6dee47 | ||
|
4ee047b591 | ||
|
b1c83255c0 | ||
|
3cacb43934 | ||
|
59f44984b8 | ||
|
43c1b8352c | ||
|
7ee247e6df | ||
|
1722e9b47f | ||
|
bf158022fa | ||
|
0ae237cfee | ||
|
d5d43a7f6e | ||
|
5254b1865c | ||
|
7674daf7f2 | ||
|
135b248bee |
20
.editorconfig
Normal file
20
.editorconfig
Normal file
@ -0,0 +1,20 @@
|
||||
# For more information about the properties used in
|
||||
# this file, please see the EditorConfig documentation:
|
||||
# http://editorconfig.org/
|
||||
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 4
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.{yml,js,json,css,scss,eslintrc}]
|
||||
indent_size = 2
|
||||
indent_style = space
|
1
.github/ISSUE_TEMPLATE.md
vendored
Normal file
1
.github/ISSUE_TEMPLATE.md
vendored
Normal file
@ -0,0 +1 @@
|
||||
<!-- Blank templates are for use by maintainers only! If you aren't a maintainer, please go back and pick one of the issue templates. -->
|
72
.github/ISSUE_TEMPLATE/1_bug_report.yml
vendored
Normal file
72
.github/ISSUE_TEMPLATE/1_bug_report.yml
vendored
Normal file
@ -0,0 +1,72 @@
|
||||
name: 🪳 Bug Report
|
||||
description: Tell us if something isn't working the way it's supposed to
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
We strongly encourage you to [submit a pull request](https://docs.silverstripe.org/en/contributing/code/) which fixes the issue.
|
||||
Bug reports which are accompanied with a pull request are a lot more likely to be resolved quickly.
|
||||
- type: input
|
||||
id: affected-versions
|
||||
attributes:
|
||||
label: Module version(s) affected
|
||||
description: |
|
||||
What version of _this module_ have you reproduced this bug on?
|
||||
Run `composer info` to see the specific version of each module installed in your project.
|
||||
If you don't have access to that, check inside the help menu in the bottom left of the CMS.
|
||||
placeholder: x.y.z
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Description
|
||||
description: A clear and concise description of the problem
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: how-to-reproduce
|
||||
attributes:
|
||||
label: How to reproduce
|
||||
description: |
|
||||
⚠️ This is the most important part of the report ⚠️
|
||||
Without a way to easily reproduce your issue, there is little chance we will be able to help you and work on a fix.
|
||||
- Please, take the time to show us some code and/or configuration that is needed for others to reproduce the problem easily.
|
||||
- If the bug is too complex to reproduce with some short code samples, please reproduce it in a public repository and provide a link to the repository along with steps for setting up and reproducing the bug using that repository.
|
||||
- If part of the bug includes an error or exception, please provide a full stack trace.
|
||||
- If any user interaction is required to reproduce the bug, please add an ordered list of steps that are required to reproduce it.
|
||||
- Be as clear as you can, but don't miss any steps out. Simply saying "create a page" is less useful than guiding us through the steps you're taking to create a page, for example.
|
||||
placeholder: |
|
||||
|
||||
#### Code sample
|
||||
```php
|
||||
|
||||
```
|
||||
|
||||
#### Reproduction steps
|
||||
1.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: possible-solution
|
||||
attributes:
|
||||
label: Possible Solution
|
||||
description: |
|
||||
*Optional: only if you have suggestions on a fix/reason for the bug*
|
||||
Please consider [submitting a pull request](https://docs.silverstripe.org/en/contributing/code/) with your solution! It helps get faster feedback and greatly increases the chance of the bug being fixed.
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: Additional Context
|
||||
description: "*Optional: any other context about the problem: log messages, screenshots, etc.*"
|
||||
- type: checkboxes
|
||||
id: validations
|
||||
attributes:
|
||||
label: Validations
|
||||
description: "Before submitting the issue, please make sure you do the following:"
|
||||
options:
|
||||
- label: Check that there isn't already an issue that reports the same bug
|
||||
required: true
|
||||
- label: Double check that your reproduction steps work in a fresh installation of [`silverstripe/installer`](https://github.com/silverstripe/silverstripe-installer) (with any code examples you've provided)
|
||||
required: true
|
35
.github/ISSUE_TEMPLATE/2_feature_request.yml
vendored
Normal file
35
.github/ISSUE_TEMPLATE/2_feature_request.yml
vendored
Normal file
@ -0,0 +1,35 @@
|
||||
name: 🚀 Feature Request
|
||||
description: Submit a feature request (but only if you're planning on implementing it)
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Please only submit feature requests if you plan on implementing the feature yourself.
|
||||
See the [contributing code documentation](https://docs.silverstripe.org/en/contributing/code/#make-or-find-a-github-issue) for more guidelines about submitting feature requests.
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Description
|
||||
description: A clear and concise description of the new feature, and why it belongs in core
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: more-info
|
||||
attributes:
|
||||
label: Additional context or points of discussion
|
||||
description: |
|
||||
*Optional: Any additional context, points of discussion, etc that might help validate and refine your idea*
|
||||
- type: checkboxes
|
||||
id: validations
|
||||
attributes:
|
||||
label: Validations
|
||||
description: "Before submitting the issue, please confirm the following:"
|
||||
options:
|
||||
- label: You intend to implement the feature yourself
|
||||
required: true
|
||||
- label: You have read the [contributing guide](https://docs.silverstripe.org/en/contributing/code/)
|
||||
required: true
|
||||
- label: You strongly believe this feature should be in core, rather than being its own community module
|
||||
required: true
|
||||
- label: You have checked for existing issues or pull requests related to this feature (and didn't find any)
|
||||
required: true
|
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Security Vulnerability
|
||||
url: https://docs.silverstripe.org/en/contributing/issues_and_bugs/#reporting-security-issues
|
||||
about: ⚠️ We do not use GitHub issues to track security vulnerability reports. Click "open" on the right to see how to report security vulnerabilities.
|
||||
- name: Support Question
|
||||
url: https://www.silverstripe.org/community/
|
||||
about: We use GitHub issues only to discuss bugs and new features. For support questions, please use one of the support options available in our community channels.
|
39
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
39
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@ -0,0 +1,39 @@
|
||||
<!--
|
||||
Thanks for contributing, you're awesome! ⭐
|
||||
|
||||
Please read https://docs.silverstripe.org/en/contributing/code/ if you haven't contributed to this project recently.
|
||||
-->
|
||||
## Description
|
||||
<!--
|
||||
Please describe expected and observed behaviour, and what you're fixing.
|
||||
For visual fixes, please include tested browsers and screenshots.
|
||||
-->
|
||||
|
||||
## Manual testing steps
|
||||
<!--
|
||||
Include any manual testing steps here which a reviewer can perform to validate your pull request works correctly.
|
||||
Note that this DOES NOT replace unit or end-to-end tests.
|
||||
-->
|
||||
|
||||
## Issues
|
||||
<!--
|
||||
List all issues here that this pull request fixes/resolves.
|
||||
If there is no issue already, create a new one! You must link your pull request to at least one issue.
|
||||
-->
|
||||
- #
|
||||
|
||||
## Pull request checklist
|
||||
<!--
|
||||
PLEASE check each of these to ensure you have done everything you need to do!
|
||||
If there's something in this list you need help with, please ask so that we can assist you.
|
||||
-->
|
||||
- [ ] The target branch is correct
|
||||
- See [picking the right version](https://docs.silverstripe.org/en/contributing/code/#picking-the-right-version)
|
||||
- [ ] All commits are relevant to the purpose of the PR (e.g. no debug statements, unrelated refactoring, or arbitrary linting)
|
||||
- Small amounts of additional linting are usually okay, but if it makes it hard to concentrate on the relevant changes, ask for the unrelated changes to be reverted, and submitted as a separate PR.
|
||||
- [ ] The commit messages follow our [commit message guidelines](https://docs.silverstripe.org/en/contributing/code/#commit-messages)
|
||||
- [ ] The PR follows our [contribution guidelines](https://docs.silverstripe.org/en/contributing/code/)
|
||||
- [ ] Code changes follow our [coding conventions](https://docs.silverstripe.org/en/contributing/coding_conventions/)
|
||||
- [ ] This change is covered with tests (or tests aren't necessary for this change)
|
||||
- [ ] Any relevant User Help/Developer documentation is updated; for impactful changes, information is added to the changelog for the intended release
|
||||
- [ ] CI is green
|
11
.github/workflows/ci.yml
vendored
Normal file
11
.github/workflows/ci.yml
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
name: CI
|
||||
uses: silverstripe/gha-ci/.github/workflows/ci.yml@v1
|
21
.github/workflows/dispatch-ci.yml
vendored
Normal file
21
.github/workflows/dispatch-ci.yml
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
name: Dispatch CI
|
||||
|
||||
on:
|
||||
# At 8:40 PM UTC, only on Sunday and Monday
|
||||
schedule:
|
||||
- cron: '40 20 * * 0,1'
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
dispatch-ci:
|
||||
name: Dispatch CI
|
||||
# Only run cron on the symbiote account
|
||||
if: (github.event_name == 'schedule' && github.repository_owner == 'symbiote') || (github.event_name != 'schedule')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
actions: write
|
||||
steps:
|
||||
- name: Dispatch CI
|
||||
uses: silverstripe/gha-dispatch-ci@v1
|
17
.github/workflows/keepalive.yml
vendored
Normal file
17
.github/workflows/keepalive.yml
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
name: Keepalive
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
# The 7th of every month at 12:50pm UTC
|
||||
schedule:
|
||||
- cron: '50 12 7 * *'
|
||||
|
||||
jobs:
|
||||
keepalive:
|
||||
name: Keepalive
|
||||
# Only run cron on the symbiote account
|
||||
if: (github.event_name == 'schedule' && github.repository_owner == 'symbiote') || (github.event_name != 'schedule')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Keepalive
|
||||
uses: silverstripe/gha-keepalive@v1
|
22
.github/workflows/merge-up.yml
vendored
Normal file
22
.github/workflows/merge-up.yml
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
name: Merge-up
|
||||
|
||||
on:
|
||||
# At 8:40 PM UTC, only on Thursday
|
||||
schedule:
|
||||
- cron: '40 20 * * 4'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
merge-up:
|
||||
name: Merge-up
|
||||
# Only run cron on the symbiote account
|
||||
if: (github.event_name == 'schedule' && github.repository_owner == 'symbiote') || (github.event_name != 'schedule')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
actions: write
|
||||
steps:
|
||||
- name: Merge-up
|
||||
uses: silverstripe/gha-merge-up@v1
|
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
vendor/
|
||||
resources/
|
||||
composer.lock
|
||||
|
||||
assets/
|
36
.travis.yml
36
.travis.yml
@ -1,36 +0,0 @@
|
||||
# See https://github.com/silverstripe-labs/silverstripe-travis-support for setup details
|
||||
|
||||
sudo: false
|
||||
|
||||
language: php
|
||||
|
||||
php:
|
||||
- 5.3
|
||||
- 5.4
|
||||
- 5.5
|
||||
- 5.6
|
||||
|
||||
env:
|
||||
- DB=MYSQL CORE_RELEASE=3.2
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- php: 5.6
|
||||
env: DB=MYSQL CORE_RELEASE=3
|
||||
- php: 5.6
|
||||
env: DB=PGSQL CORE_RELEASE=3.1
|
||||
- php: 5.6
|
||||
env: DB=PGSQL CORE_RELEASE=3.3
|
||||
- php: 5.6
|
||||
env: DB=PGSQL CORE_RELEASE=3.4
|
||||
fast_finish: true
|
||||
|
||||
before_script:
|
||||
- composer self-update || true
|
||||
- git clone git://github.com/silverstripe-labs/silverstripe-travis-support.git ~/travis-support
|
||||
- php ~/travis-support/travis_setup.php --source `pwd` --target ~/builds/ss
|
||||
- cd ~/builds/ss
|
||||
- composer install
|
||||
|
||||
script:
|
||||
- vendor/bin/phpunit gridfieldextensions/tests
|
15
.tx/config
Normal file
15
.tx/config
Normal file
@ -0,0 +1,15 @@
|
||||
[main]
|
||||
host = https://www.transifex.com
|
||||
|
||||
[o:silverstripe:p:silverstripe-gridfieldextensions:r:master]
|
||||
file_filter = lang/<lang>.yml
|
||||
source_file = lang/en.yml
|
||||
source_lang = en
|
||||
type = YML
|
||||
|
||||
[o:silverstripe:p:silverstripe-gridfieldextensions:r:master-js]
|
||||
file_filter = client/lang/src/<lang>.json
|
||||
source_file = client/lang/src/en.json
|
||||
source_lang = en
|
||||
type = KEYVALUEJSON
|
||||
|
12
.upgrade.yml
Normal file
12
.upgrade.yml
Normal file
@ -0,0 +1,12 @@
|
||||
mappings:
|
||||
GridFieldAddExistingSearchButton: Symbiote\GridFieldExtensions\GridFieldAddExistingSearchButton
|
||||
GridFieldAddExistingSearchHandler: Symbiote\GridFieldExtensions\GridFieldAddExistingSearchHandler
|
||||
GridFieldAddNewInlineButton: Symbiote\GridFieldExtensions\GridFieldAddNewInlineButton
|
||||
GridFieldAddNewMultiClass: Symbiote\GridFieldExtensions\GridFieldAddNewMultiClass
|
||||
GridFieldAddNewMultiClassHandler: Symbiote\GridFieldExtensions\GridFieldAddNewMultiClassHandler
|
||||
GridFieldEditableColumns: Symbiote\GridFieldExtensions\GridFieldEditableColumns
|
||||
GridFieldExtensions: Symbiote\GridFieldExtensions\GridFieldExtensions
|
||||
GridFieldExternalLink: Symbiote\GridFieldExtensions\GridFieldExternalLink
|
||||
GridFieldOrderableRows: Symbiote\GridFieldExtensions\GridFieldOrderableRows
|
||||
GridFieldRequestHandler: Symbiote\GridFieldExtensions\GridFieldRequestHandler
|
||||
GridFieldTitleHeader: Symbiote\GridFieldExtensions\GridFieldTitleHeader
|
@ -1,4 +1,4 @@
|
||||
Copyright (c) 2013, SilverStripe Australia
|
||||
Copyright (c) 2013, Symbiote
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
@ -9,7 +9,7 @@ are permitted provided that the following conditions are met:
|
||||
* Redistributions in binary form must reproduce the above copyright notice, this
|
||||
list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
* Neither the name of the SilverStripe Australia nor the names of its
|
||||
* Neither the name of the Symbiote nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
|
56
README.md
56
README.md
@ -1,25 +1,31 @@
|
||||
SilverStripe Grid Field Extensions Module
|
||||
=========================================
|
||||
|
||||
[![Build Status](https://travis-ci.org/silverstripe-australia/silverstripe-gridfieldextensions.svg?branch=master)](https://travis-ci.org/silverstripe-australia/silverstripe-gridfieldextensions)
|
||||
[![Latest Stable Version](https://poser.pugx.org/silverstripe-australia/gridfieldextensions/version.svg)](https://github.com/silverstripe-australia/silverstripe-gridfieldextensions/releases)
|
||||
[![Latest Unstable Version](https://poser.pugx.org/silverstripe-australia/gridfieldextensions/v/unstable.svg)](https://packagist.org/packages/silverstripe-australia/gridfieldextensions)
|
||||
[![Total Downloads](https://poser.pugx.org/silverstripe-australia/gridfieldextensions/downloads.svg)](https://packagist.org/packages/silverstripe-australia/gridfieldextensions)
|
||||
[![License](https://poser.pugx.org/silverstripe-australia/gridfieldextensions/license.svg)](https://github.com/silverstripe-australia/silverstripe-gridfieldextensions/blob/master/LICENSE.md)
|
||||
|
||||
This module provides a number of useful grid field components:
|
||||
|
||||
* `GridFieldAddExistingSearchButton` - a more advanced search form for adding
|
||||
items.
|
||||
* `GridFieldAddNewInlineButton` - builds on `GridFieldEditableColumns` to allow
|
||||
inline creation of records.
|
||||
* `GridFieldAddNewMultiClass` - lets the user select from a list of classes to
|
||||
create a new record from.
|
||||
* `GridFieldEditableColumns` - allows inline editing of records.
|
||||
* `GridFieldOrderableRows` - drag and drop re-ordering of rows.
|
||||
* `GridFieldRequestHandler` - a basic utility class which can be used to build
|
||||
custom grid field detail views including tabs, breadcrumbs and other CMS
|
||||
features.
|
||||
* `GridFieldTitleHeader` - a simple header which displays column titles.
|
||||
|
||||
See [docs/en/index.md](docs/en/index.md) for documentation and examples.
|
||||
# Silverstripe Grid Field Extensions Module
|
||||
|
||||
[![CI](https://github.com/symbiote/silverstripe-gridfieldextensions/actions/workflows/ci.yml/badge.svg)](https://github.com/symbiote/silverstripe-gridfieldextensions/actions/workflows/ci.yml)
|
||||
[![Silverstripe supported module](https://img.shields.io/badge/silverstripe-supported-0071C4.svg)](https://www.silverstripe.org/software/addons/silverstripe-commercially-supported-module-list/)
|
||||
|
||||
This module provides a number of useful grid field components:
|
||||
|
||||
* `GridFieldAddExistingSearchButton` - a more advanced search form for adding
|
||||
items.
|
||||
* `GridFieldAddNewInlineButton` - builds on `GridFieldEditableColumns` to allow
|
||||
inline creation of records.
|
||||
* `GridFieldAddNewMultiClass` - lets the user select from a list of classes to
|
||||
create a new record from.
|
||||
* `GridFieldEditableColumns` - allows inline editing of records.
|
||||
* `GridFieldOrderableRows` - drag and drop re-ordering of rows.
|
||||
* `GridFieldRequestHandler` - a basic utility class which can be used to build
|
||||
custom grid field detail views including tabs, breadcrumbs and other CMS
|
||||
features.
|
||||
* `GridFieldTitleHeader` - a simple header which displays column titles.
|
||||
* `GridFieldConfigurablePaginator` - a paginator for GridField that allows customisable page sizes.
|
||||
|
||||
This branch will aim for compatibility with Silverstripe 4.x.
|
||||
|
||||
## Installation
|
||||
```bash
|
||||
composer require symbiote/silverstripe-gridfieldextensions:^3
|
||||
```
|
||||
|
||||
For Silverstripe 3.x, please see the [compatible branch](https://github.com/symbiote/silverstripe-gridfieldextensions/tree/2).
|
||||
|
||||
See [docs/en/index.md](docs/en/index.md) for documentation and examples.
|
||||
|
@ -2,4 +2,8 @@
|
||||
name: gridfieldextensions
|
||||
---
|
||||
GridFieldAddNewMultiClass:
|
||||
showEmptyString: true
|
||||
showEmptyString: true
|
||||
|
||||
SilverStripe\Forms\GridField\GridFieldDetailForm_ItemRequest:
|
||||
extensions:
|
||||
- Symbiote\GridFieldExtensions\Extensions\GridFieldDetailFormItemRequestExtension
|
||||
|
14
client/lang/en.js
Normal file
14
client/lang/en.js
Normal file
@ -0,0 +1,14 @@
|
||||
// This file was generated by silverstripe/tx-translator from client/lang/src/en.json.
|
||||
// See https://github.com/silverstripe/silverstripe-tx-translator for details
|
||||
if (typeof(ss) === 'undefined' || typeof(ss.i18n) === 'undefined') {
|
||||
if (typeof(console) !== 'undefined') { // eslint-disable-line no-console
|
||||
console.error('Class ss.i18n not defined'); // eslint-disable-line no-console
|
||||
}
|
||||
} else {
|
||||
ss.i18n.addDictionary('en', {
|
||||
"GridFieldExtensions.ADD_CLASS": "Add: <i>{classname}</i>",
|
||||
"GridFieldExtensions.CONFIRMDEL": "Are you sure you want to delete this?",
|
||||
"GridFieldExtensions.OPEN_SEARCH_FILTER": "Open search and filter",
|
||||
"GridFieldExtensions.SAVE_PUBLISH": "Save & publish"
|
||||
});
|
||||
}
|
14
client/lang/eo.js
Normal file
14
client/lang/eo.js
Normal file
@ -0,0 +1,14 @@
|
||||
// This file was generated by silverstripe/tx-translator from client/lang/src/eo.json.
|
||||
// See https://github.com/silverstripe/silverstripe-tx-translator for details
|
||||
if (typeof(ss) === 'undefined' || typeof(ss.i18n) === 'undefined') {
|
||||
if (typeof(console) !== 'undefined') { // eslint-disable-line no-console
|
||||
console.error('Class ss.i18n not defined'); // eslint-disable-line no-console
|
||||
}
|
||||
} else {
|
||||
ss.i18n.addDictionary('eo', {
|
||||
"GridFieldExtensions.ADD_CLASS": "Aldoni: <i>{classname}</i>",
|
||||
"GridFieldExtensions.CONFIRMDEL": "Ĉu vi certas ke vi volas forigi tion?",
|
||||
"GridFieldExtensions.OPEN_SEARCH_FILTER": "Malfermi serĉon kaj filtrilon",
|
||||
"GridFieldExtensions.SAVE_PUBLISH": "Konservi kaj publikigi"
|
||||
});
|
||||
}
|
14
client/lang/nl_NL.js
Normal file
14
client/lang/nl_NL.js
Normal file
@ -0,0 +1,14 @@
|
||||
// This file was generated by silverstripe/tx-translator from client/lang/src/nl_NL.json.
|
||||
// See https://github.com/silverstripe/silverstripe-tx-translator for details
|
||||
if (typeof(ss) === 'undefined' || typeof(ss.i18n) === 'undefined') {
|
||||
if (typeof(console) !== 'undefined') { // eslint-disable-line no-console
|
||||
console.error('Class ss.i18n not defined'); // eslint-disable-line no-console
|
||||
}
|
||||
} else {
|
||||
ss.i18n.addDictionary('nl_NL', {
|
||||
"GridFieldExtensions.ADD_CLASS": "Toevoegen: <i>{classname}</i>",
|
||||
"GridFieldExtensions.CONFIRMDEL": "Weet je zeker dat je dit wil verwijderen?",
|
||||
"GridFieldExtensions.OPEN_SEARCH_FILTER": "Open zoeken en filteren",
|
||||
"GridFieldExtensions.SAVE_PUBLISH": "Opslaan & Publiceren"
|
||||
});
|
||||
}
|
14
client/lang/sk.js
Normal file
14
client/lang/sk.js
Normal file
@ -0,0 +1,14 @@
|
||||
// This file was generated by silverstripe/tx-translator from client/lang/src/sk.json.
|
||||
// See https://github.com/silverstripe/silverstripe-tx-translator for details
|
||||
if (typeof(ss) === 'undefined' || typeof(ss.i18n) === 'undefined') {
|
||||
if (typeof(console) !== 'undefined') { // eslint-disable-line no-console
|
||||
console.error('Class ss.i18n not defined'); // eslint-disable-line no-console
|
||||
}
|
||||
} else {
|
||||
ss.i18n.addDictionary('sk', {
|
||||
"GridFieldExtensions.ADD_CLASS": "Pridať: <i>{classname}</i>",
|
||||
"GridFieldExtensions.CONFIRMDEL": "Naozaj to chcete odstrániť?",
|
||||
"GridFieldExtensions.OPEN_SEARCH_FILTER": "Otvorte vyhľadávanie a filter",
|
||||
"GridFieldExtensions.SAVE_PUBLISH": "Uložiť a zverejniť"
|
||||
});
|
||||
}
|
14
client/lang/sl.js
Normal file
14
client/lang/sl.js
Normal file
@ -0,0 +1,14 @@
|
||||
// This file was generated by silverstripe/tx-translator from client/lang/src/sl.json.
|
||||
// See https://github.com/silverstripe/silverstripe-tx-translator for details
|
||||
if (typeof(ss) === 'undefined' || typeof(ss.i18n) === 'undefined') {
|
||||
if (typeof(console) !== 'undefined') { // eslint-disable-line no-console
|
||||
console.error('Class ss.i18n not defined'); // eslint-disable-line no-console
|
||||
}
|
||||
} else {
|
||||
ss.i18n.addDictionary('sl', {
|
||||
"GridFieldExtensions.ADD_CLASS": "Dodaj: <i>{classname}</i>",
|
||||
"GridFieldExtensions.CONFIRMDEL": "Res želite to izbrisati?",
|
||||
"GridFieldExtensions.OPEN_SEARCH_FILTER": "Prikaži iskalnik in filtre",
|
||||
"GridFieldExtensions.SAVE_PUBLISH": "Shrani in objavi"
|
||||
});
|
||||
}
|
6
client/lang/src/en.json
Normal file
6
client/lang/src/en.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"GridFieldExtensions.ADD_CLASS": "Add: <i>{classname}</i>",
|
||||
"GridFieldExtensions.CONFIRMDEL": "Are you sure you want to delete this?",
|
||||
"GridFieldExtensions.OPEN_SEARCH_FILTER": "Open search and filter",
|
||||
"GridFieldExtensions.SAVE_PUBLISH": "Save & publish"
|
||||
}
|
6
client/lang/src/eo.json
Normal file
6
client/lang/src/eo.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"GridFieldExtensions.ADD_CLASS": "Aldoni: <i>{classname}</i>",
|
||||
"GridFieldExtensions.CONFIRMDEL": "Ĉu vi certas ke vi volas forigi tion?",
|
||||
"GridFieldExtensions.OPEN_SEARCH_FILTER": "Malfermi serĉon kaj filtrilon",
|
||||
"GridFieldExtensions.SAVE_PUBLISH": "Konservi kaj publikigi"
|
||||
}
|
6
client/lang/src/nl_NL.json
Normal file
6
client/lang/src/nl_NL.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"GridFieldExtensions.ADD_CLASS": "Toevoegen: <i>{classname}</i>",
|
||||
"GridFieldExtensions.CONFIRMDEL": "Weet je zeker dat je dit wil verwijderen?",
|
||||
"GridFieldExtensions.OPEN_SEARCH_FILTER": "Open zoeken en filteren",
|
||||
"GridFieldExtensions.SAVE_PUBLISH": "Opslaan & Publiceren"
|
||||
}
|
6
client/lang/src/sk.json
Normal file
6
client/lang/src/sk.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"GridFieldExtensions.ADD_CLASS": "Pridať: <i>{classname}</i>",
|
||||
"GridFieldExtensions.CONFIRMDEL": "Naozaj to chcete odstrániť?",
|
||||
"GridFieldExtensions.OPEN_SEARCH_FILTER": "Otvorte vyhľadávanie a filter",
|
||||
"GridFieldExtensions.SAVE_PUBLISH": "Uložiť a zverejniť"
|
||||
}
|
6
client/lang/src/sl.json
Normal file
6
client/lang/src/sl.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"GridFieldExtensions.ADD_CLASS": "Dodaj: <i>{classname}</i>",
|
||||
"GridFieldExtensions.CONFIRMDEL": "Res želite to izbrisati?",
|
||||
"GridFieldExtensions.OPEN_SEARCH_FILTER": "Prikaži iskalnik in filtre",
|
||||
"GridFieldExtensions.SAVE_PUBLISH": "Shrani in objavi"
|
||||
}
|
1
code-of-conduct.md
Normal file
1
code-of-conduct.md
Normal file
@ -0,0 +1 @@
|
||||
When having discussions about this module in issues or pull request please adhere to the [SilverStripe Community Code of Conduct](https://docs.silverstripe.org/en/contributing/code_of_conduct).
|
@ -1,99 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* A modal search dialog which uses search context to search for and add
|
||||
* existing records to a grid field.
|
||||
*/
|
||||
class GridFieldAddExistingSearchButton implements
|
||||
GridField_HTMLProvider,
|
||||
GridField_URLHandler {
|
||||
|
||||
private static $allowed_actions = array(
|
||||
'handleSearch'
|
||||
);
|
||||
|
||||
protected $title;
|
||||
protected $fragment;
|
||||
protected $searchList;
|
||||
|
||||
/**
|
||||
* @param string $fragment
|
||||
*/
|
||||
public function __construct($fragment = 'buttons-before-left') {
|
||||
$this->fragment = $fragment;
|
||||
$this->title = _t('GridFieldExtensions.ADDEXISTING', 'Add Existing');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getTitle() {
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $title
|
||||
* @return GridFieldAddExistingSearchButton $this
|
||||
*/
|
||||
public function setTitle($title) {
|
||||
$this->title = $title;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getFragment() {
|
||||
return $this->fragment;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $fragment
|
||||
* @return GridFieldAddExistingSearchButton $this
|
||||
*/
|
||||
public function setFragment($fragment) {
|
||||
$this->fragment = $fragment;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a custom list to use to provide the searchable items.
|
||||
*
|
||||
* @param SS_List $list
|
||||
* @return GridFieldAddExistingSearchButton $this
|
||||
*/
|
||||
public function setSearchList(SS_List $list) {
|
||||
$this->searchList = $list;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return SS_List|null
|
||||
*/
|
||||
public function getSearchList() {
|
||||
return $this->searchList;
|
||||
}
|
||||
|
||||
public function getHTMLFragments($grid) {
|
||||
GridFieldExtensions::include_requirements();
|
||||
|
||||
$data = new ArrayData(array(
|
||||
'Title' => $this->getTitle(),
|
||||
'Link' => $grid->Link('add-existing-search')
|
||||
));
|
||||
|
||||
return array(
|
||||
$this->fragment => $data->renderWith('GridFieldAddExistingSearchButton'),
|
||||
);
|
||||
}
|
||||
|
||||
public function getURLHandlers($grid) {
|
||||
return array(
|
||||
'add-existing-search' => 'handleSearch'
|
||||
);
|
||||
}
|
||||
|
||||
public function handleSearch($grid, $request) {
|
||||
return new GridFieldAddExistingSearchHandler($grid, $this);
|
||||
}
|
||||
|
||||
}
|
@ -1,109 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Used by {@link GridFieldAddExistingSearchButton} to provide the searching
|
||||
* functionality.
|
||||
*/
|
||||
class GridFieldAddExistingSearchHandler extends RequestHandler {
|
||||
|
||||
private static $allowed_actions = array(
|
||||
'index',
|
||||
'add',
|
||||
'SearchForm'
|
||||
);
|
||||
|
||||
/**
|
||||
* @var GridField
|
||||
*/
|
||||
protected $grid;
|
||||
|
||||
/**
|
||||
* @var GridFieldAddExistingSearchButton
|
||||
*/
|
||||
protected $button;
|
||||
|
||||
/**
|
||||
* @var SearchContext
|
||||
*/
|
||||
protected $context;
|
||||
|
||||
public function __construct($grid, $button) {
|
||||
$this->grid = $grid;
|
||||
$this->button = $button;
|
||||
$this->context = singleton($grid->getModelClass())->getDefaultSearchContext();
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function index() {
|
||||
return $this->renderWith('GridFieldAddExistingSearchHandler');
|
||||
}
|
||||
|
||||
public function add($request) {
|
||||
if(!$id = $request->postVar('id')) {
|
||||
$this->httpError(400);
|
||||
}
|
||||
|
||||
$list = $this->grid->getList();
|
||||
$item = DataList::create($list->dataClass())->byID($id);
|
||||
|
||||
if(!$item) {
|
||||
$this->httpError(400);
|
||||
}
|
||||
|
||||
$list->add($item);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Form
|
||||
*/
|
||||
public function SearchForm() {
|
||||
$form = new Form(
|
||||
$this,
|
||||
'SearchForm',
|
||||
$this->context->getFields(),
|
||||
new FieldList(
|
||||
FormAction::create('doSearch', _t('GridFieldExtensions.SEARCH', 'Search'))
|
||||
->setUseButtonTag(true)
|
||||
->addExtraClass('ss-ui-button')
|
||||
->setAttribute('data-icon', 'magnifier')
|
||||
)
|
||||
);
|
||||
|
||||
$form->addExtraClass('stacked add-existing-search-form');
|
||||
$form->setFormMethod('GET');
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
public function doSearch($data, $form) {
|
||||
$list = $this->context->getQuery($data, false, false, $this->getSearchList());
|
||||
$list = $list->subtract($this->grid->getList());
|
||||
$list = new PaginatedList($list, $this->request);
|
||||
|
||||
$data = $this->customise(array(
|
||||
'SearchForm' => $form,
|
||||
'Items' => $list
|
||||
));
|
||||
return $data->index();
|
||||
}
|
||||
|
||||
public function Items() {
|
||||
$list = $this->getSearchList();
|
||||
$list = $list->subtract($this->grid->getList());
|
||||
$list = new PaginatedList($list, $this->request);
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
public function Link($action = null) {
|
||||
return Controller::join_links($this->grid->Link(), 'add-existing-search', $action);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return DataList
|
||||
*/
|
||||
protected function getSearchList() {
|
||||
return $this->button->getSearchList() ?: DataList::create($this->grid->getList()->dataClass());
|
||||
}
|
||||
|
||||
}
|
@ -1,178 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Builds on the {@link GridFieldEditableColumns} component to allow creating new records.
|
||||
*/
|
||||
class GridFieldAddNewInlineButton implements GridField_HTMLProvider, GridField_SaveHandler {
|
||||
|
||||
private $fragment;
|
||||
|
||||
private $title;
|
||||
|
||||
/**
|
||||
* @param string $fragment the fragment to render the button in
|
||||
*/
|
||||
public function __construct($fragment = 'buttons-before-left') {
|
||||
$this->setFragment($fragment);
|
||||
$this->setTitle(_t('GridFieldExtensions.ADD', 'Add'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the fragment name this button is rendered into.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getFragment() {
|
||||
return $this->fragment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the fragment name this button is rendered into.
|
||||
*
|
||||
* @param string $fragment
|
||||
* @return GridFieldAddNewInlineButton $this
|
||||
*/
|
||||
public function setFragment($fragment) {
|
||||
$this->fragment = $fragment;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the button title text.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getTitle() {
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the button title text.
|
||||
*
|
||||
* @param string $title
|
||||
* @return GridFieldAddNewInlineButton $this
|
||||
*/
|
||||
public function setTitle($title) {
|
||||
$this->title = $title;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getHTMLFragments($grid) {
|
||||
if($grid->getList() && !singleton($grid->getModelClass())->canCreate()) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$fragment = $this->getFragment();
|
||||
|
||||
if(!$editable = $grid->getConfig()->getComponentByType('GridFieldEditableColumns')) {
|
||||
throw new Exception('Inline adding requires the editable columns component');
|
||||
}
|
||||
|
||||
Requirements::javascript(THIRDPARTY_DIR . '/javascript-templates/tmpl.js');
|
||||
GridFieldExtensions::include_requirements();
|
||||
|
||||
$data = new ArrayData(array(
|
||||
'Title' => $this->getTitle(),
|
||||
));
|
||||
|
||||
return array(
|
||||
$fragment => $data->renderWith(__CLASS__),
|
||||
'after' => $this->getRowTemplate($grid, $editable)
|
||||
);
|
||||
}
|
||||
|
||||
private function getRowTemplate(GridField $grid, GridFieldEditableColumns $editable) {
|
||||
$columns = new ArrayList();
|
||||
$handled = array_keys($editable->getDisplayFields($grid));
|
||||
|
||||
if($grid->getList()) {
|
||||
$record = Object::create($grid->getModelClass());
|
||||
} else {
|
||||
$record = null;
|
||||
}
|
||||
|
||||
$fields = $editable->getFields($grid, $record);
|
||||
|
||||
foreach($grid->getColumns() as $column) {
|
||||
if(in_array($column, $handled)) {
|
||||
$field = $fields->dataFieldByName($column);
|
||||
$field->setName(sprintf(
|
||||
'%s[%s][{%%=o.num%%}][%s]', $grid->getName(), __CLASS__, $field->getName()
|
||||
));
|
||||
|
||||
$content = $field->Field();
|
||||
|
||||
// Convert HTML IDs built by FormTemplateHelper to the template format
|
||||
$content = str_replace(
|
||||
'GridFieldAddNewInlineButton_o.num_',
|
||||
'GridFieldAddNewInlineButton_{%=o.num%}_',
|
||||
$content
|
||||
);
|
||||
} else {
|
||||
$content = $grid->getColumnContent($record, $column);
|
||||
|
||||
// Convert GridFieldEditableColumns to the template format
|
||||
$content = str_replace(
|
||||
'[GridFieldEditableColumns][0]',
|
||||
'[GridFieldAddNewInlineButton][{%=o.num%}]',
|
||||
$content
|
||||
);
|
||||
}
|
||||
|
||||
$attrs = '';
|
||||
|
||||
foreach($grid->getColumnAttributes($record, $column) as $attr => $val) {
|
||||
$attrs .= sprintf(' %s="%s"', $attr, Convert::raw2att($val));
|
||||
}
|
||||
|
||||
$columns->push(new ArrayData(array(
|
||||
'Content' => $content,
|
||||
'Attributes' => $attrs,
|
||||
'IsActions' => $column == 'Actions'
|
||||
)));
|
||||
}
|
||||
|
||||
return $columns->renderWith('GridFieldAddNewInlineRow');
|
||||
}
|
||||
|
||||
public function handleSave(GridField $grid, DataObjectInterface $record) {
|
||||
$list = $grid->getList();
|
||||
$value = $grid->Value();
|
||||
|
||||
if(!isset($value[__CLASS__]) || !is_array($value[__CLASS__])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$class = $grid->getModelClass();
|
||||
/** @var GridFieldEditableColumns $editable */
|
||||
$editable = $grid->getConfig()->getComponentByType('GridFieldEditableColumns');
|
||||
/** @var GridFieldOrderableRows $sortable */
|
||||
$sortable = $grid->getConfig()->getComponentByType('GridFieldOrderableRows');
|
||||
$form = $editable->getForm($grid, $record);
|
||||
|
||||
if(!singleton($class)->canCreate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach($value[__CLASS__] as $fields) {
|
||||
$item = $class::create();
|
||||
$extra = array();
|
||||
|
||||
$form->loadDataFrom($fields, Form::MERGE_CLEAR_MISSING);
|
||||
$form->saveInto($item);
|
||||
|
||||
// Check if we are also sorting these records
|
||||
if ($sortable) {
|
||||
$sortField = $sortable->getSortField();
|
||||
$item->setField($sortField, $fields[$sortField]);
|
||||
}
|
||||
|
||||
if($list instanceof ManyManyList) {
|
||||
$extra = array_intersect_key($form->getData(), (array) $list->getExtraFields());
|
||||
}
|
||||
|
||||
$item->write();
|
||||
$list->add($item, $extra);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,244 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* A component which lets the user select from a list of classes to create a new record form.
|
||||
*
|
||||
* By default the list of classes that are createable is the grid field's model class, and any
|
||||
* subclasses. This can be customised using {@link setClasses()}.
|
||||
*/
|
||||
class GridFieldAddNewMultiClass implements GridField_HTMLProvider, GridField_URLHandler {
|
||||
|
||||
private static $allowed_actions = array(
|
||||
'handleAdd'
|
||||
);
|
||||
|
||||
// Should we add an empty string to the add class dropdown?
|
||||
private static $showEmptyString = true;
|
||||
|
||||
private $fragment;
|
||||
|
||||
private $title;
|
||||
|
||||
private $classes;
|
||||
|
||||
private $defaultClass;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $itemRequestClass = 'GridFieldAddNewMultiClassHandler';
|
||||
|
||||
/**
|
||||
* @param string $fragment the fragment to render the button in
|
||||
*/
|
||||
public function __construct($fragment = 'before') {
|
||||
$this->setFragment($fragment);
|
||||
$this->setTitle(_t('GridFieldExtensions.ADD', 'Add'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the fragment name this button is rendered into.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getFragment() {
|
||||
return $this->fragment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the fragment name this button is rendered into.
|
||||
*
|
||||
* @param string $fragment
|
||||
* @return GridFieldAddNewMultiClass $this
|
||||
*/
|
||||
public function setFragment($fragment) {
|
||||
$this->fragment = $fragment;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the button title text.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getTitle() {
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the button title text.
|
||||
*
|
||||
* @param string $title
|
||||
* @return GridFieldAddNewMultiClass $this
|
||||
*/
|
||||
public function setTitle($title) {
|
||||
$this->title = $title;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the classes that can be created using this button, defaulting to the model class and
|
||||
* its subclasses.
|
||||
*
|
||||
* @param GridField $grid
|
||||
* @return array a map of class name to title
|
||||
*/
|
||||
public function getClasses(GridField $grid) {
|
||||
$result = array();
|
||||
|
||||
if(is_null($this->classes)) {
|
||||
$classes = array_values(ClassInfo::subclassesFor($grid->getModelClass()));
|
||||
sort($classes);
|
||||
} else {
|
||||
$classes = $this->classes;
|
||||
}
|
||||
|
||||
$kill_ancestors = array();
|
||||
foreach($classes as $class => $title) {
|
||||
if(!is_string($class)) {
|
||||
$class = $title;
|
||||
}
|
||||
if (!class_exists($class)) {
|
||||
continue;
|
||||
}
|
||||
$is_abstract = (($reflection = new ReflectionClass($class)) && $reflection->isAbstract());
|
||||
if (!$is_abstract && $class === $title) {
|
||||
$title = singleton($class)->i18n_singular_name();
|
||||
}
|
||||
|
||||
if ($ancestor_to_hide = Config::inst()->get($class, 'hide_ancestor', Config::FIRST_SET)) {
|
||||
$kill_ancestors[$ancestor_to_hide] = true;
|
||||
}
|
||||
|
||||
if($is_abstract || !singleton($class)->canCreate()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$result[$class] = $title;
|
||||
}
|
||||
|
||||
if($kill_ancestors) {
|
||||
foreach($kill_ancestors as $class => $bool) {
|
||||
unset($result[$class]);
|
||||
}
|
||||
}
|
||||
|
||||
$sanitised = array();
|
||||
foreach($result as $class=>$title) {
|
||||
$sanitised[$this->sanitiseClassName($class)] = $title;
|
||||
}
|
||||
|
||||
return $sanitised;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the classes that can be created using this button.
|
||||
*
|
||||
* @param array $classes a set of class names, optionally mapped to titles
|
||||
* @return GridFieldAddNewMultiClass $this
|
||||
*/
|
||||
public function setClasses(array $classes, $default = null) {
|
||||
$this->classes = $classes;
|
||||
if($default) $this->defaultClass = $default;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the default class that is selected automatically.
|
||||
*
|
||||
* @param string $default the class name to use as default
|
||||
* @return GridFieldAddNewMultiClass $this
|
||||
*/
|
||||
public function setDefaultClass($default) {
|
||||
$this->defaultClass = $default;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles adding a new instance of a selected class.
|
||||
*
|
||||
* @param GridField $grid
|
||||
* @param SS_HTTPRequest $request
|
||||
* @return GridFieldAddNewMultiClassHandler
|
||||
*/
|
||||
public function handleAdd($grid, $request) {
|
||||
$class = $request->param('ClassName');
|
||||
$classes = $this->getClasses($grid);
|
||||
$component = $grid->getConfig()->getComponentByType('GridFieldDetailForm');
|
||||
|
||||
if(!$component) {
|
||||
throw new Exception('The add new multi class component requires the detail form component.');
|
||||
}
|
||||
|
||||
if(!$class || !array_key_exists($class, $classes)) {
|
||||
throw new SS_HTTPResponse_Exception(400);
|
||||
}
|
||||
|
||||
$unsanitisedClass = $this->unsanitiseClassName($class);
|
||||
$handler = Object::create($this->itemRequestClass,
|
||||
$grid, $component, new $unsanitisedClass(), $grid->getForm()->getController(), 'add-multi-class'
|
||||
);
|
||||
$handler->setTemplate($component->getTemplate());
|
||||
|
||||
return $handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getHTMLFragments($grid) {
|
||||
$classes = $this->getClasses($grid);
|
||||
|
||||
if(!count($classes)) {
|
||||
return array();
|
||||
}
|
||||
|
||||
GridFieldExtensions::include_requirements();
|
||||
|
||||
|
||||
$field = new DropdownField(sprintf('%s[%s]', __CLASS__, $grid->getName()), '', $classes, $this->defaultClass);
|
||||
if (Config::inst()->get('GridFieldAddNewMultiClass', 'showEmptyString')) {
|
||||
$field->setEmptyString(_t('GridFieldExtensions.SELECTTYPETOCREATE', '(Select type to create)'));
|
||||
}
|
||||
$field->addExtraClass('no-change-track');
|
||||
|
||||
$data = new ArrayData(array(
|
||||
'Title' => $this->getTitle(),
|
||||
'Link' => Controller::join_links($grid->Link(), 'add-multi-class', '{class}'),
|
||||
'ClassField' => $field
|
||||
));
|
||||
|
||||
return array(
|
||||
$this->getFragment() => $data->renderWith(__CLASS__)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getURLHandlers($grid) {
|
||||
return array(
|
||||
'add-multi-class/$ClassName!' => 'handleAdd'
|
||||
);
|
||||
}
|
||||
|
||||
public function setItemRequestClass($class) {
|
||||
$this->itemRequestClass = $class;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitise a model class' name for inclusion in a link
|
||||
* @return string
|
||||
*/
|
||||
protected function sanitiseClassName($class) {
|
||||
return str_replace('\\', '-', $class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsanitise a model class' name from a URL param
|
||||
* @return string
|
||||
*/
|
||||
protected function unsanitiseClassName($class) {
|
||||
return str_replace('-', '\\', $class);
|
||||
}
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* A custom grid field request handler that allows interacting with form fields when adding records.
|
||||
*/
|
||||
class GridFieldAddNewMultiClassHandler extends GridFieldDetailForm_ItemRequest {
|
||||
|
||||
public function Link($action = null) {
|
||||
if($this->record->ID) {
|
||||
return parent::Link($action);
|
||||
} else {
|
||||
return Controller::join_links(
|
||||
$this->gridField->Link(), 'add-multi-class', get_class($this->record)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,269 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Allows inline editing of grid field records without having to load a separate
|
||||
* edit interface.
|
||||
*
|
||||
* The form fields used can be configured by setting the value in {@link setDisplayFields()} to one
|
||||
* of the following forms:
|
||||
* - A Closure which returns the field instance.
|
||||
* - An array with a `callback` key pointing to a function which returns the field.
|
||||
* - An array with a `field` key->response specifying the field class to use.
|
||||
*/
|
||||
class GridFieldEditableColumns extends GridFieldDataColumns implements
|
||||
GridField_HTMLProvider,
|
||||
GridField_SaveHandler,
|
||||
GridField_URLHandler {
|
||||
|
||||
private static $allowed_actions = array(
|
||||
'handleForm'
|
||||
);
|
||||
|
||||
/**
|
||||
* @var Form[]
|
||||
*/
|
||||
protected $forms = array();
|
||||
|
||||
public function getColumnContent($grid, $record, $col) {
|
||||
if(!$record->canEdit()) {
|
||||
return parent::getColumnContent($grid, $record, $col);
|
||||
}
|
||||
|
||||
$fields = $this->getForm($grid, $record)->Fields();
|
||||
|
||||
if (!$this->displayFields)
|
||||
{
|
||||
// If setDisplayFields() not used, utilize $summary_fields
|
||||
// in a way similar to base class
|
||||
$colRelation = explode('.', $col);
|
||||
$value = $grid->getDataFieldValue($record, $colRelation[0]);
|
||||
$field = $fields->fieldByName($colRelation[0]);
|
||||
if (!$field || $field->isReadonly() || $field->isDisabled()) {
|
||||
return parent::getColumnContent($grid, $record, $col);
|
||||
}
|
||||
|
||||
// Ensure this field is available to edit on the record
|
||||
// (ie. Maybe its readonly due to certain circumstances, or removed and not editable)
|
||||
$cmsFields = $record->getCMSFields();
|
||||
$cmsField = $cmsFields->dataFieldByName($colRelation[0]);
|
||||
if (!$cmsField || $cmsField->isReadonly() || $cmsField->isDisabled())
|
||||
{
|
||||
return parent::getColumnContent($grid, $record, $col);
|
||||
}
|
||||
$field = clone $field;
|
||||
}
|
||||
else
|
||||
{
|
||||
$value = $grid->getDataFieldValue($record, $col);
|
||||
$rel = (strpos($col,'.') === false); // field references a relation value
|
||||
$field = ($rel) ? clone $fields->fieldByName($col) : new ReadonlyField($col);
|
||||
|
||||
if(!$field) {
|
||||
throw new Exception("Could not find the field '$col'");
|
||||
}
|
||||
}
|
||||
|
||||
if(array_key_exists($col, $this->fieldCasting)) {
|
||||
$value = $grid->getCastedValue($value, $this->fieldCasting[$col]);
|
||||
}
|
||||
|
||||
$value = $this->formatValue($grid, $record, $col, $value);
|
||||
|
||||
$field->setName($this->getFieldName($field->getName(), $grid, $record));
|
||||
$field->setValue($value);
|
||||
|
||||
if ($field instanceof HtmlEditorField) {
|
||||
return $field->FieldHolder();
|
||||
}
|
||||
|
||||
return $field->forTemplate();
|
||||
}
|
||||
|
||||
public function getHTMLFragments($grid) {
|
||||
GridFieldExtensions::include_requirements();
|
||||
$grid->addExtraClass('ss-gridfield-editable');
|
||||
}
|
||||
|
||||
public function handleSave(GridField $grid, DataObjectInterface $record) {
|
||||
$list = $grid->getList();
|
||||
$value = $grid->Value();
|
||||
|
||||
if(!isset($value[__CLASS__]) || !is_array($value[__CLASS__])) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var GridFieldOrderableRows $sortable */
|
||||
$sortable = $grid->getConfig()->getComponentByType('GridFieldOrderableRows');
|
||||
|
||||
$form = $this->getForm($grid, $record);
|
||||
|
||||
foreach($value[__CLASS__] as $id => $fields) {
|
||||
if(!is_numeric($id) || !is_array($fields)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$item = $list->byID($id);
|
||||
|
||||
if(!$item || !$item->canEdit()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$extra = array();
|
||||
|
||||
$form->loadDataFrom($fields, Form::MERGE_CLEAR_MISSING);
|
||||
$form->saveInto($item);
|
||||
|
||||
// Check if we are also sorting these records
|
||||
if ($sortable) {
|
||||
$sortField = $sortable->getSortField();
|
||||
$item->setField($sortField, $fields[$sortField]);
|
||||
}
|
||||
|
||||
if($list instanceof ManyManyList) {
|
||||
$extra = array_intersect_key($form->getData(), (array) $list->getExtraFields());
|
||||
}
|
||||
|
||||
$item->write();
|
||||
$list->add($item, $extra);
|
||||
}
|
||||
}
|
||||
|
||||
public function handleForm(GridField $grid, $request) {
|
||||
$id = $request->param('ID');
|
||||
$list = $grid->getList();
|
||||
|
||||
if(!ctype_digit($id)) {
|
||||
throw new SS_HTTPResponse_Exception(null, 400);
|
||||
}
|
||||
|
||||
if(!$record = $list->byID($id)) {
|
||||
throw new SS_HTTPResponse_Exception(null, 404);
|
||||
}
|
||||
|
||||
$form = $this->getForm($grid, $record);
|
||||
|
||||
foreach($form->Fields() as $field) {
|
||||
$field->setName($this->getFieldName($field->getName(), $grid, $record));
|
||||
}
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
public function getURLHandlers($grid) {
|
||||
return array(
|
||||
'editable/form/$ID' => 'handleForm'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the field list for a record.
|
||||
*
|
||||
* @param GridField $grid
|
||||
* @param DataObjectInterface $record
|
||||
* @return FieldList
|
||||
*/
|
||||
public function getFields(GridField $grid, DataObjectInterface $record) {
|
||||
$cols = $this->getDisplayFields($grid);
|
||||
$fields = new FieldList();
|
||||
|
||||
$list = $grid->getList();
|
||||
$class = $list ? $list->dataClass() : null;
|
||||
|
||||
foreach($cols as $col => $info) {
|
||||
$field = null;
|
||||
|
||||
if($info instanceof Closure) {
|
||||
$field = call_user_func($info, $record, $col, $grid);
|
||||
} elseif(is_array($info)) {
|
||||
if(isset($info['callback'])) {
|
||||
$field = call_user_func($info['callback'], $record, $col, $grid);
|
||||
} elseif(isset($info['field'])) {
|
||||
if ($info['field'] == 'LiteralField') {
|
||||
$field = new $info['field']($col, null);
|
||||
} else {
|
||||
$field = new $info['field']($col);
|
||||
}
|
||||
}
|
||||
|
||||
if(!$field instanceof FormField) {
|
||||
throw new Exception(sprintf(
|
||||
'The field for column "%s" is not a valid form field',
|
||||
$col
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if(!$field && $list instanceof ManyManyList) {
|
||||
$extra = $list->getExtraFields();
|
||||
|
||||
if($extra && array_key_exists($col, $extra)) {
|
||||
$field = Object::create_from_string($extra[$col], $col)->scaffoldFormField();
|
||||
}
|
||||
}
|
||||
|
||||
if(!$field) {
|
||||
if (!$this->displayFields)
|
||||
{
|
||||
// If setDisplayFields() not used, utilize $summary_fields
|
||||
// in a way similar to base class
|
||||
//
|
||||
// Allows use of 'MyBool.Nice' and 'MyHTML.NoHTML' so that
|
||||
// GridFields not using inline editing still look good or
|
||||
// revert to looking good in cases where the field isn't
|
||||
// available or is readonly
|
||||
//
|
||||
$colRelation = explode('.', $col);
|
||||
if($class && $obj = singleton($class)->dbObject($colRelation[0])) {
|
||||
$field = $obj->scaffoldFormField();
|
||||
} else {
|
||||
$field = new ReadonlyField($colRelation[0]);
|
||||
}
|
||||
}
|
||||
else if($class && $obj = singleton($class)->dbObject($col)) {
|
||||
$field = $obj->scaffoldFormField();
|
||||
} else {
|
||||
$field = new ReadonlyField($col);
|
||||
}
|
||||
}
|
||||
|
||||
if(!$field instanceof FormField) {
|
||||
throw new Exception(sprintf(
|
||||
'Invalid form field instance for column "%s"', $col
|
||||
));
|
||||
}
|
||||
|
||||
// Add CSS class for interactive fields
|
||||
if (!($field->isReadOnly() || $field instanceof LiteralField)) $field->addExtraClass('editable-column-field');
|
||||
|
||||
$fields->push($field);
|
||||
}
|
||||
|
||||
return $fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the form instance for a record.
|
||||
*
|
||||
* @param GridField $grid
|
||||
* @param DataObjectInterface $record
|
||||
* @return Form
|
||||
*/
|
||||
public function getForm(GridField $grid, DataObjectInterface $record) {
|
||||
$fields = $this->getFields($grid, $record);
|
||||
|
||||
$form = new Form($this, null, $fields, new FieldList());
|
||||
$form->loadDataFrom($record);
|
||||
|
||||
$form->setFormAction(Controller::join_links(
|
||||
$grid->Link(), 'editable/form', $record->ID
|
||||
));
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
protected function getFieldName($name, GridField $grid, DataObjectInterface $record) {
|
||||
return sprintf(
|
||||
'%s[%s][%s][%s]', $grid->getName(), __CLASS__, $record->ID, $name
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Utility functions for the grid fields extension module.
|
||||
*/
|
||||
class GridFieldExtensions {
|
||||
|
||||
public static function include_requirements() {
|
||||
$moduleDir = self::get_module_dir();
|
||||
Requirements::css($moduleDir.'/css/GridFieldExtensions.css');
|
||||
Requirements::javascript($moduleDir.'/javascript/GridFieldExtensions.js');
|
||||
}
|
||||
|
||||
public static function get_module_dir() {
|
||||
return basename(dirname(__DIR__));
|
||||
}
|
||||
|
||||
}
|
@ -1,68 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Displays a link to an external source referenced 'external link'
|
||||
*/
|
||||
class GridFieldExternalLink extends GridFieldDataColumns {
|
||||
|
||||
/**
|
||||
* Add a column for the actions
|
||||
*
|
||||
* @param type $gridField
|
||||
* @param array $columns
|
||||
*/
|
||||
public function augmentColumns($gridField, &$columns) {
|
||||
if(!in_array('Actions', $columns)) $columns[] = 'Actions';
|
||||
}
|
||||
|
||||
/**
|
||||
* Return any special attributes that will be used for FormField::create_tag()
|
||||
*
|
||||
* @param GridField $gridField
|
||||
* @param DataObject $record
|
||||
* @param string $columnName
|
||||
* @return array
|
||||
*/
|
||||
public function getColumnAttributes($gridField, $record, $columnName) {
|
||||
return array('class' => 'col-buttons');
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the title
|
||||
*
|
||||
* @param GridField $gridField
|
||||
* @param string $columnName
|
||||
* @return array
|
||||
*/
|
||||
public function getColumnMetadata($gridField, $columnName) {
|
||||
if($columnName == 'Actions') {
|
||||
return array('title' => '');
|
||||
}
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Which columns are handled by this component
|
||||
*
|
||||
* @param type $gridField
|
||||
* @return type
|
||||
*/
|
||||
public function getColumnsHandled($gridField) {
|
||||
return array('Actions');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param GridField $gridField
|
||||
* @param DataObject $record
|
||||
* @param string $columnName
|
||||
*
|
||||
* @return string - the HTML for the column
|
||||
*/
|
||||
public function getColumnContent($gridField, $record, $columnName) {
|
||||
$data = new ArrayData(array(
|
||||
'Link' => $record->hasMethod('getExternalLink') ? $record->getExternalLink() : $record->ExternalLink,
|
||||
'Text' => $record->hasMethod('getExternalLinkText') ? $record->getExternalLinkText() : 'External Link'
|
||||
));
|
||||
|
||||
return $data->renderWith('GridFieldExternalLink');
|
||||
}
|
||||
}
|
@ -1,584 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Allows grid field rows to be re-ordered via drag and drop. Both normal data
|
||||
* lists and many many lists can be ordered.
|
||||
*
|
||||
* If the grid field has not been sorted, this component will sort the data by
|
||||
* the sort field.
|
||||
*/
|
||||
class GridFieldOrderableRows extends RequestHandler implements
|
||||
GridField_ColumnProvider,
|
||||
GridField_DataManipulator,
|
||||
GridField_HTMLProvider,
|
||||
GridField_URLHandler,
|
||||
GridField_SaveHandler {
|
||||
|
||||
/**
|
||||
* @see $immediateUpdate
|
||||
* @var boolean
|
||||
*/
|
||||
private static $default_immediate_update = true;
|
||||
|
||||
private static $allowed_actions = array(
|
||||
'handleReorder',
|
||||
'handleMoveToPage'
|
||||
);
|
||||
|
||||
/**
|
||||
* The database field which specifies the sort, defaults to "Sort".
|
||||
*
|
||||
* @see setSortField()
|
||||
* @var string
|
||||
*/
|
||||
protected $sortField;
|
||||
|
||||
/**
|
||||
* If set to true, when an item is re-ordered, it will update on the
|
||||
* database and refresh the gridfield. When set to false, it will only
|
||||
* update the sort order when the record is saved.
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
protected $immediateUpdate;
|
||||
|
||||
/**
|
||||
* Extra sort fields to apply before the sort field.
|
||||
*
|
||||
* @see setExtraSortFields()
|
||||
* @var string|array
|
||||
*/
|
||||
protected $extraSortFields = null;
|
||||
|
||||
/**
|
||||
* The number of the column containing the reorder handles
|
||||
*
|
||||
* @see setReorderColumnNumber()
|
||||
* @var int
|
||||
*/
|
||||
protected $reorderColumnNumber = 0;
|
||||
|
||||
/**
|
||||
* @param string $sortField
|
||||
*/
|
||||
public function __construct($sortField = 'Sort') {
|
||||
parent::__construct();
|
||||
$this->sortField = $sortField;
|
||||
$this->immediateUpdate = $this->config()->default_immediate_update;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getSortField() {
|
||||
return $this->sortField;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the field used to specify the sort.
|
||||
*
|
||||
* @param string $sortField
|
||||
* @return GridFieldOrderableRows $this
|
||||
*/
|
||||
public function setSortField($field) {
|
||||
$this->sortField = $field;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return boolean
|
||||
*/
|
||||
public function getImmediateUpdate() {
|
||||
return $this->immediateUpdate;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see $immediateUpdate
|
||||
* @param boolean $immediateUpdate
|
||||
* @return GridFieldOrderableRows $this
|
||||
*/
|
||||
public function setImmediateUpdate($bool) {
|
||||
$this->immediateUpdate = $bool;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|array
|
||||
*/
|
||||
public function getExtraSortFields() {
|
||||
return $this->extraSortFields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets extra sort fields to apply before the sort field.
|
||||
*
|
||||
* @param string|array $fields
|
||||
* @return GridFieldOrderableRows $this
|
||||
*/
|
||||
public function setExtraSortFields($fields) {
|
||||
$this->extraSortFields = $fields;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getReorderColumnNumber() {
|
||||
return $this->reorderColumnNumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the number of the column containing the reorder handles.
|
||||
*
|
||||
* @param int $colno
|
||||
* @return GridFieldOrderableRows $this
|
||||
*/
|
||||
public function setReorderColumnNumber($colno) {
|
||||
$this->reorderColumnNumber = $colno;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the table which contains the sort field.
|
||||
*
|
||||
* @param DataList $list
|
||||
* @return string
|
||||
*/
|
||||
public function getSortTable(SS_List $list) {
|
||||
$field = $this->getSortField();
|
||||
|
||||
if($list instanceof ManyManyList) {
|
||||
$extra = $list->getExtraFields();
|
||||
$table = $list->getJoinTable();
|
||||
|
||||
if($extra && array_key_exists($field, $extra)) {
|
||||
return $table;
|
||||
}
|
||||
}
|
||||
|
||||
$classes = ClassInfo::dataClassesFor($list->dataClass());
|
||||
|
||||
foreach($classes as $class) {
|
||||
if(singleton($class)->hasOwnTableDatabaseField($field)) {
|
||||
return $class;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Exception("Couldn't find the sort field '$field'");
|
||||
}
|
||||
|
||||
public function getURLHandlers($grid) {
|
||||
return array(
|
||||
'POST reorder' => 'handleReorder',
|
||||
'POST movetopage' => 'handleMoveToPage'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param GridField $field
|
||||
*/
|
||||
public function getHTMLFragments($field) {
|
||||
GridFieldExtensions::include_requirements();
|
||||
|
||||
$field->addExtraClass('ss-gridfield-orderable');
|
||||
$field->setAttribute('data-immediate-update', (string)(int)$this->immediateUpdate);
|
||||
$field->setAttribute('data-url-reorder', $field->Link('reorder'));
|
||||
$field->setAttribute('data-url-movetopage', $field->Link('movetopage'));
|
||||
}
|
||||
|
||||
public function augmentColumns($grid, &$cols) {
|
||||
if(!in_array('Reorder', $cols) && $grid->getState()->GridFieldOrderableRows->enabled) {
|
||||
array_splice($cols, $this->reorderColumnNumber, 0, 'Reorder');
|
||||
}
|
||||
}
|
||||
|
||||
public function getColumnsHandled($grid) {
|
||||
return array('Reorder');
|
||||
}
|
||||
|
||||
public function getColumnContent($grid, $record, $col) {
|
||||
// In case you are using GridFieldEditableColumns, this ensures that
|
||||
// the correct sort order is saved. If you are not using that component,
|
||||
// this will be ignored by other components, but will still work for this.
|
||||
$sortFieldName = sprintf(
|
||||
'%s[GridFieldEditableColumns][%s][%s]',
|
||||
$grid->getName(),
|
||||
$record->ID,
|
||||
$this->getSortField()
|
||||
);
|
||||
$sortField = new HiddenField($sortFieldName, false, $record->getField($this->getSortField()));
|
||||
$sortField->addExtraClass('ss-orderable-hidden-sort');
|
||||
$sortField->setForm($grid->getForm());
|
||||
|
||||
return ViewableData::create()->customise(array(
|
||||
'SortField' => $sortField
|
||||
))->renderWith('GridFieldOrderableRowsDragHandle');
|
||||
}
|
||||
|
||||
public function getColumnAttributes($grid, $record, $col) {
|
||||
return array('class' => 'col-reorder');
|
||||
}
|
||||
|
||||
public function getColumnMetadata($grid, $col) {
|
||||
if ($fieldLabels = singleton($grid->getModelClass())->fieldLabels()) {
|
||||
return array('title' => isset($fieldLabels['Reorder']) ? $fieldLabels['Reorder'] : '');
|
||||
}
|
||||
|
||||
return array('title' => '');
|
||||
}
|
||||
|
||||
public function getManipulatedData(GridField $grid, SS_List $list) {
|
||||
$state = $grid->getState();
|
||||
$sorted = (bool) ((string) $state->GridFieldSortableHeader->SortColumn);
|
||||
|
||||
// If the data has not been sorted by the user, then sort it by the
|
||||
// sort column, otherwise disable reordering.
|
||||
$state->GridFieldOrderableRows->enabled = !$sorted;
|
||||
|
||||
if(!$sorted) {
|
||||
$sortterm = '';
|
||||
if ($this->extraSortFields) {
|
||||
if (is_array($this->extraSortFields)) {
|
||||
foreach($this->extraSortFields as $col => $dir) {
|
||||
$sortterm .= "$col $dir, ";
|
||||
}
|
||||
} else {
|
||||
$sortterm = $this->extraSortFields.', ';
|
||||
}
|
||||
}
|
||||
if ($list instanceof ArrayList) {
|
||||
// Fix bug in 3.1.3+ where ArrayList doesn't account for quotes
|
||||
$sortterm .= $this->getSortTable($list).'.'.$this->getSortField();
|
||||
} else {
|
||||
$sortterm .= '"'.$this->getSortTable($list).'"."'.$this->getSortField().'"';
|
||||
}
|
||||
return $list->sort($sortterm);
|
||||
} else {
|
||||
return $list;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles requests to reorder a set of IDs in a specific order.
|
||||
*
|
||||
* @param GridField $grid
|
||||
* @param SS_HTTPRequest $request
|
||||
* @return SS_HTTPResponse
|
||||
*/
|
||||
public function handleReorder($grid, $request) {
|
||||
if (!$this->immediateUpdate)
|
||||
{
|
||||
$this->httpError(400);
|
||||
}
|
||||
$list = $grid->getList();
|
||||
$modelClass = $grid->getModelClass();
|
||||
if ($list instanceof ManyManyList && !singleton($modelClass)->canView()) {
|
||||
$this->httpError(403);
|
||||
} else if(!($list instanceof ManyManyList) && !singleton($modelClass)->canEdit()) {
|
||||
$this->httpError(403);
|
||||
}
|
||||
|
||||
// Save any un-committed changes to the gridfield
|
||||
if(($form = $grid->getForm()) && ($record = $form->getRecord()) ) {
|
||||
$form->loadDataFrom($request->requestVars(), true);
|
||||
$grid->saveInto($record);
|
||||
}
|
||||
|
||||
// Get records from the `GridFieldEditableColumns` column
|
||||
$data = $request->postVar($grid->getName());
|
||||
$sortedIDs = $this->getSortedIDs($data);
|
||||
if (!$this->executeReorder($grid, $sortedIDs))
|
||||
{
|
||||
$this->httpError(400);
|
||||
}
|
||||
|
||||
Controller::curr()->getResponse()->addHeader('X-Status', rawurlencode('Records reordered.'));
|
||||
return $grid->FieldHolder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get mapping of sort value to ID from posted data
|
||||
*
|
||||
* @param array $data Raw posted data
|
||||
* @return array
|
||||
*/
|
||||
protected function getSortedIDs($data) {
|
||||
if (empty($data['GridFieldEditableColumns'])) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$sortedIDs = array();
|
||||
foreach($data['GridFieldEditableColumns'] as $id => $recordData) {
|
||||
$sortValue = $recordData[$this->sortField];
|
||||
$sortedIDs[$sortValue] = $id;
|
||||
}
|
||||
ksort($sortedIDs);
|
||||
return $sortedIDs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles requests to move an item to the previous or next page.
|
||||
*/
|
||||
public function handleMoveToPage(GridField $grid, $request) {
|
||||
if(!$paginator = $grid->getConfig()->getComponentByType('GridFieldPaginator')) {
|
||||
$this->httpError(404, 'Paginator component not found');
|
||||
}
|
||||
|
||||
$move = $request->postVar('move');
|
||||
$field = $this->getSortField();
|
||||
|
||||
$list = $grid->getList();
|
||||
$manip = $grid->getManipulatedList();
|
||||
|
||||
$existing = $manip->map('ID', $field)->toArray();
|
||||
$values = $existing;
|
||||
$order = array();
|
||||
|
||||
$id = isset($move['id']) ? (int) $move['id'] : null;
|
||||
$to = isset($move['page']) ? $move['page'] : null;
|
||||
|
||||
if(!isset($values[$id])) {
|
||||
$this->httpError(400, 'Invalid item ID');
|
||||
}
|
||||
|
||||
$this->populateSortValues($list);
|
||||
|
||||
$page = ((int) $grid->getState()->GridFieldPaginator->currentPage) ?: 1;
|
||||
$per = $paginator->getItemsPerPage();
|
||||
|
||||
if($to == 'prev') {
|
||||
$swap = $list->limit(1, ($page - 1) * $per - 1)->first();
|
||||
$values[$swap->ID] = $swap->$field;
|
||||
|
||||
$order[] = $id;
|
||||
$order[] = $swap->ID;
|
||||
|
||||
foreach($existing as $_id => $sort) {
|
||||
if($id != $_id) $order[] = $_id;
|
||||
}
|
||||
} elseif($to == 'next') {
|
||||
$swap = $list->limit(1, $page * $per)->first();
|
||||
$values[$swap->ID] = $swap->$field;
|
||||
|
||||
foreach($existing as $_id => $sort) {
|
||||
if($id != $_id) $order[] = $_id;
|
||||
}
|
||||
|
||||
$order[] = $swap->ID;
|
||||
$order[] = $id;
|
||||
} else {
|
||||
$this->httpError(400, 'Invalid page target');
|
||||
}
|
||||
|
||||
$this->reorderItems($list, $values, $order);
|
||||
|
||||
return $grid->FieldHolder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle saving when 'immediateUpdate' is disabled, otherwise this isn't
|
||||
* necessary for the default sort mode.
|
||||
*/
|
||||
public function handleSave(GridField $grid, DataObjectInterface $record) {
|
||||
if (!$this->immediateUpdate)
|
||||
{
|
||||
$value = $grid->Value();
|
||||
$sortedIDs = $this->getSortedIDs($value);
|
||||
if ($sortedIDs) {
|
||||
$this->executeReorder($grid, $sortedIDs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param GridField $grid
|
||||
* @param array $sortedIDs List of IDS, where the key is the sort field value to save
|
||||
* @return bool
|
||||
*/
|
||||
protected function executeReorder(GridField $grid, $sortedIDs) {
|
||||
if(!is_array($sortedIDs)) {
|
||||
return false;
|
||||
}
|
||||
$field = $this->getSortField();
|
||||
|
||||
$sortterm = '';
|
||||
if ($this->extraSortFields) {
|
||||
if (is_array($this->extraSortFields)) {
|
||||
foreach($this->extraSortFields as $col => $dir) {
|
||||
$sortterm .= "$col $dir, ";
|
||||
}
|
||||
} else {
|
||||
$sortterm = $this->extraSortFields.', ';
|
||||
}
|
||||
}
|
||||
$list = $grid->getList();
|
||||
$sortterm .= '"'.$this->getSortTable($list).'"."'.$field.'"';
|
||||
$items = $list->filter('ID', $sortedIDs)->sort($sortterm);
|
||||
|
||||
// Ensure that each provided ID corresponded to an actual object.
|
||||
if(count($items) != count($sortedIDs)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Populate each object we are sorting with a sort value.
|
||||
$this->populateSortValues($items);
|
||||
|
||||
// Generate the current sort values.
|
||||
if ($items instanceof ManyManyList)
|
||||
{
|
||||
$current = array();
|
||||
foreach ($items->toArray() as $record)
|
||||
{
|
||||
// NOTE: _SortColumn0 is the first ->sort() field
|
||||
// used by SS when functions are detected in a SELECT
|
||||
// or CASE WHEN.
|
||||
if (isset($record->_SortColumn0)) {
|
||||
$current[$record->ID] = $record->_SortColumn0;
|
||||
} else {
|
||||
$current[$record->ID] = $record->$field;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$current = $items->map('ID', $field)->toArray();
|
||||
}
|
||||
|
||||
// Perform the actual re-ordering.
|
||||
$this->reorderItems($list, $current, $sortedIDs);
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function reorderItems($list, array $values, array $sortedIDs) {
|
||||
$sortField = $this->getSortField();
|
||||
/** @var SS_List $map */
|
||||
$map = $list->map('ID', $sortField);
|
||||
//fix for versions of SS that return inconsistent types for `map` function
|
||||
if ($map instanceof SS_Map) {
|
||||
$map = $map->toArray();
|
||||
}
|
||||
|
||||
// If not a ManyManyList and using versioning, detect it.
|
||||
$isVersioned = false;
|
||||
$class = $list->dataClass();
|
||||
if ($class == $this->getSortTable($list)) {
|
||||
$isVersioned = $class::has_extension('Versioned');
|
||||
}
|
||||
|
||||
// Loop through each item, and update the sort values which do not
|
||||
// match to order the objects.
|
||||
if (!$isVersioned) {
|
||||
$sortTable = $this->getSortTable($list);
|
||||
$additionalSQL = '';
|
||||
$baseTable = $sortTable;
|
||||
if(class_exists($sortTable)) {
|
||||
$baseTable = singleton($sortTable)->baseTable();
|
||||
}
|
||||
$isBaseTable = ($baseTable == $sortTable);
|
||||
if(!$list instanceof ManyManyList && $isBaseTable){
|
||||
$additionalSQL = ', "LastEdited" = NOW()';
|
||||
}
|
||||
|
||||
foreach($sortedIDs as $sortValue => $id) {
|
||||
if($map[$id] != $sortValue) {
|
||||
DB::query(sprintf(
|
||||
'UPDATE "%s" SET "%s" = %d%s WHERE %s',
|
||||
$sortTable,
|
||||
$sortField,
|
||||
$sortValue,
|
||||
$additionalSQL,
|
||||
$this->getSortTableClauseForIds($list, $id)
|
||||
));
|
||||
|
||||
if(!$isBaseTable) {
|
||||
DB::query(sprintf(
|
||||
'UPDATE "%s" SET "LastEdited" = NOW() WHERE %s',
|
||||
$baseTable,
|
||||
$this->getSortTableClauseForIds($list, $id)
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// For versioned objects, modify them with the ORM so that the
|
||||
// *_versions table is updated. This ensures re-ordering works
|
||||
// similar to the SiteTree where you change the position, and then
|
||||
// you go into the record and publish it.
|
||||
foreach($sortedIDs as $sortValue => $id) {
|
||||
if($map[$id] != $sortValue) {
|
||||
$record = $class::get()->byID($id);
|
||||
$record->$sortField = $sortValue;
|
||||
$record->write();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->extend('onAfterReorderItems', $list);
|
||||
}
|
||||
|
||||
protected function populateSortValues(DataList $list) {
|
||||
$list = clone $list;
|
||||
$field = $this->getSortField();
|
||||
$table = $this->getSortTable($list);
|
||||
$clause = sprintf('"%s"."%s" = 0', $table, $this->getSortField());
|
||||
|
||||
$additionalSQL = '';
|
||||
$baseTable = $table;
|
||||
if(class_exists($table)) {
|
||||
$baseTable = singleton($table)->baseTable();
|
||||
}
|
||||
$isBaseTable = ($baseTable == $table);
|
||||
if(!$list instanceof ManyManyList && $isBaseTable){
|
||||
$additionalSQL = ', "LastEdited" = NOW()';
|
||||
}
|
||||
|
||||
foreach($list->where($clause)->column('ID') as $id) {
|
||||
$max = DB::query(sprintf('SELECT MAX("%s") + 1 FROM "%s"', $field, $table));
|
||||
$max = $max->value();
|
||||
|
||||
DB::query(sprintf(
|
||||
'UPDATE "%s" SET "%s" = %d%s WHERE %s',
|
||||
$table,
|
||||
$field,
|
||||
$max,
|
||||
$additionalSQL,
|
||||
$this->getSortTableClauseForIds($list, $id)
|
||||
));
|
||||
|
||||
if(!$isBaseTable) {
|
||||
DB::query(sprintf(
|
||||
'UPDATE "%s" SET "LastEdited" = NOW() WHERE %s',
|
||||
$baseTable,
|
||||
$this->getSortTableClauseForIds($list, $id)
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function getSortTableClauseForIds(DataList $list, $ids) {
|
||||
if(is_array($ids)) {
|
||||
$value = 'IN (' . implode(', ', array_map('intval', $ids)) . ')';
|
||||
} else {
|
||||
$value = '= ' . (int) $ids;
|
||||
}
|
||||
|
||||
if($list instanceof ManyManyList) {
|
||||
$extra = $list->getExtraFields();
|
||||
$key = $list->getLocalKey();
|
||||
$foreignKey = $list->getForeignKey();
|
||||
$foreignID = (int) $list->getForeignID();
|
||||
|
||||
if($extra && array_key_exists($this->getSortField(), $extra)) {
|
||||
return sprintf(
|
||||
'"%s" %s AND "%s" = %d',
|
||||
$key,
|
||||
$value,
|
||||
$foreignKey,
|
||||
$foreignID
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return "\"ID\" $value";
|
||||
}
|
||||
|
||||
}
|
@ -1,151 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* A base utility class for request handlers which present a grid field detail
|
||||
* view.
|
||||
*
|
||||
* This class provides some useful defaults for grid field detail views, such
|
||||
* as tabs, breadcrumbs and a back link. Much of this code is extracted from the
|
||||
* detail form.
|
||||
*/
|
||||
abstract class GridFieldRequestHandler extends RequestHandler {
|
||||
|
||||
private static $allowed_actions = array(
|
||||
'Form'
|
||||
);
|
||||
|
||||
/**
|
||||
* @var GridField
|
||||
*/
|
||||
protected $grid;
|
||||
|
||||
/**
|
||||
* @var GridFieldComponent
|
||||
*/
|
||||
protected $component;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $name;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $template = __CLASS__;
|
||||
|
||||
public function __construct(GridField $grid, GridFieldComponent $component, $name) {
|
||||
$this->grid = $grid;
|
||||
$this->component = $component;
|
||||
$this->name = $name;
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function index($request) {
|
||||
$result = $this->renderWith($this->template);
|
||||
|
||||
if($request->isAjax()) {
|
||||
return $result;
|
||||
} else {
|
||||
return $this->getTopLevelController()->customise(array(
|
||||
'Content' => $result
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
public function Link($action = null) {
|
||||
return Controller::join_links($this->grid->Link(), $this->name, $action);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method should be overloaded to build out the detail form.
|
||||
*
|
||||
* @return Form
|
||||
*/
|
||||
public function Form() {
|
||||
$form = new Form(
|
||||
$this,
|
||||
'Form',
|
||||
new FieldList($root = new TabSet('Root', new Tab('Main'))),
|
||||
new FieldList()
|
||||
);
|
||||
|
||||
if($this->getTopLevelController() instanceof LeftAndMain) {
|
||||
$form->setTemplate('LeftAndMain_EditForm');
|
||||
$form->addExtraClass('cms-content cms-edit-form cms-tabset center');
|
||||
$form->setAttribute('data-pjax-fragment', 'CurrentForm Content');
|
||||
|
||||
$root->setTemplate('CMSTabSet');
|
||||
$form->Backlink = $this->getBackLink();
|
||||
}
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Controller
|
||||
*/
|
||||
public function getController() {
|
||||
return $this->grid->getForm()->getController();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $template
|
||||
*/
|
||||
public function setTemplate($template) {
|
||||
$this->template = $template;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getTemplate() {
|
||||
return $this->template;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ArrayList
|
||||
*/
|
||||
public function getBreadcrumbs() {
|
||||
$controller = $this->getController();
|
||||
|
||||
if($controller->hasMethod('Breadcrumbs')) {
|
||||
return $controller->Breadcrumbs();
|
||||
} else {
|
||||
return new ArrayList();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function getBackLink() {
|
||||
$controller = $this->getTopLevelController();
|
||||
|
||||
if($controller->hasMethod('Backlink')) {
|
||||
return $controller->Backlink();
|
||||
} else {
|
||||
return $controller->Link();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Controller
|
||||
*/
|
||||
protected function getTopLevelController() {
|
||||
$controller = $this->getController();
|
||||
|
||||
while($controller) {
|
||||
if($controller instanceof GridFieldRequestHandler) {
|
||||
$controller = $controller->getController();
|
||||
} elseif($controller instanceof GridFieldDetailForm_ItemRequest) {
|
||||
$controller = $controller->getController();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $controller;
|
||||
}
|
||||
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* A simple header which displays column titles.
|
||||
*/
|
||||
class GridFieldTitleHeader implements GridField_HTMLProvider {
|
||||
|
||||
public function getHTMLFragments($grid) {
|
||||
$cols = new ArrayList();
|
||||
|
||||
foreach ($grid->getColumns() as $name) {
|
||||
$meta = $grid->getColumnMetadata($name);
|
||||
|
||||
$cols->push(new ArrayData(array(
|
||||
'Name' => $name,
|
||||
'Title' => $meta['title']
|
||||
)));
|
||||
}
|
||||
|
||||
return array(
|
||||
'header' => $cols->renderWith('GridFieldTitleHeader'),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
1
codecov.yml
Normal file
1
codecov.yml
Normal file
@ -0,0 +1 @@
|
||||
comment: false
|
@ -1,37 +1,54 @@
|
||||
{
|
||||
"name": "silverstripe-australia/gridfieldextensions",
|
||||
"description": "A collection of useful grid field components",
|
||||
"type": "silverstripe-module",
|
||||
"homepage": "http://github.com/silverstripe-australia/silverstripe-gridfieldextensions",
|
||||
"keywords": ["silverstripe", "gridfield"],
|
||||
"license": "BSD-3-Clause",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Andrew Short",
|
||||
"email": "andrewjshort@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Marcus Nyeholt",
|
||||
"email": "marcus@silverstripe.com.au"
|
||||
}
|
||||
],
|
||||
"support": {
|
||||
"issues": "http://github.com/silverstripe-australia/silverstripe-gridfieldextensions/issues"
|
||||
},
|
||||
"require": {
|
||||
"silverstripe/framework": "~3.1"
|
||||
},
|
||||
"extra": {
|
||||
"installer-name": "gridfieldextensions",
|
||||
"branch-alias": {
|
||||
"dev-master": "1.4.x-dev"
|
||||
},
|
||||
"screenshots": [
|
||||
"docs/en/_images/editable-rows.png",
|
||||
"docs/en/_images/add-existing-search.png"
|
||||
]
|
||||
},
|
||||
"replace": {
|
||||
"ajshort/silverstripe-gridfieldextensions": "self.version"
|
||||
}
|
||||
}
|
||||
{
|
||||
"name": "symbiote/silverstripe-gridfieldextensions",
|
||||
"description": "A collection of useful grid field components",
|
||||
"type": "silverstripe-vendormodule",
|
||||
"homepage": "http://github.com/symbiote/silverstripe-gridfieldextensions",
|
||||
"keywords": ["silverstripe", "gridfield", "sortable", "sort", "sort field"],
|
||||
"license": "BSD-3-Clause",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Andrew Short",
|
||||
"email": "andrewjshort@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Marcus Nyeholt",
|
||||
"email": "marcus@symbiote.com.au"
|
||||
}
|
||||
],
|
||||
"support": {
|
||||
"issues": "http://github.com/symbiote/silverstripe-gridfieldextensions/issues"
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.4 || ^8.0",
|
||||
"silverstripe/vendor-plugin": "^1.0",
|
||||
"silverstripe/framework": "^4.11"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^9.5",
|
||||
"squizlabs/php_codesniffer": "^3.0",
|
||||
"silverstripe/versioned": "^1"
|
||||
},
|
||||
"extra": {
|
||||
"screenshots": [
|
||||
"docs/en/_images/editable-rows.png",
|
||||
"docs/en/_images/add-existing-search.png"
|
||||
],
|
||||
"expose": [
|
||||
"css",
|
||||
"javascript",
|
||||
"client/lang"
|
||||
]
|
||||
},
|
||||
"replace": {
|
||||
"ajshort/silverstripe-gridfieldextensions": "self.version",
|
||||
"silverstripe-australia/gridfieldextensions": "self.version"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symbiote\\GridFieldExtensions\\": "src/",
|
||||
"Symbiote\\GridFieldExtensions\\Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"prefer-stable": true,
|
||||
"minimum-stability": "dev"
|
||||
}
|
||||
|
@ -1,184 +1,225 @@
|
||||
/**
|
||||
* GridFieldAddExistingSearchButton
|
||||
*/
|
||||
|
||||
.add-existing-search-dialog {
|
||||
min-width: inherit !important;
|
||||
}
|
||||
|
||||
.add-existing-search-dialog .add-existing-search-form .field {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.add-existing-search-dialog .add-existing-search-form .field label {
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
.add-existing-search-dialog .add-existing-search-form .Actions {
|
||||
margin-top: 10px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.add-existing-search-dialog .add-existing-search-items li a {
|
||||
background: #FFF;
|
||||
border-bottom-width: 1px;
|
||||
border-color: #CCC;
|
||||
border-left-width: 1px;
|
||||
border-right-width: 1px;
|
||||
border-style: solid;
|
||||
display: block;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.add-existing-search-dialog .add-existing-search-items li:first-child a {
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
border-top-width: 1px;
|
||||
}
|
||||
|
||||
.add-existing-search-dialog .add-existing-search-items li:last-child a {
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
|
||||
.add-existing-search-dialog .add-existing-search-items li a:hover {
|
||||
background: #F4F4F4;
|
||||
}
|
||||
|
||||
.add-existing-search-dialog .add-existing-search-pagination li {
|
||||
background: #FFF;
|
||||
display: block;
|
||||
float: left;
|
||||
margin-right: 2px;
|
||||
margin-top: 12px;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
/**
|
||||
* GridFieldAddNewInlineButton
|
||||
*/
|
||||
|
||||
.ss-gridfield-inline-new {
|
||||
background: #EFE;
|
||||
}
|
||||
|
||||
.ss-gridfield-inline-new:nth-child(2n) {
|
||||
background: #DFD;
|
||||
}
|
||||
|
||||
/**
|
||||
* GridFieldAddNewMultiClass
|
||||
*/
|
||||
|
||||
.ss-gridfield-add-new-multi-class {
|
||||
margin-bottom: 8px !important;
|
||||
}
|
||||
|
||||
.ss-gridfield-add-new-multi-class .field {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
float: left;
|
||||
margin: 0 4px 0 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* GridFieldEditableColumns
|
||||
*/
|
||||
|
||||
.ss-gridfield-editable .readonly {
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
|
||||
.ss-gridfield-editable input.text,
|
||||
.ss-gridfield-editable textarea,
|
||||
.ss-gridfield-editable select,
|
||||
.ss-gridfield-editable .TreeDropdownField {
|
||||
margin: 0 !important;
|
||||
max-width: none !important;
|
||||
}
|
||||
|
||||
.ss-gridfield-editable select.dropdown {
|
||||
border: 1px solid #b3b3b3;
|
||||
background-color: #fff;
|
||||
padding: 7px 7px;
|
||||
padding-left: 4px;
|
||||
line-height: 16px;
|
||||
-moz-border-radius: 4px;
|
||||
-webkit-border-radius: 4px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.ss-gridfield-add-new-inline {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.ss-gridfield-add-new-inline span.readonly {
|
||||
color: #FFF !important;
|
||||
}
|
||||
|
||||
.ss-gridfield-add-new-inline .col-buttons {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/**
|
||||
* GridFieldOrderableRows
|
||||
*/
|
||||
|
||||
.ss-gridfield-orderable thead tr th.col-Reorder span {
|
||||
padding: 0 !important;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.ss-gridfield-orderable .col-reorder {
|
||||
position: relative;
|
||||
padding: 0 !important;
|
||||
width: 16px !important;
|
||||
}
|
||||
|
||||
.ss-gridfield-orderable .col-reorder .handle {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
.ss-gridfield-orderable .col-reorder .handle .icon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 5px;
|
||||
height: 11px;
|
||||
margin: -5px 0 0 -2px;
|
||||
background-image: url('../../framework/thirdparty/jquery-ui-themes/smoothness/images/ui-icons_222222_256x240.png');
|
||||
background-position: -5px -227px;
|
||||
}
|
||||
|
||||
.ss-gridfield-orderhelper {
|
||||
border-bottom: 1px solid rgba(0, 0, 0, .1);
|
||||
border-top: 1px solid rgba(0, 0, 0, .1);
|
||||
box-shadow: 0 0 8px rgba(0, 0, 0, .4);
|
||||
}
|
||||
|
||||
.ss-gridfield-orderable tfoot .ui-droppable {
|
||||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
}
|
||||
|
||||
.ss-gridfield-orderable tfoot .ui-droppable-active {
|
||||
background-color: #D4CF90 !important;
|
||||
}
|
||||
|
||||
.ss-gridfield-orderable tfoot .ss-gridfield-previouspage {
|
||||
background-position: -16px 9px !important;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.ss-gridfield-orderable tfoot .ss-gridfield-nextpage {
|
||||
background-position: -40px 9px !important;
|
||||
margin-right: 0;
|
||||
}
|
||||
/**
|
||||
* GridFieldAddExistingSearchButton
|
||||
*/
|
||||
|
||||
.add-existing-search-dialog {
|
||||
min-width: inherit !important;
|
||||
}
|
||||
|
||||
.add-existing-search-dialog .add-existing-search-form .field {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.add-existing-search-dialog .add-existing-search-form .field label {
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.add-existing-search-dialog .add-existing-search-form .Actions {
|
||||
margin-top: 10px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.add-existing-search-dialog .list-group-item {
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
.add-existing-search-dialog .btn-toolbar {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
/**
|
||||
* GridFieldAddNewInlineButton
|
||||
*/
|
||||
|
||||
.ss-gridfield-inline-new {
|
||||
background: #EFE;
|
||||
}
|
||||
|
||||
.ss-gridfield-inline-new:nth-child(2n) {
|
||||
background: #DFD;
|
||||
}
|
||||
|
||||
.grid-field__table .form-check-input.editable-column-field {
|
||||
margin-top: .9rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/**
|
||||
* GridFieldAddNewMultiClass
|
||||
*/
|
||||
|
||||
.ss-gridfield-add-new-multi-class {
|
||||
margin-bottom: 8px !important;white-space: nowrap;
|
||||
}
|
||||
|
||||
.ss-gridfield-add-new-multi-class .field {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
float: left;
|
||||
margin: 0 4px 0 0;
|
||||
}
|
||||
|
||||
.ss-gridfield-add-new-multi-class .form-group {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.cms-edit-form:not(.AssetAdmin) .ss-gridfield-add-new-multi-class .form-group .form__field-holder {
|
||||
display: inline;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.cms-edit-form:not(.AssetAdmin) .ss-gridfield-add-new-multi-class .form-group {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.ss-gridfield-add-new-multi-class .form-group:after {
|
||||
border:0;
|
||||
}
|
||||
|
||||
.btn__addnewmulticlass {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
line-height: 1.85em;
|
||||
}
|
||||
|
||||
.ss-gridfield-add-new-multi-class .chosen-container-active.chosen-with-drop .chosen-single,
|
||||
.ss-gridfield-add-new-multi-class .chosen-container-single .chosen-single {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
/**
|
||||
* GridFieldEditableColumns
|
||||
*/
|
||||
|
||||
.ss-gridfield-editable .readonly {
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
|
||||
.ss-gridfield-editable input.text,
|
||||
.ss-gridfield-editable textarea,
|
||||
.ss-gridfield-editable select,
|
||||
.ss-gridfield-editable .TreeDropdownField {
|
||||
margin: 0 !important;
|
||||
max-width: none !important;
|
||||
}
|
||||
|
||||
.ss-gridfield-editable select.dropdown {
|
||||
border: 1px solid #b3b3b3;
|
||||
background-color: #fff;
|
||||
padding: 7px 7px 7px 4px;
|
||||
line-height: 16px;
|
||||
-moz-border-radius: 4px;
|
||||
-webkit-border-radius: 4px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.ss-gridfield-add-new-inline {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.ss-gridfield-add-new-inline span.readonly {
|
||||
color: #FFF !important;
|
||||
}
|
||||
|
||||
.ss-gridfield-add-new-inline .col-buttons {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/**
|
||||
* GridFieldOrderableRows
|
||||
*/
|
||||
|
||||
.ss-gridfield-orderable thead tr th.col-Reorder span {
|
||||
padding: 0 !important;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.ss-gridfield-orderable .col-reorder {
|
||||
position: relative;
|
||||
padding: 0 !important;
|
||||
width: 16px !important;
|
||||
}
|
||||
|
||||
.ss-gridfield-orderable .col-reorder .handle {
|
||||
|
||||
cursor: move;padding: 16px 0 11px;
|
||||
}
|
||||
|
||||
.ss-gridfield-orderable .col-reorder .handle .icon {
|
||||
line-height: 100%;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.ss-gridfield-orderhelper {
|
||||
border-bottom: 1px solid rgba(0, 0, 0, .1);
|
||||
border-top: 1px solid rgba(0, 0, 0, .1);
|
||||
box-shadow: 0 0 8px rgba(0, 0, 0, .4);
|
||||
}
|
||||
|
||||
.ss-gridfield-orderable tfoot .ui-droppable {
|
||||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
}
|
||||
|
||||
.ss-gridfield-orderable tfoot .ui-droppable-active {
|
||||
background-color: #D4CF90 !important;
|
||||
}
|
||||
|
||||
.ss-gridfield-orderable tfoot .ss-gridfield-previouspage {
|
||||
background-position: -16px 9px !important;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.ss-gridfield-orderable tfoot .ss-gridfield-nextpage {
|
||||
background-position: -40px 9px !important;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* GridFieldConfigurablePaginator
|
||||
*/
|
||||
.ss-gridfield-configurable-paginator .pagination-page-size {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.ss-gridfield-configurable-paginator .pagination-page-size-select {
|
||||
margin-left: .5rem;
|
||||
}
|
||||
|
||||
.ss-gridfield-configurable-paginator .ss-gridfield-pagesize-submit {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ss-gridfield-configurable-paginator .pagination-page-number input {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.grid-field-inline-new--multi-class-list {
|
||||
display: none;
|
||||
background-color: #008a00;
|
||||
border-radius: 5px;
|
||||
bottom: 3em;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
right: 3em;
|
||||
padding: 10px;
|
||||
position: fixed;
|
||||
|
||||
}
|
||||
.grid-field-inline-new--multi-class-list a {
|
||||
color: #FFF !important;
|
||||
display: block;
|
||||
margin: 5px -10px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
.grid-field-inline-new--multi-class-list__visible {
|
||||
display: block;
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ existing records than a basic autocomplete. It uses the search context construct
|
||||
class to provide the search form.
|
||||
|
||||
```php
|
||||
$grid->getConfig()->addComponent(new GridFieldAddExistingSearchButton());
|
||||
$grid->getConfig()->addComponent(GridFieldAddExistingSearchButton::create());
|
||||
```
|
||||
|
||||
Inline Editing
|
||||
@ -19,17 +19,17 @@ This example replaces the default data columns component with an inline editable
|
||||
default add new button with one that adds new records inline.
|
||||
|
||||
```php
|
||||
$grid = new GridField(
|
||||
$grid = GridField::create(
|
||||
'ExampleGrid',
|
||||
'Example Grid',
|
||||
$this->Items(),
|
||||
GridFieldConfig::create()
|
||||
->addComponent(new GridFieldButtonRow('before'))
|
||||
->addComponent(new GridFieldToolbarHeader())
|
||||
->addComponent(new GridFieldTitleHeader())
|
||||
->addComponent(new GridFieldEditableColumns())
|
||||
->addComponent(new GridFieldDeleteAction())
|
||||
->addComponent(new GridFieldAddNewInlineButton())
|
||||
->addComponent(GridFieldButtonRow::create('before'))
|
||||
->addComponent(GridFieldToolbarHeader::create())
|
||||
->addComponent(GridFieldTitleHeader::create())
|
||||
->addComponent(GridFieldEditableColumns::create())
|
||||
->addComponent(GridFieldDeleteAction::create())
|
||||
->addComponent(GridFieldAddNewInlineButton::create())
|
||||
);
|
||||
```
|
||||
|
||||
@ -37,13 +37,13 @@ You can customise the form fields that are used in the grid by calling `setDispl
|
||||
inline editing component. By default field scaffolding will be used.
|
||||
|
||||
```php
|
||||
$grid->getConfig()->getComponentByType('GridFieldEditableColumns')->setDisplayFields(array(
|
||||
$grid->getConfig()->getComponentByType(GridFieldEditableColumns::class)->setDisplayFields(array(
|
||||
'FirstField' => function($record, $column, $grid) {
|
||||
return new TextField($column);
|
||||
return TextField::create($column);
|
||||
},
|
||||
'SecondField' => array(
|
||||
'title' => 'Custom Title',
|
||||
'field' => 'ReadonlyField'
|
||||
'field' => ReadonlyField::class
|
||||
),
|
||||
'ThirdField' => array(
|
||||
'title' => 'Custom Title Two',
|
||||
@ -65,9 +65,11 @@ a new record. By default it allows them to select the model class for the grid f
|
||||
subclasses. You can control the createable classes using the `setClasses` method.
|
||||
|
||||
```php
|
||||
use SilverStripe\Forms\GridField\GridFieldAddNewButton;
|
||||
|
||||
$grid->getConfig()
|
||||
->removeComponentsByType('GridFieldAddNewButton')
|
||||
->addComponent(new GridFieldAddNewMultiClass());
|
||||
->removeComponentsByType(GridFieldAddNewButton::class)
|
||||
->addComponent(GridFieldAddNewMultiClass::create());
|
||||
```
|
||||
|
||||
Orderable Rows
|
||||
@ -80,10 +82,10 @@ the relationship.
|
||||
|
||||
```php
|
||||
// Basic usage, defaults to "Sort" for the sort field.
|
||||
$grid->getConfig()->addComponent(new GridFieldOrderableRows());
|
||||
$grid->getConfig()->addComponent(GridFieldOrderableRows::create());
|
||||
|
||||
// Specifying the sort field.
|
||||
$grid->getConfig()->addComponent(new GridFieldOrderableRows('SortField'));
|
||||
$grid->getConfig()->addComponent(GridFieldOrderableRows::create('SortField'));
|
||||
```
|
||||
|
||||
By default, when you create a new item, it is created with a sort order of "0" - that is, it is added
|
||||
@ -104,5 +106,49 @@ class Item extends DataObject {
|
||||
}
|
||||
```
|
||||
|
||||
### Versioning
|
||||
By default `GridFieldOrderableRows` will handle versioning but won't automatically publish any records. The user will need to go into each record and publish them manually which could get cumbersome for large lists.
|
||||
|
||||
You can configure the list to automatically publish a record if the record is the latest version and is already published. This won't publish any records which have draft changes.
|
||||
|
||||
```php
|
||||
$orderable = GridFieldOrderableRows::create()->setRepublishLiveRecords(true);
|
||||
```
|
||||
|
||||
There are caveats with both approaches so consideration should be made for which approach best suits the requirements.
|
||||
|
||||
**Please NOTE:** There is a limitation when using `GridFieldOrderableRows` on unsaved data objects; namely, that it doesn't work as without data being saved, the list of related objects has no context. Please check `$this->ID` before adding the `GridFieldOrderableRows` component to the grid field config (or even, before adding the gridfield at all).
|
||||
|
||||
Configurable Paginator
|
||||
----------------------
|
||||
|
||||
The `GridFieldConfigurablePaginator` component allows you to have a page size dropdown added to your GridField
|
||||
pagination controls. The page sizes are configurable via the configuration system, or at call time using the public API.
|
||||
To use this component you should remove the original paginator component first:
|
||||
|
||||
```php
|
||||
$gridField->getConfig()
|
||||
->removeComponentsByType('GridFieldPaginator')
|
||||
->addComponent(GridFieldConfigurablePaginator::create());
|
||||
```
|
||||
|
||||
You can configure the page sizes with the configuration system. Note that merging is the default strategy, so to replace
|
||||
the default sizes with your own you will need to unset the original first, for example:
|
||||
|
||||
```php
|
||||
# File: mysite/_config.php
|
||||
Config::inst()->remove('GridFieldConfigurablePaginator', 'default_page_sizes');
|
||||
Config::inst()->update('GridFieldConfigurablePaginator', 'default_page_sizes', array(100, 200, 500));
|
||||
```
|
||||
|
||||
You can also override these at call time:
|
||||
|
||||
```php
|
||||
$paginator = GridFieldConfigurablePaginator::create(100, array(100, 200, 500));
|
||||
|
||||
$paginator->setPageSizes(array(200, 500, 1000));
|
||||
$paginator->setItemsPerPage(500);
|
||||
```
|
||||
|
||||
The first shown record will be maintained across page size changes, and the number of pages and current page will be
|
||||
recalculated on each request, based on the current first shown record and page size.
|
||||
|
@ -44,6 +44,15 @@
|
||||
}
|
||||
});
|
||||
|
||||
// Allow the list item to be clickable as well as the anchor
|
||||
$('.add-existing-search-dialog .add-existing-search-items .list-group-item-action').entwine({
|
||||
onclick: function() {
|
||||
if (this.children('a').length > 0) {
|
||||
this.children('a').first().trigger('click');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$(".add-existing-search-dialog .add-existing-search-items a").entwine({
|
||||
onclick: function() {
|
||||
var link = this.closest(".add-existing-search-items").data("add-link");
|
||||
@ -199,7 +208,7 @@
|
||||
}
|
||||
});
|
||||
|
||||
$(".ss-gridfield-delete-inline").entwine({
|
||||
$(".grid-field .action.ss-gridfield-delete-inline").entwine({
|
||||
onclick: function() {
|
||||
var msg = ss.i18n._t("GridFieldExtensions.CONFIRMDEL", "Are you sure you want to delete this?");
|
||||
|
||||
@ -215,19 +224,41 @@
|
||||
* GridFieldAddNewMultiClass
|
||||
*/
|
||||
|
||||
$(".ss-gridfield-add-new-multi-class .ss-ui-button").entwine({
|
||||
$(".ss-gridfield-add-new-multi-class .btn__addnewmulticlass").entwine({
|
||||
onclick: function() {
|
||||
var link = this.data("href");
|
||||
var cls = this.parents(".ss-gridfield-add-new-multi-class").find("select").val();
|
||||
|
||||
if(cls && cls.length) {
|
||||
this.getGridField().showDetailView(link.replace("{class}", cls));
|
||||
this.getGridField().showDetailView(link.replace("{class}", encodeURI(cls)));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
$(".action--new__multi-class").entwine({
|
||||
onmatch: function () {
|
||||
const hrefTemplate = this.data('hrefTemplate');
|
||||
const classes = this.data('classes');
|
||||
const liHtml = Object.keys(classes).map(className => {
|
||||
const link = hrefTemplate.replace('{class}', className);
|
||||
const linkText = ss.i18n.inject(
|
||||
ss.i18n._t('GridFieldExtensions.ADD_CLASS', 'Add: <i>{classname}</i>'),
|
||||
{classname: classes[className]}
|
||||
);
|
||||
return `<li><a href="${link}">${linkText}</a></li>`;
|
||||
});
|
||||
|
||||
const listElement = $(`<ul class="grid-field-inline-new--multi-class-list">${liHtml.join('')}</ul>`);
|
||||
listElement.insertBefore(this);
|
||||
|
||||
this.on('click', function () {
|
||||
listElement.toggleClass('grid-field-inline-new--multi-class-list__visible');
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
$(".ss-gridfield-add-new-multi-class select").entwine({
|
||||
onadd: function() {
|
||||
this.update();
|
||||
@ -236,12 +267,12 @@
|
||||
this.update();
|
||||
},
|
||||
update: function() {
|
||||
var btn = this.parents(".ss-gridfield-add-new-multi-class").find(".ss-ui-button");
|
||||
var btn = this.parents(".ss-gridfield-add-new-multi-class").find('[data-add-multiclass]');
|
||||
|
||||
if(this.val() && this.val().length) {
|
||||
btn.button("enable");
|
||||
btn.removeClass('disabled');
|
||||
} else {
|
||||
btn.button("disable");
|
||||
btn.addClass('disabled');
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -250,7 +281,7 @@
|
||||
* GridFieldEditableColumns
|
||||
*/
|
||||
|
||||
$('.ss-gridfield.ss-gridfield-editable .ss-gridfield-item td').entwine({
|
||||
$('.ss-gridfield-editable .ss-gridfield-item').entwine({
|
||||
onclick: function(e) {
|
||||
// Prevent the default row click action when clicking a cell that contains a field
|
||||
if (this.find('.editable-column-field').length) {
|
||||
@ -264,6 +295,89 @@
|
||||
*/
|
||||
|
||||
$(".ss-gridfield-orderable tbody").entwine({
|
||||
// reload the gridfield without triggering the change event
|
||||
// this is because the change has already been saved by reorder action
|
||||
reload: function (ajaxOpts, successCallback) {
|
||||
var self = this.getGridField(), form = this.closest('form'),
|
||||
focusedElName = this.find(':input:focus').attr('name'), // Save focused element for restoring after refresh
|
||||
data = form.find(':input').serializeArray();
|
||||
|
||||
if (!ajaxOpts) {
|
||||
ajaxOpts = {};
|
||||
}
|
||||
if (!ajaxOpts.data) {
|
||||
ajaxOpts.data = [];
|
||||
}
|
||||
ajaxOpts.data = ajaxOpts.data.concat(data);
|
||||
|
||||
// Include any GET parameters from the current URL, as the view state might depend on it.
|
||||
// For example, a list prefiltered through external search criteria might be passed to GridField.
|
||||
if (window.location.search) {
|
||||
ajaxOpts.data = window.location.search.replace(/^\?/, '') + '&' + $.param(ajaxOpts.data);
|
||||
}
|
||||
|
||||
form.addClass('loading');
|
||||
|
||||
$.ajax($.extend({}, {
|
||||
headers: {"X-Pjax": 'CurrentField'},
|
||||
type: "POST",
|
||||
url: this.data('url'),
|
||||
dataType: 'html',
|
||||
success: function (data) {
|
||||
// Replace the grid field with response, not the form.
|
||||
// TODO Only replaces all its children, to avoid replacing the current scope
|
||||
// of the executing method. Means that it doesn't retrigger the onmatch() on the main container.
|
||||
self.empty().append($(data).children());
|
||||
|
||||
// Refocus previously focused element. Useful e.g. for finding+adding
|
||||
// multiple relationships via keyboard.
|
||||
if (focusedElName) self.find(':input[name="' + focusedElName + '"]').focus();
|
||||
|
||||
// Update filter
|
||||
if (self.find('.grid-field__filter-header').length) {
|
||||
var content;
|
||||
if (ajaxOpts.data[0].filter == "show") {
|
||||
content = '<span class="non-sortable"></span>';
|
||||
self.addClass('show-filter').find('.grid-field__filter-header').show();
|
||||
} else {
|
||||
const contentTitle = ss.i18n._t('GridFieldExtensions.OPEN_SEARCH_FILTER', 'Open search and filter');
|
||||
content = `<button type="button" title="${contentTitle}" name="showFilter" class="btn btn-secondary font-icon-search btn--no-text btn--icon-large grid-field__filter-open"></button>`;
|
||||
self.removeClass('show-filter').find('.grid-field__filter-header').hide();
|
||||
}
|
||||
|
||||
self.find('.sortable-header th:last').html(content);
|
||||
}
|
||||
|
||||
// update CMS preview
|
||||
var preview = $('.cms-preview');
|
||||
if (preview.length) {
|
||||
preview.entwine('.ss.preview')._initialiseFromContent();
|
||||
}
|
||||
|
||||
form.removeClass('loading');
|
||||
if (successCallback) {
|
||||
successCallback.apply(this, arguments);
|
||||
}
|
||||
self.trigger('reload', self);
|
||||
|
||||
// update publish button if necessary
|
||||
const publish = $('#Form_EditForm_action_publish');
|
||||
|
||||
// button needs to be updated only if it's in published state
|
||||
if (publish.length > 0 && publish.hasClass('btn-outline-primary')) {
|
||||
publish.removeClass('btn-outline-primary');
|
||||
publish.removeClass('font-icon-tick');
|
||||
publish.addClass('btn-primary');
|
||||
publish.addClass('font-icon-rocket');
|
||||
publish.find('.btn__title').html(ss.i18n._t('GridFieldExtensions.SAVE_PUBLISH', 'Save & publish'));
|
||||
}
|
||||
},
|
||||
error: function (e) {
|
||||
alert(i18n._t('Admin.ERRORINTRANSACTION'));
|
||||
form.removeClass('loading');
|
||||
}
|
||||
}, ajaxOpts));
|
||||
},
|
||||
rebuildSort: function() {
|
||||
var grid = this.getGridField();
|
||||
|
||||
@ -320,7 +434,7 @@
|
||||
var grid = self.getGridField();
|
||||
if (grid.data("immediate-update") && postback)
|
||||
{
|
||||
grid.reload({
|
||||
self.reload({
|
||||
url: grid.data("url-reorder")
|
||||
});
|
||||
}
|
||||
@ -387,5 +501,14 @@
|
||||
if(this.hasClass("ui-droppable")) this.droppable("destroy");
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GridFieldConfigurablePaginator
|
||||
*/
|
||||
$('.ss-gridfield-configurable-paginator .pagination-page-size-select').entwine({
|
||||
onchange: function () {
|
||||
this.parent().find('.ss-gridfield-pagesize-submit').trigger('click');
|
||||
}
|
||||
});
|
||||
});
|
||||
})(jQuery);
|
||||
|
85
javascript/tmpl.js
Normal file
85
javascript/tmpl.js
Normal file
@ -0,0 +1,85 @@
|
||||
/*
|
||||
* JavaScript Templates 1.0.2
|
||||
* https://github.com/blueimp/JavaScript-Templates
|
||||
*
|
||||
* Copyright 2011, Sebastian Tschan
|
||||
* https://blueimp.net
|
||||
*
|
||||
* Licensed under the MIT license:
|
||||
* http://www.opensource.org/licenses/MIT
|
||||
*
|
||||
* Inspired by John Resig's JavaScript Micro-Templating:
|
||||
* http://ejohn.org/blog/javascript-micro-templating/
|
||||
*/
|
||||
|
||||
/*jslint evil: true, regexp: true */
|
||||
/*global document, define */
|
||||
|
||||
(function ($) {
|
||||
"use strict";
|
||||
var tmpl = function (str, data) {
|
||||
var f = !/[^\-\w]/.test(str) ? tmpl.cache[str] = tmpl.cache[str] ||
|
||||
tmpl(tmpl.load(str)) :
|
||||
new Function(
|
||||
tmpl.arg,
|
||||
("var _s=''" + tmpl.helper + ";_s+='" +
|
||||
str.replace(tmpl.regexp, tmpl.func) +
|
||||
"';return _s;").split("_s+='';").join("")
|
||||
);
|
||||
f.tmpl = f.tmpl || tmpl;
|
||||
return data ? f(data) : f;
|
||||
};
|
||||
tmpl.cache = {};
|
||||
tmpl.load = function (id) {
|
||||
return document.getElementById(id).innerHTML;
|
||||
};
|
||||
tmpl.regexp = /(\s+)|('|\\)(?![^%]*%\})|(?:\{%(=|#)(.+?)%\})|(\{%)|(%\})/g;
|
||||
tmpl.func = function (s, p1, p2, p3, p4, p5, p6, o, str) {
|
||||
if (p1) { // whitespace
|
||||
return o && o + s.length !== str.length ? " " : "";
|
||||
}
|
||||
if (p2) { // single quote or backslash
|
||||
return "\\" + s;
|
||||
}
|
||||
if (p3) { // interpolation: {%=prop%}, or unescaped: {%#prop%}
|
||||
if (p3 === "=") {
|
||||
return "'+_e(" + p4 + ")+'";
|
||||
}
|
||||
return "'+(" + p4 + "||'')+'";
|
||||
}
|
||||
if (p5) { // evaluation start tag: {%
|
||||
return "';";
|
||||
}
|
||||
if (p6) { // evaluation end tag: %}
|
||||
return "_s+='";
|
||||
}
|
||||
};
|
||||
tmpl.encReg = /[<>&"\x00]/g;
|
||||
tmpl.encMap = {
|
||||
"<": "<",
|
||||
">": ">",
|
||||
"&": "&",
|
||||
"\"": """,
|
||||
"\x00": ""
|
||||
};
|
||||
tmpl.encode = function (s) {
|
||||
return String(s || "").replace(
|
||||
tmpl.encReg,
|
||||
function (c) {
|
||||
return tmpl.encMap[c];
|
||||
}
|
||||
);
|
||||
};
|
||||
tmpl.arg = "o";
|
||||
tmpl.helper = ",_t=arguments.callee.tmpl,_e=_t.encode" +
|
||||
",print=function(s,e){_s+=e&&(s||'')||_e(s);}" +
|
||||
",include=function(s,d){_s+=_t(s,d);}";
|
||||
if (typeof define === "function" && define.amd) {
|
||||
// Register as an AMD module:
|
||||
define("tmpl", function () {
|
||||
return tmpl;
|
||||
});
|
||||
} else {
|
||||
$.tmpl = tmpl;
|
||||
}
|
||||
}(this));
|
@ -1,9 +1,9 @@
|
||||
de_DE:
|
||||
GridFieldAddExistingSearchHandler.ss:
|
||||
NOITEMS: 'Kein Ergebnis'
|
||||
RESULTS: 'Ergebnisse'
|
||||
RESULTS: Ergebnisse
|
||||
GridFieldExtensions:
|
||||
ADD: 'Hinzufügen'
|
||||
ADD: Hinzufügen
|
||||
ADDEXISTING: 'Bestehenden Eintrag hinzufügen'
|
||||
SEARCH: 'Suche'
|
||||
SEARCH: Suche
|
||||
SELECTTYPETOCREATE: '(Bitte Typ auswählen)'
|
||||
|
16
lang/en.yml
Normal file
16
lang/en.yml
Normal file
@ -0,0 +1,16 @@
|
||||
en:
|
||||
GridFieldExtensions:
|
||||
ADD: Add
|
||||
ADDEXISTING: 'Add Existing'
|
||||
BACK: Back
|
||||
CURRENT: (current)
|
||||
NOITEMS: 'There are no items.'
|
||||
Next: Next
|
||||
PREVIOUS: Previous
|
||||
RESULTS: Results
|
||||
SEARCH: Search
|
||||
SELECTTYPETOCREATE: '(Select type to create)'
|
||||
Symbiote\GridFieldExtensions\Extensions\GridFieldDetailFormItemRequestExtension:
|
||||
NEW: 'Add new record'
|
||||
Symbiote\GridFieldExtensions\GridFieldConfigurablePaginator:
|
||||
SHOW: Show
|
16
lang/eo.yml
Normal file
16
lang/eo.yml
Normal file
@ -0,0 +1,16 @@
|
||||
eo:
|
||||
GridFieldExtensions:
|
||||
ADD: Aldoni
|
||||
ADDEXISTING: 'Aldoni ekzistantan'
|
||||
BACK: Antaŭa
|
||||
CURRENT: (aktuala)
|
||||
NOITEMS: 'Mankas elementoj'
|
||||
Next: Sekva
|
||||
PREVIOUS: Antaŭa
|
||||
RESULTS: Rezultoj
|
||||
SEARCH: Serĉi
|
||||
SELECTTYPETOCREATE: '(Elektu tipon kreotan)'
|
||||
Symbiote\GridFieldExtensions\Extensions\GridFieldDetailFormItemRequestExtension:
|
||||
NEW: 'Aldoni novan rikordon'
|
||||
Symbiote\GridFieldExtensions\GridFieldConfigurablePaginator:
|
||||
SHOW: Vidigi
|
@ -1,9 +1,9 @@
|
||||
es_ES:
|
||||
GridFieldAddExistingSearchHandler.ss:
|
||||
NOITEMS: 'No hay items.'
|
||||
RESULTS: 'Resultados'
|
||||
RESULTS: Resultados
|
||||
GridFieldExtensions:
|
||||
ADD: 'Agregar'
|
||||
ADD: Agregar
|
||||
ADDEXISTING: 'Agregar existente'
|
||||
SEARCH: 'Buscar'
|
||||
SEARCH: Buscar
|
||||
SELECTTYPETOCREATE: '(Seleccionar tipo para crear)'
|
||||
|
@ -1,7 +1,7 @@
|
||||
et_EE:
|
||||
GridFieldAddExistingSearchHandler.ss:
|
||||
NOITEMS: 'Kirjed puuduvad.'
|
||||
RESULTS: 'Tulemused'
|
||||
RESULTS: Tulemused
|
||||
GridFieldExtensions:
|
||||
ADD: Lisa
|
||||
ADDEXISTING: 'Lisa olemasolev'
|
||||
|
@ -1,9 +1,9 @@
|
||||
fi_FI:
|
||||
GridFieldAddExistingSearchHandler.ss:
|
||||
NOITEMS: 'Ei kohteita'
|
||||
RESULTS: 'Tulokset'
|
||||
RESULTS: Tulokset
|
||||
GridFieldExtensions:
|
||||
ADD: 'Lisää'
|
||||
ADD: Lisää
|
||||
ADDEXISTING: 'Lisää olemassa oleva'
|
||||
SEARCH: 'Etsi'
|
||||
SEARCH: Etsi
|
||||
SELECTTYPETOCREATE: 'Valitse lisättävä tyyppi'
|
||||
|
@ -1,9 +1,9 @@
|
||||
it_IT:
|
||||
GridFieldAddExistingSearchHandler.ss:
|
||||
NOITEMS: 'Nessun elemento.'
|
||||
RESULTS: 'Risultati'
|
||||
RESULTS: Risultati
|
||||
GridFieldExtensions:
|
||||
ADD: 'Aggiungi'
|
||||
ADD: Aggiungi
|
||||
ADDEXISTING: 'Aggiungi esistente'
|
||||
SEARCH: 'Cerca'
|
||||
SEARCH: Cerca
|
||||
SELECTTYPETOCREATE: '(Seleziona tipo per creare)'
|
||||
|
@ -1,9 +1,19 @@
|
||||
nl_NL:
|
||||
GridFieldAddExistingSearchHandler.ss:
|
||||
NOITEMS: 'Geen resultaten gevonden.'
|
||||
RESULTS: 'Resultaten'
|
||||
RESULTS: Resultaten
|
||||
GridFieldExtensions:
|
||||
ADD: 'Toevoegen'
|
||||
ADD: Toevoegen
|
||||
ADDEXISTING: 'Bestaande toevoegen'
|
||||
SEARCH: 'Zoeken'
|
||||
BACK: Terug
|
||||
CURRENT: (huidige)
|
||||
NOITEMS: 'Er zijn geen items.'
|
||||
Next: Volgende
|
||||
PREVIOUS: Vorige
|
||||
RESULTS: Resultaten
|
||||
SEARCH: Zoeken
|
||||
SELECTTYPETOCREATE: '(Selecteer type om te creeën)'
|
||||
Symbiote\GridFieldExtensions\Extensions\GridFieldDetailFormItemRequestExtension:
|
||||
NEW: 'Nieuw item maken'
|
||||
Symbiote\GridFieldExtensions\GridFieldConfigurablePaginator:
|
||||
SHOW: Toon
|
||||
|
9
lang/ru_RU.yml
Normal file
9
lang/ru_RU.yml
Normal file
@ -0,0 +1,9 @@
|
||||
ru_RU:
|
||||
GridFieldAddExistingSearchHandler.ss:
|
||||
NOITEMS: 'Нет элементов.'
|
||||
RESULTS: Результаты
|
||||
GridFieldExtensions:
|
||||
ADD: Добавить
|
||||
ADDEXISTING: 'Добавить существующий'
|
||||
SEARCH: Поиск
|
||||
SELECTTYPETOCREATE: '(Выберите тип для создания)'
|
16
lang/sk.yml
16
lang/sk.yml
@ -1,9 +1,19 @@
|
||||
sk:
|
||||
GridFieldAddExistingSearchHandler.ss:
|
||||
NOITEMS: 'Nie sú tu žiadne položky.'
|
||||
RESULTS: 'Výsledky'
|
||||
RESULTS: Výsledky
|
||||
GridFieldExtensions:
|
||||
ADD: Pridať
|
||||
ADDEXISTING: Pridať existujúci
|
||||
ADDEXISTING: 'Pridať existujúci'
|
||||
BACK: Späť
|
||||
CURRENT: (aktuálny)
|
||||
NOITEMS: 'Nie sú k dispozícii žiadne položky.'
|
||||
Next: Ďalší
|
||||
PREVIOUS: Predchádzajúci
|
||||
RESULTS: Výsledky
|
||||
SEARCH: Hľadať
|
||||
SELECTTYPETOCREATE: '(Prosím, vyberte typ)'
|
||||
SELECTTYPETOCREATE: '(Vyberte typ na vytvorenie)'
|
||||
Symbiote\GridFieldExtensions\Extensions\GridFieldDetailFormItemRequestExtension:
|
||||
NEW: 'Pridať nový záznam'
|
||||
Symbiote\GridFieldExtensions\GridFieldConfigurablePaginator:
|
||||
SHOW: Zobraziť
|
||||
|
16
lang/sl.yml
Normal file
16
lang/sl.yml
Normal file
@ -0,0 +1,16 @@
|
||||
sl:
|
||||
GridFieldExtensions:
|
||||
ADD: Dodaj
|
||||
ADDEXISTING: 'Dodaj obstoječe'
|
||||
BACK: Nazaj
|
||||
CURRENT: (trenutno)
|
||||
NOITEMS: 'Ni nobenih elementov.'
|
||||
Next: Naslednja
|
||||
PREVIOUS: Prejšnja
|
||||
RESULTS: Rezultati
|
||||
SEARCH: Iskanje
|
||||
SELECTTYPETOCREATE: '(Izberite tip, ki ga želite ustvariti)'
|
||||
Symbiote\GridFieldExtensions\Extensions\GridFieldDetailFormItemRequestExtension:
|
||||
NEW: 'Dodaj zapis'
|
||||
Symbiote\GridFieldExtensions\GridFieldConfigurablePaginator:
|
||||
SHOW: Prikaži
|
16
lang/sv.yml
Normal file
16
lang/sv.yml
Normal file
@ -0,0 +1,16 @@
|
||||
sv:
|
||||
GridFieldExtensions:
|
||||
ADD: 'Lägg till'
|
||||
ADDEXISTING: 'Lägg till existerande'
|
||||
BACK: Tillbaka
|
||||
CURRENT: (befintliga)
|
||||
NOITEMS: 'Hittade ingen data.'
|
||||
Next: Nästa
|
||||
PREVIOUS: Föregående
|
||||
RESULTS: Resultat
|
||||
SEARCH: Sök
|
||||
SELECTTYPETOCREATE: '(Välj för att skapa)'
|
||||
Symbiote\GridFieldExtensions\Extensions\GridFieldDetailFormItemRequestExtension:
|
||||
NEW: 'Lägg till ny rad'
|
||||
Symbiote\GridFieldExtensions\GridFieldConfigurablePaginator:
|
||||
SHOW: Visa
|
12
phpcs.xml.dist
Normal file
12
phpcs.xml.dist
Normal file
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ruleset name="SilverStripe">
|
||||
<description>CodeSniffer ruleset for SilverStripe coding conventions.</description>
|
||||
|
||||
<file>src</file>
|
||||
<file>tests</file>
|
||||
|
||||
<rule ref="PSR2" >
|
||||
<!-- Current exclusions -->
|
||||
<exclude name="PSR1.Methods.CamelCapsMethodName" />
|
||||
</rule>
|
||||
</ruleset>
|
15
phpunit.xml.dist
Normal file
15
phpunit.xml.dist
Normal file
@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit bootstrap="vendor/silverstripe/framework/tests/bootstrap.php" colors="true">
|
||||
<testsuite name="Default">
|
||||
<directory>tests</directory>
|
||||
</testsuite>
|
||||
|
||||
<filter>
|
||||
<whitelist addUncoveredFilesFromWhitelist="true">
|
||||
<directory suffix=".php">src/</directory>
|
||||
<exclude>
|
||||
<directory suffix=".php">tests/</directory>
|
||||
</exclude>
|
||||
</whitelist>
|
||||
</filter>
|
||||
</phpunit>
|
83
src/Extensions/GridFieldDetailFormItemRequestExtension.php
Normal file
83
src/Extensions/GridFieldDetailFormItemRequestExtension.php
Normal file
@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace Symbiote\GridFieldExtensions\Extensions;
|
||||
|
||||
use SilverStripe\Control\Controller;
|
||||
use SilverStripe\Core\Convert;
|
||||
use SilverStripe\Core\Extension;
|
||||
use SilverStripe\Forms\FieldList;
|
||||
use SilverStripe\Forms\GridField\GridFieldDetailForm_ItemRequest as CoreGridFieldDetailForm_ItemRequest;
|
||||
use SilverStripe\Forms\LiteralField;
|
||||
use SilverStripe\View\ArrayData;
|
||||
use SilverStripe\View\HTML;
|
||||
use Symbiote\GridFieldExtensions\GridFieldAddNewMultiClass;
|
||||
use Symbiote\GridFieldExtensions\GridFieldExtensions;
|
||||
|
||||
/**
|
||||
* @property CoreGridFieldDetailForm_ItemRequest $owner
|
||||
*/
|
||||
class GridFieldDetailFormItemRequestExtension extends Extension
|
||||
{
|
||||
/**
|
||||
* @param FieldList $actions
|
||||
*/
|
||||
public function updateFormActions(FieldList &$actions)
|
||||
{
|
||||
$grid = $this->owner->getGridField();
|
||||
$gridFieldConfig = $grid->getConfig();
|
||||
$addMultiClassComponent = $gridFieldConfig->getComponentByType(GridFieldAddNewMultiClass::class);
|
||||
if ($addMultiClassComponent) {
|
||||
$newRecordField = static::get_new_record_field_from_actions($actions);
|
||||
if ($newRecordField) {
|
||||
$newRecordField->getContainerFieldList()->removeByName('new-record');
|
||||
$newRecordField->getContainerFieldList()->push(
|
||||
LiteralField::create('new-record', $this->getHTMLFragment($addMultiClassComponent))
|
||||
);
|
||||
GridFieldExtensions::include_requirements();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
private function getHTMLFragment(GridFieldAddNewMultiClass $component)
|
||||
{
|
||||
$grid = $this->owner->getGridField();
|
||||
|
||||
$classes = $component->getClasses($grid);
|
||||
|
||||
if (!count($classes ?? [])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return HTML::createTag('a', [
|
||||
'data-href-template' => Controller::join_links($grid->Link(), 'add-multi-class', '{class}'),
|
||||
'title' => _t(__CLASS__ . '.NEW', 'Add new record'),
|
||||
'aria-label' => _t(__CLASS__ . '.NEW', 'Add new record'),
|
||||
'class' => implode(' ', array(
|
||||
'btn',
|
||||
'btn-primary',
|
||||
'font-icon-plus-thin',
|
||||
'btn--circular',
|
||||
'action--new',
|
||||
'discard-confirmation',
|
||||
'action--new__multi-class',
|
||||
)),
|
||||
'data-classes' => json_encode($classes),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FieldList $actions
|
||||
* @return LiteralField OR NULL
|
||||
*/
|
||||
private static function get_new_record_field_from_actions(FieldList &$actions)
|
||||
{
|
||||
$rightGroup = $actions->fieldByName('RightGroup');
|
||||
if (!$rightGroup) {
|
||||
return null;
|
||||
}
|
||||
return $rightGroup->getChildren()->fieldByName('new-record');
|
||||
}
|
||||
}
|
119
src/GridFieldAddExistingSearchButton.php
Executable file
119
src/GridFieldAddExistingSearchButton.php
Executable file
@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
namespace Symbiote\GridFieldExtensions;
|
||||
|
||||
use SilverStripe\Forms\GridField\AbstractGridFieldComponent;
|
||||
use SilverStripe\Forms\GridField\GridField_HTMLProvider;
|
||||
use SilverStripe\Forms\GridField\GridField_URLHandler;
|
||||
use SilverStripe\ORM\SS_List;
|
||||
use SilverStripe\View\ArrayData;
|
||||
|
||||
/**
|
||||
* A modal search dialog which uses search context to search for and add
|
||||
* existing records to a grid field.
|
||||
*/
|
||||
class GridFieldAddExistingSearchButton extends AbstractGridFieldComponent implements
|
||||
GridField_HTMLProvider,
|
||||
GridField_URLHandler
|
||||
{
|
||||
|
||||
private static $allowed_actions = array(
|
||||
'handleSearch'
|
||||
);
|
||||
|
||||
protected $title;
|
||||
protected $fragment;
|
||||
protected $searchList;
|
||||
|
||||
/**
|
||||
* @param string $fragment
|
||||
*/
|
||||
public function __construct($fragment = 'buttons-before-left')
|
||||
{
|
||||
$this->fragment = $fragment;
|
||||
$this->title = _t('GridFieldExtensions.ADDEXISTING', 'Add Existing');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getTitle()
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $title
|
||||
* @return GridFieldAddExistingSearchButton $this
|
||||
*/
|
||||
public function setTitle($title)
|
||||
{
|
||||
$this->title = $title;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getFragment()
|
||||
{
|
||||
return $this->fragment;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $fragment
|
||||
* @return GridFieldAddExistingSearchButton $this
|
||||
*/
|
||||
public function setFragment($fragment)
|
||||
{
|
||||
$this->fragment = $fragment;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a custom list to use to provide the searchable items.
|
||||
*
|
||||
* @param SS_List $list
|
||||
* @return GridFieldAddExistingSearchButton $this
|
||||
*/
|
||||
public function setSearchList(SS_List $list)
|
||||
{
|
||||
$this->searchList = $list;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return SS_List|null
|
||||
*/
|
||||
public function getSearchList()
|
||||
{
|
||||
return $this->searchList;
|
||||
}
|
||||
|
||||
public function getHTMLFragments($grid)
|
||||
{
|
||||
GridFieldExtensions::include_requirements();
|
||||
|
||||
$data = ArrayData::create([
|
||||
'Title' => $this->getTitle(),
|
||||
'Classes' => 'action btn btn-primary font-icon-search add-existing-search',
|
||||
'Link' => $grid->Link('add-existing-search'),
|
||||
]);
|
||||
|
||||
return [
|
||||
$this->fragment => $data->renderWith(__CLASS__),
|
||||
];
|
||||
}
|
||||
|
||||
public function getURLHandlers($grid)
|
||||
{
|
||||
return array(
|
||||
'add-existing-search' => 'handleSearch'
|
||||
);
|
||||
}
|
||||
|
||||
public function handleSearch($grid, $request)
|
||||
{
|
||||
return GridFieldAddExistingSearchHandler::create($grid, $this);
|
||||
}
|
||||
}
|
129
src/GridFieldAddExistingSearchHandler.php
Normal file
129
src/GridFieldAddExistingSearchHandler.php
Normal file
@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
namespace Symbiote\GridFieldExtensions;
|
||||
|
||||
use SilverStripe\Control\Controller;
|
||||
use SilverStripe\Control\RequestHandler;
|
||||
use SilverStripe\Forms\FieldList;
|
||||
use SilverStripe\Forms\Form;
|
||||
use SilverStripe\Forms\FormAction;
|
||||
use SilverStripe\Forms\GridField\GridField;
|
||||
use SilverStripe\ORM\DataList;
|
||||
use SilverStripe\ORM\PaginatedList;
|
||||
use SilverStripe\ORM\Search\SearchContext;
|
||||
|
||||
/**
|
||||
* Used by {@link GridFieldAddExistingSearchButton} to provide the searching
|
||||
* functionality.
|
||||
*/
|
||||
class GridFieldAddExistingSearchHandler extends RequestHandler
|
||||
{
|
||||
|
||||
private static $allowed_actions = array(
|
||||
'index',
|
||||
'add',
|
||||
'SearchForm'
|
||||
);
|
||||
|
||||
/**
|
||||
* @var GridField
|
||||
*/
|
||||
protected $grid;
|
||||
|
||||
/**
|
||||
* @var GridFieldAddExistingSearchButton
|
||||
*/
|
||||
protected $button;
|
||||
|
||||
/**
|
||||
* @var SearchContext
|
||||
*/
|
||||
protected $context;
|
||||
|
||||
public function __construct($grid, $button)
|
||||
{
|
||||
$this->grid = $grid;
|
||||
$this->button = $button;
|
||||
$this->context = singleton($grid->getModelClass())->getDefaultSearchContext();
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
return $this->renderWith(__CLASS__);
|
||||
}
|
||||
|
||||
public function add($request)
|
||||
{
|
||||
if (!$id = $request->postVar('id')) {
|
||||
$this->httpError(400);
|
||||
}
|
||||
|
||||
$list = $this->grid->getList();
|
||||
$item = DataList::create($list->dataClass())->byID($id);
|
||||
|
||||
if (!$item) {
|
||||
$this->httpError(400);
|
||||
}
|
||||
|
||||
$list->add($item);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Form
|
||||
*/
|
||||
public function SearchForm()
|
||||
{
|
||||
$form = Form::create(
|
||||
$this,
|
||||
'SearchForm',
|
||||
$this->context->getFields(),
|
||||
FieldList::create(
|
||||
FormAction::create('doSearch', _t('GridFieldExtensions.SEARCH', 'Search'))
|
||||
->setUseButtonTag(true)
|
||||
->addExtraClass('btn btn-primary font-icon-search')
|
||||
)
|
||||
);
|
||||
|
||||
$form->addExtraClass('stacked add-existing-search-form form--no-dividers');
|
||||
$form->setFormMethod('GET');
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
public function doSearch($data, $form)
|
||||
{
|
||||
$list = $this->context->getQuery($data, false, null, $this->getSearchList());
|
||||
$list = $list->subtract($this->grid->getList());
|
||||
$list = PaginatedList::create($list, $this->request);
|
||||
|
||||
$data = $this->customise(array(
|
||||
'SearchForm' => $form,
|
||||
'Items' => $list
|
||||
));
|
||||
return $data->index();
|
||||
}
|
||||
|
||||
public function Items()
|
||||
{
|
||||
$list = $this->getSearchList();
|
||||
$list = $list->subtract($this->grid->getList());
|
||||
$list = PaginatedList::create($list, $this->request);
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
public function Link($action = null)
|
||||
{
|
||||
return Controller::join_links($this->grid->Link(), 'add-existing-search', $action);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return DataList
|
||||
*/
|
||||
protected function getSearchList()
|
||||
{
|
||||
return $this->button->getSearchList() ?: DataList::create($this->grid->getList()->dataClass());
|
||||
}
|
||||
}
|
229
src/GridFieldAddNewInlineButton.php
Executable file
229
src/GridFieldAddNewInlineButton.php
Executable file
@ -0,0 +1,229 @@
|
||||
<?php
|
||||
|
||||
namespace Symbiote\GridFieldExtensions;
|
||||
|
||||
use SilverStripe\Core\Convert;
|
||||
use SilverStripe\Core\Injector\Injector;
|
||||
use SilverStripe\Forms\Form;
|
||||
use SilverStripe\Forms\GridField\AbstractGridFieldComponent;
|
||||
use SilverStripe\Forms\GridField\GridField;
|
||||
use SilverStripe\Forms\GridField\GridField_HTMLProvider;
|
||||
use SilverStripe\Forms\GridField\GridField_SaveHandler;
|
||||
use SilverStripe\ORM\ArrayList;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
use SilverStripe\ORM\DataObjectInterface;
|
||||
use SilverStripe\ORM\FieldType\DBField;
|
||||
use SilverStripe\ORM\ManyManyList;
|
||||
use SilverStripe\ORM\ManyManyThroughList;
|
||||
use SilverStripe\View\ArrayData;
|
||||
use SilverStripe\View\Requirements;
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* Builds on the {@link GridFieldEditableColumns} component to allow creating new records.
|
||||
*/
|
||||
class GridFieldAddNewInlineButton extends AbstractGridFieldComponent implements
|
||||
GridField_HTMLProvider,
|
||||
GridField_SaveHandler
|
||||
{
|
||||
/**
|
||||
* @skipUpgrade
|
||||
*/
|
||||
const POST_KEY = 'GridFieldAddNewInlineButton';
|
||||
|
||||
private $fragment;
|
||||
|
||||
private $title;
|
||||
|
||||
/**
|
||||
* @param string $fragment the fragment to render the button in
|
||||
*/
|
||||
public function __construct($fragment = 'buttons-before-left')
|
||||
{
|
||||
$this->setFragment($fragment);
|
||||
$this->setTitle(_t('GridFieldExtensions.ADD', 'Add'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the fragment name this button is rendered into.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getFragment()
|
||||
{
|
||||
return $this->fragment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the fragment name this button is rendered into.
|
||||
*
|
||||
* @param string $fragment
|
||||
* @return GridFieldAddNewInlineButton $this
|
||||
*/
|
||||
public function setFragment($fragment)
|
||||
{
|
||||
$this->fragment = $fragment;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the button title text.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getTitle()
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the button title text.
|
||||
*
|
||||
* @param string $title
|
||||
* @return GridFieldAddNewInlineButton $this
|
||||
*/
|
||||
public function setTitle($title)
|
||||
{
|
||||
$this->title = $title;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getHTMLFragments($grid)
|
||||
{
|
||||
if ($grid->getList() && !singleton($grid->getModelClass())->canCreate()) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$fragment = $this->getFragment();
|
||||
|
||||
/** @var GridFieldEditableColumns $editable */
|
||||
$editable = $grid->getConfig()->getComponentByType(GridFieldEditableColumns::class);
|
||||
if (!$editable) {
|
||||
throw new Exception('Inline adding requires the editable columns component');
|
||||
}
|
||||
|
||||
Requirements::javascript('symbiote/silverstripe-gridfieldextensions:javascript/tmpl.js');
|
||||
GridFieldExtensions::include_requirements();
|
||||
|
||||
$data = ArrayData::create(array(
|
||||
'Title' => $this->getTitle(),
|
||||
));
|
||||
|
||||
return array(
|
||||
$fragment => $data->renderWith(__CLASS__),
|
||||
'after' => $this->getRowTemplate($grid, $editable)
|
||||
);
|
||||
}
|
||||
|
||||
private function getRowTemplate(GridField $grid, GridFieldEditableColumns $editable)
|
||||
{
|
||||
$columns = ArrayList::create();
|
||||
$handled = array_keys($editable->getDisplayFields($grid) ?? []);
|
||||
|
||||
if ($grid->getList()) {
|
||||
$record = Injector::inst()->create($grid->getModelClass());
|
||||
} else {
|
||||
$record = null;
|
||||
}
|
||||
|
||||
$fields = $editable->getFields($grid, $record);
|
||||
|
||||
foreach ($grid->getColumns() as $column) {
|
||||
if (in_array($column, $handled ?? [])) {
|
||||
$field = $fields->dataFieldByName($column);
|
||||
$field->setName(sprintf(
|
||||
'%s[%s][{%%=o.num%%}][%s]',
|
||||
$grid->getName(),
|
||||
self::POST_KEY,
|
||||
$field->getName()
|
||||
));
|
||||
|
||||
if ($record && $record->hasField($column)) {
|
||||
$field->setValue($record->getField($column));
|
||||
}
|
||||
$content = $field->Field();
|
||||
} else {
|
||||
$content = $grid->getColumnContent($record, $column);
|
||||
|
||||
// Convert GridFieldEditableColumns to the template format
|
||||
$content = str_replace(
|
||||
sprintf('[%s][0]', GridFieldEditableColumns::POST_KEY),
|
||||
sprintf('[%s][{%%=o.num%%}]', self::POST_KEY),
|
||||
$content ?? ''
|
||||
);
|
||||
}
|
||||
|
||||
// Cast content
|
||||
if (! $content instanceof DBField) {
|
||||
$content = DBField::create_field('HTMLFragment', $content);
|
||||
}
|
||||
|
||||
$attrs = '';
|
||||
|
||||
foreach ($grid->getColumnAttributes($record, $column) as $attr => $val) {
|
||||
$attrs .= sprintf(' %s="%s"', $attr, Convert::raw2att($val));
|
||||
}
|
||||
|
||||
$columns->push(ArrayData::create(array(
|
||||
'Content' => $content,
|
||||
'Attributes' => DBField::create_field('HTMLFragment', $attrs),
|
||||
'IsActions' => $column == 'Actions'
|
||||
)));
|
||||
}
|
||||
|
||||
return $columns->renderWith('Symbiote\\GridFieldExtensions\\GridFieldAddNewInlineRow');
|
||||
}
|
||||
|
||||
public function handleSave(GridField $grid, DataObjectInterface $record)
|
||||
{
|
||||
$list = $grid->getList();
|
||||
$value = $grid->Value();
|
||||
|
||||
if (!isset($value[self::POST_KEY]) || !is_array($value[self::POST_KEY])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$class = $grid->getModelClass();
|
||||
/** @var GridFieldEditableColumns $editable */
|
||||
$editable = $grid->getConfig()->getComponentByType(GridFieldEditableColumns::class);
|
||||
/** @var GridFieldOrderableRows $sortable */
|
||||
$sortable = $grid->getConfig()->getComponentByType(GridFieldOrderableRows::class);
|
||||
|
||||
if (!singleton($class)->canCreate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($value[self::POST_KEY] as $fields) {
|
||||
/** @var DataObject $item */
|
||||
$item = $class::create();
|
||||
|
||||
// Add the item before the form is loaded so that the join-object is available
|
||||
if ($list instanceof ManyManyThroughList) {
|
||||
$list->add($item);
|
||||
}
|
||||
|
||||
$extra = array();
|
||||
|
||||
$form = $editable->getForm($grid, $item);
|
||||
$form->loadDataFrom($fields, Form::MERGE_CLEAR_MISSING);
|
||||
$form->saveInto($item);
|
||||
|
||||
// Check if we are also sorting these records
|
||||
if ($sortable) {
|
||||
$sortField = $sortable->getSortField();
|
||||
$item->setField($sortField, $fields[$sortField]);
|
||||
}
|
||||
|
||||
if ($list instanceof ManyManyList) {
|
||||
$extra = array_intersect_key($form->getData() ?? [], (array) $list->getExtraFields());
|
||||
}
|
||||
|
||||
$item->write(false, false, false, true);
|
||||
|
||||
// Add non-through lists after the write. many_many_extraFields are added there too
|
||||
if (!($list instanceof ManyManyThroughList)) {
|
||||
$list->add($item, $extra);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
309
src/GridFieldAddNewMultiClass.php
Executable file
309
src/GridFieldAddNewMultiClass.php
Executable file
@ -0,0 +1,309 @@
|
||||
<?php
|
||||
|
||||
namespace Symbiote\GridFieldExtensions;
|
||||
|
||||
use SilverStripe\Control\Controller;
|
||||
use SilverStripe\Control\HTTPRequest;
|
||||
use SilverStripe\Control\HTTPResponse_Exception;
|
||||
use SilverStripe\Core\ClassInfo;
|
||||
use SilverStripe\Core\Config\Config;
|
||||
use SilverStripe\Core\Injector\Injector;
|
||||
use SilverStripe\Forms\DropdownField;
|
||||
use SilverStripe\Forms\GridField\AbstractGridFieldComponent;
|
||||
use SilverStripe\Forms\GridField\GridField;
|
||||
use SilverStripe\Forms\GridField\GridField_HTMLProvider;
|
||||
use SilverStripe\Forms\GridField\GridField_URLHandler;
|
||||
use SilverStripe\Forms\GridField\GridFieldDetailForm;
|
||||
use SilverStripe\View\ArrayData;
|
||||
use ReflectionClass;
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* A component which lets the user select from a list of classes to create a new record form.
|
||||
*
|
||||
* By default the list of classes that are createable is the grid field's model class, and any
|
||||
* subclasses. This can be customised using {@link setClasses()}.
|
||||
*/
|
||||
class GridFieldAddNewMultiClass extends AbstractGridFieldComponent implements
|
||||
GridField_HTMLProvider,
|
||||
GridField_URLHandler
|
||||
{
|
||||
/**
|
||||
* @skipUpgrade
|
||||
*/
|
||||
const POST_KEY = 'GridFieldAddNewMultiClass';
|
||||
|
||||
private static $allowed_actions = array(
|
||||
'handleAdd'
|
||||
);
|
||||
|
||||
// Should we add an empty string to the add class dropdown?
|
||||
private static $showEmptyString = true;
|
||||
|
||||
private $fragment;
|
||||
|
||||
private $title;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $classes;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $defaultClass;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $itemRequestClass = 'Symbiote\\GridFieldExtensions\\GridFieldAddNewMultiClassHandler';
|
||||
|
||||
/**
|
||||
* @param string $fragment the fragment to render the button in
|
||||
*/
|
||||
public function __construct($fragment = 'before')
|
||||
{
|
||||
$this->setFragment($fragment);
|
||||
$this->setTitle(_t('GridFieldExtensions.ADD', 'Add'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the fragment name this button is rendered into.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getFragment()
|
||||
{
|
||||
return $this->fragment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the fragment name this button is rendered into.
|
||||
*
|
||||
* @param string $fragment
|
||||
* @return GridFieldAddNewMultiClass $this
|
||||
*/
|
||||
public function setFragment($fragment)
|
||||
{
|
||||
$this->fragment = $fragment;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the button title text.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getTitle()
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the button title text.
|
||||
*
|
||||
* @param string $title
|
||||
* @return GridFieldAddNewMultiClass $this
|
||||
*/
|
||||
public function setTitle($title)
|
||||
{
|
||||
$this->title = $title;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the classes that can be created using this button, defaulting to the model class and
|
||||
* its subclasses.
|
||||
*
|
||||
* @param GridField $grid
|
||||
* @return array a map of class name to title
|
||||
*/
|
||||
public function getClasses(GridField $grid)
|
||||
{
|
||||
$result = array();
|
||||
|
||||
if (is_null($this->classes)) {
|
||||
$classes = array_values(ClassInfo::subclassesFor($grid->getModelClass()) ?? []);
|
||||
sort($classes);
|
||||
} else {
|
||||
$classes = $this->classes;
|
||||
}
|
||||
|
||||
$kill_ancestors = array();
|
||||
foreach ($classes as $class => $title) {
|
||||
if (!is_string($class)) {
|
||||
$class = $title;
|
||||
}
|
||||
if (!class_exists($class ?? '')) {
|
||||
continue;
|
||||
}
|
||||
$is_abstract = (($reflection = new ReflectionClass($class)) && $reflection->isAbstract());
|
||||
if (!$is_abstract && $class === $title) {
|
||||
$title = singleton($class)->i18n_singular_name();
|
||||
}
|
||||
|
||||
if ($ancestor_to_hide = Config::inst()->get($class, 'hide_ancestor')) {
|
||||
$kill_ancestors[$ancestor_to_hide] = true;
|
||||
}
|
||||
|
||||
if ($is_abstract || !singleton($class)->canCreate()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$result[$class] = $title;
|
||||
}
|
||||
|
||||
if ($kill_ancestors) {
|
||||
foreach ($kill_ancestors as $class => $bool) {
|
||||
unset($result[$class]);
|
||||
}
|
||||
}
|
||||
|
||||
$sanitised = array();
|
||||
foreach ($result as $class => $title) {
|
||||
$sanitised[$this->sanitiseClassName($class)] = $title;
|
||||
}
|
||||
|
||||
return $sanitised;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the classes that can be created using this button.
|
||||
*
|
||||
* @param array $classes a set of class names, optionally mapped to titles
|
||||
* @param string $default
|
||||
* @return GridFieldAddNewMultiClass $this
|
||||
*/
|
||||
public function setClasses(array $classes, $default = null)
|
||||
{
|
||||
$this->classes = $classes;
|
||||
if ($default) {
|
||||
$this->defaultClass = $default;
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the default class that is selected automatically.
|
||||
*
|
||||
* @param string $default the class name to use as default
|
||||
* @return GridFieldAddNewMultiClass $this
|
||||
*/
|
||||
public function setDefaultClass($default)
|
||||
{
|
||||
$this->defaultClass = $default;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles adding a new instance of a selected class.
|
||||
*
|
||||
* @param GridField $grid
|
||||
* @param HTTPRequest $request
|
||||
* @return GridFieldAddNewMultiClassHandler
|
||||
* @throws Exception
|
||||
* @throws HTTPResponse_Exception
|
||||
*/
|
||||
public function handleAdd($grid, $request)
|
||||
{
|
||||
$class = $request->param('ClassName');
|
||||
$classes = $this->getClasses($grid);
|
||||
/** @var GridFieldDetailForm $component */
|
||||
$component = $grid->getConfig()->getComponentByType(GridFieldDetailForm::class);
|
||||
|
||||
if (!$component) {
|
||||
throw new Exception('The add new multi class component requires the detail form component.');
|
||||
}
|
||||
|
||||
if (!$class || !array_key_exists($class, $classes ?? [])) {
|
||||
throw new HTTPResponse_Exception(400);
|
||||
}
|
||||
|
||||
$unsanitisedClass = $this->unsanitiseClassName($class);
|
||||
$handler = Injector::inst()->create(
|
||||
$this->itemRequestClass,
|
||||
$grid,
|
||||
$component,
|
||||
new $unsanitisedClass(),
|
||||
$grid->getForm()->getController(),
|
||||
'add-multi-class'
|
||||
);
|
||||
$handler->setTemplate($component->getTemplate());
|
||||
|
||||
return $handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getHTMLFragments($grid)
|
||||
{
|
||||
$classes = $this->getClasses($grid);
|
||||
|
||||
if (!count($classes ?? [])) {
|
||||
return array();
|
||||
}
|
||||
|
||||
GridFieldExtensions::include_requirements();
|
||||
|
||||
$field = DropdownField::create(
|
||||
sprintf('%s[%s]', __CLASS__, $grid->getName()),
|
||||
'',
|
||||
$classes,
|
||||
$this->defaultClass
|
||||
);
|
||||
if (Config::inst()->get(__CLASS__, 'showEmptyString')) {
|
||||
$field->setEmptyString(_t('GridFieldExtensions.SELECTTYPETOCREATE', '(Select type to create)'));
|
||||
}
|
||||
$field->addExtraClass('no-change-track');
|
||||
|
||||
$data = ArrayData::create(array(
|
||||
'Title' => $this->getTitle(),
|
||||
'Link' => Controller::join_links($grid->Link(), 'add-multi-class', '{class}'),
|
||||
'ClassField' => $field
|
||||
));
|
||||
|
||||
return array(
|
||||
$this->getFragment() => $data->renderWith(__CLASS__)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getURLHandlers($grid)
|
||||
{
|
||||
return array(
|
||||
'add-multi-class/$ClassName!' => 'handleAdd'
|
||||
);
|
||||
}
|
||||
|
||||
public function setItemRequestClass($class)
|
||||
{
|
||||
$this->itemRequestClass = $class;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitise a model class' name for inclusion in a link
|
||||
*
|
||||
* @param string $class
|
||||
* @return string
|
||||
*/
|
||||
protected function sanitiseClassName($class)
|
||||
{
|
||||
return str_replace('\\', '-', $class ?? '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsanitise a model class' name from a URL param
|
||||
*
|
||||
* @param string $class
|
||||
* @return string
|
||||
*/
|
||||
protected function unsanitiseClassName($class)
|
||||
{
|
||||
return str_replace('-', '\\', $class ?? '');
|
||||
}
|
||||
}
|
36
src/GridFieldAddNewMultiClassHandler.php
Normal file
36
src/GridFieldAddNewMultiClassHandler.php
Normal file
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace Symbiote\GridFieldExtensions;
|
||||
|
||||
use SilverStripe\Control\Controller;
|
||||
use SilverStripe\Forms\GridField\GridFieldDetailForm_ItemRequest;
|
||||
|
||||
/**
|
||||
* A custom grid field request handler that allows interacting with form fields when adding records.
|
||||
*/
|
||||
class GridFieldAddNewMultiClassHandler extends GridFieldDetailForm_ItemRequest
|
||||
{
|
||||
|
||||
public function Link($action = null)
|
||||
{
|
||||
if ($this->record->ID) {
|
||||
return parent::Link($action);
|
||||
} else {
|
||||
return Controller::join_links(
|
||||
$this->gridField->Link(),
|
||||
'add-multi-class',
|
||||
$this->sanitiseClassName(get_class($this->record))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sanitise a model class' name for inclusion in a link
|
||||
* @return string
|
||||
*/
|
||||
protected function sanitiseClassName($class)
|
||||
{
|
||||
return str_replace('\\', '-', $class ?? '');
|
||||
}
|
||||
}
|
457
src/GridFieldConfigurablePaginator.php
Normal file
457
src/GridFieldConfigurablePaginator.php
Normal file
@ -0,0 +1,457 @@
|
||||
<?php
|
||||
|
||||
namespace Symbiote\GridFieldExtensions;
|
||||
|
||||
use Exception;
|
||||
use SilverStripe\Core\Config\Configurable;
|
||||
use SilverStripe\Forms\GridField\GridField;
|
||||
use SilverStripe\Forms\GridField\GridField_FormAction;
|
||||
use SilverStripe\Forms\GridField\GridFieldPaginator;
|
||||
use SilverStripe\Forms\GridField\GridState_Data;
|
||||
use SilverStripe\ORM\ArrayList;
|
||||
use SilverStripe\ORM\Limitable;
|
||||
use SilverStripe\ORM\SS_List;
|
||||
use SilverStripe\ORM\UnsavedRelationList;
|
||||
use SilverStripe\View\ArrayData;
|
||||
|
||||
/**
|
||||
* GridFieldConfigurablePaginator paginates the {@link GridField} list and adds controls to the bottom of
|
||||
* the {@link GridField}. The page sizes are configurable.
|
||||
*/
|
||||
class GridFieldConfigurablePaginator extends GridFieldPaginator
|
||||
{
|
||||
use Configurable;
|
||||
|
||||
/**
|
||||
* Specifies default page sizes
|
||||
*
|
||||
* @config
|
||||
* @var int
|
||||
*/
|
||||
private static $default_page_sizes = array(15, 30, 60);
|
||||
|
||||
/**
|
||||
* @var GridField
|
||||
*/
|
||||
protected $gridField;
|
||||
|
||||
/**
|
||||
* @var GridState_Data
|
||||
*/
|
||||
protected $gridFieldState;
|
||||
|
||||
/**
|
||||
* @var int[]
|
||||
*/
|
||||
protected $pageSizes = array();
|
||||
|
||||
/**
|
||||
* @param int $itemsPerPage How many items should be displayed per page
|
||||
* @param int $pageSizes The page sizes to show in the dropdown
|
||||
*/
|
||||
public function __construct($itemsPerPage = null, $pageSizes = null)
|
||||
{
|
||||
$this->setPageSizes($pageSizes ?: $this->config()->get('default_page_sizes'));
|
||||
|
||||
if (!$itemsPerPage) {
|
||||
$itemsPerPage = $this->pageSizes[0];
|
||||
}
|
||||
|
||||
parent::__construct($itemsPerPage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total number of records in the list
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getTotalRecords()
|
||||
{
|
||||
return (int) $this->getGridField()->getList()->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the first shown record number
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getFirstShown()
|
||||
{
|
||||
$firstShown = $this->getGridPagerState()->firstShown ?: 1;
|
||||
// Prevent visiting a page with an offset higher than the total number of items
|
||||
if ($firstShown > $this->getTotalRecords()) {
|
||||
$this->getGridPagerState()->firstShown = $firstShown = 1;
|
||||
}
|
||||
return $firstShown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the first shown record number. Will be stored in the state.
|
||||
*
|
||||
* @param int $firstShown
|
||||
* @return $this
|
||||
*/
|
||||
public function setFirstShown($firstShown = 1)
|
||||
{
|
||||
$this->getGridPagerState()->firstShown = (int) $firstShown;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last shown record number
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getLastShown()
|
||||
{
|
||||
return min($this->getTotalRecords(), $this->getFirstShown() + $this->getItemsPerPage() - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total number of pages, given the current number of items per page. The total
|
||||
* pages might be higher than <totalitems> / <itemsperpage> if the first shown record
|
||||
* is half way through a standard page break point.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getTotalPages()
|
||||
{
|
||||
// Pages before
|
||||
$pages = ceil(($this->getFirstShown() - 1) / $this->getItemsPerPage());
|
||||
|
||||
// Current page
|
||||
$pages++;
|
||||
|
||||
// Pages after
|
||||
$pages += ceil(($this->getTotalRecords() - $this->getLastShown()) / $this->getItemsPerPage());
|
||||
|
||||
return (int) $pages;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the page currently active. This is calculated by adding one to the previous number
|
||||
* of pages calculated via the "first shown record" position.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getCurrentPage()
|
||||
{
|
||||
return (int) ceil(($this->getFirstShown() - 1) / $this->getItemsPerPage()) + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the next page number
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getNextPage()
|
||||
{
|
||||
return min($this->getTotalPages(), $this->getCurrentPage() + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the previous page number
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getPreviousPage()
|
||||
{
|
||||
return max(1, $this->getCurrentPage() - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the page sizes to use in the "Show x" dropdown
|
||||
*
|
||||
* @param array $pageSizes
|
||||
* @return $this
|
||||
*/
|
||||
public function setPageSizes(array $pageSizes)
|
||||
{
|
||||
$this->pageSizes = $pageSizes;
|
||||
|
||||
// Reset items per page
|
||||
$this->setItemsPerPage(current($pageSizes ?? []));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the sizes for the "Show x" dropdown
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getPageSizes()
|
||||
{
|
||||
return $this->pageSizes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of page sizes for use in templates as a dropdown
|
||||
*
|
||||
* @return ArrayList
|
||||
*/
|
||||
public function getPageSizesAsList()
|
||||
{
|
||||
$pageSizes = ArrayList::create();
|
||||
$perPage = $this->getItemsPerPage();
|
||||
foreach ($this->getPageSizes() as $pageSize) {
|
||||
$pageSizes->push(array(
|
||||
'Size' => $pageSize,
|
||||
'Selected' => $pageSize == $perPage
|
||||
));
|
||||
}
|
||||
return $pageSizes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the GridField used in this request
|
||||
*
|
||||
* @return GridField
|
||||
* @throws Exception If the GridField has not been previously set
|
||||
*/
|
||||
public function getGridField()
|
||||
{
|
||||
if ($this->gridField) {
|
||||
return $this->gridField;
|
||||
}
|
||||
throw new Exception('No GridField available yet for this request!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the GridField so it can be used in other parts of the component during this request
|
||||
*
|
||||
* @param GridField $gridField
|
||||
* @return $this
|
||||
*/
|
||||
public function setGridField(GridField $gridField)
|
||||
{
|
||||
$this->gridField = $gridField;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function handleAction(GridField $gridField, $actionName, $arguments, $data)
|
||||
{
|
||||
$this->setGridField($gridField);
|
||||
|
||||
if ($actionName !== 'paginate') {
|
||||
return;
|
||||
}
|
||||
$state = $this->getGridPagerState();
|
||||
|
||||
$state->firstShown = (int) $arguments['first-shown'];
|
||||
$state->pageSize = $data[$gridField->getName()]['page-sizes'];
|
||||
|
||||
$this->setItemsPerPage($state->pageSize);
|
||||
}
|
||||
|
||||
public function getManipulatedData(GridField $gridField, SS_List $dataList)
|
||||
{
|
||||
// Assign the GridField to the class so it can be used later in the request
|
||||
$this->setGridField($gridField);
|
||||
|
||||
// Retain page sizes during actions provided by other components
|
||||
$state = $this->getGridPagerState();
|
||||
if (is_numeric($state->pageSize)) {
|
||||
$this->setItemsPerPage($state->pageSize);
|
||||
}
|
||||
|
||||
if (!($dataList instanceof Limitable) || ($dataList instanceof UnsavedRelationList)) {
|
||||
return $dataList;
|
||||
}
|
||||
|
||||
return $dataList->limit($this->getItemsPerPage(), $this->getFirstShown() - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the configurable page size options to the template data
|
||||
*
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @param GridField $gridField
|
||||
* @return ArrayData|null
|
||||
*/
|
||||
public function getTemplateParameters(GridField $gridField)
|
||||
{
|
||||
$state = $this->getGridPagerState();
|
||||
if (is_numeric($state->pageSize)) {
|
||||
$this->setItemsPerPage($state->pageSize);
|
||||
}
|
||||
$arguments = $this->getPagerArguments();
|
||||
|
||||
// Figure out which page and record range we're on
|
||||
if (!$arguments['total-rows']) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Define a list of the FormActions that should be generated for pager controls (see getPagerActions())
|
||||
$controls = array(
|
||||
'first' => array(
|
||||
'title' => 'First',
|
||||
'args' => array('first-shown' => 1),
|
||||
'extra-class' => 'btn btn-secondary btn--hide-text btn-sm font-icon-angle-double-left '
|
||||
. 'ss-gridfield-pagination-action ss-gridfield-firstpage',
|
||||
'disable-previous' => ($this->getCurrentPage() == 1)
|
||||
),
|
||||
'prev' => array(
|
||||
'title' => 'Previous',
|
||||
'args' => array('first-shown' => $arguments['first-shown'] - $this->getItemsPerPage()),
|
||||
'extra-class' => 'btn btn-secondary btn--hide-text btn-sm font-icon-angle-left '
|
||||
. 'ss-gridfield-pagination-action ss-gridfield-previouspage',
|
||||
'disable-previous' => ($this->getCurrentPage() == 1)
|
||||
),
|
||||
'next' => array(
|
||||
'title' => 'Next',
|
||||
'args' => array('first-shown' => $arguments['first-shown'] + $this->getItemsPerPage()),
|
||||
'extra-class' => 'btn btn-secondary btn--hide-text btn-sm font-icon-angle-right '
|
||||
.'ss-gridfield-pagination-action ss-gridfield-nextpage',
|
||||
'disable-next' => ($this->getCurrentPage() == $arguments['total-pages'])
|
||||
),
|
||||
'last' => array(
|
||||
'title' => 'Last',
|
||||
'args' => array('first-shown' => ($this->getTotalPages() - 1) * $this->getItemsPerPage() + 1),
|
||||
'extra-class' => 'btn btn-secondary btn--hide-text btn-sm font-icon-angle-double-right '
|
||||
. 'ss-gridfield-pagination-action ss-gridfield-lastpage',
|
||||
'disable-next' => ($this->getCurrentPage() == $arguments['total-pages'])
|
||||
),
|
||||
'pagesize' => array(
|
||||
'title' => 'Page Size',
|
||||
'args' => array('first-shown' => $arguments['first-shown']),
|
||||
'extra-class' => 'ss-gridfield-pagination-action ss-gridfield-pagesize-submit'
|
||||
),
|
||||
);
|
||||
|
||||
if ($controls['prev']['args']['first-shown'] < 1) {
|
||||
$controls['prev']['args']['first-shown'] = 1;
|
||||
}
|
||||
|
||||
$actions = $this->getPagerActions($controls, $gridField);
|
||||
|
||||
// Render in template
|
||||
return ArrayData::create(array(
|
||||
'OnlyOnePage' => ($arguments['total-pages'] == 1),
|
||||
'FirstPage' => $actions['first'],
|
||||
'PreviousPage' => $actions['prev'],
|
||||
'NextPage' => $actions['next'],
|
||||
'LastPage' => $actions['last'],
|
||||
'PageSizesSubmit' => $actions['pagesize'],
|
||||
'CurrentPageNum' => $this->getCurrentPage(),
|
||||
'NumPages' => $arguments['total-pages'],
|
||||
'FirstShownRecord' => $arguments['first-shown'],
|
||||
'LastShownRecord' => $arguments['last-shown'],
|
||||
'NumRecords' => $arguments['total-rows'],
|
||||
'PageSizes' => $this->getPageSizesAsList(),
|
||||
'PageSizesName' => $gridField->getName() . '[page-sizes]',
|
||||
));
|
||||
}
|
||||
|
||||
public function getHTMLFragments($gridField)
|
||||
{
|
||||
GridFieldExtensions::include_requirements();
|
||||
|
||||
$gridField->addExtraClass('ss-gridfield-configurable-paginator');
|
||||
|
||||
$forTemplate = $this->getTemplateParameters($gridField);
|
||||
if ($forTemplate) {
|
||||
return array(
|
||||
'footer' => $forTemplate->renderWith(
|
||||
__CLASS__,
|
||||
array('Colspan' => count($gridField->getColumns() ?? []))
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array containing the arguments for the pagination: total rows, pages, first record etc
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getPagerArguments()
|
||||
{
|
||||
return array(
|
||||
'total-rows' => $this->getTotalRecords(),
|
||||
'total-pages' => $this->getTotalPages(),
|
||||
'items-per-page' => $this->getItemsPerPage(),
|
||||
'first-shown' => $this->getFirstShown(),
|
||||
'last-shown' => $this->getLastShown(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns FormActions for each of the pagination actions, in an array
|
||||
*
|
||||
* @param array $controls
|
||||
* @param GridField $gridField
|
||||
* @return GridField_FormAction[]
|
||||
*/
|
||||
public function getPagerActions(array $controls, GridField $gridField)
|
||||
{
|
||||
$actions = array();
|
||||
|
||||
foreach ($controls as $key => $arguments) {
|
||||
$action = GridField_FormAction::create(
|
||||
$gridField,
|
||||
'pagination_' . $key,
|
||||
$arguments['title'],
|
||||
'paginate',
|
||||
$arguments['args']
|
||||
);
|
||||
|
||||
if (isset($arguments['extra-class'])) {
|
||||
$action->addExtraClass($arguments['extra-class']);
|
||||
}
|
||||
|
||||
if (isset($arguments['disable-previous']) && $arguments['disable-previous']) {
|
||||
$action = $action->performDisabledTransformation();
|
||||
} elseif (isset($arguments['disable-next']) && $arguments['disable-next']) {
|
||||
$action = $action->performDisabledTransformation();
|
||||
}
|
||||
|
||||
$actions[$key] = $action;
|
||||
}
|
||||
|
||||
return $actions;
|
||||
}
|
||||
|
||||
public function getActions($gridField)
|
||||
{
|
||||
return array('paginate');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the state from the current request's GridField and sets some default values on it
|
||||
*
|
||||
* @param GridField $gridField Not used, but present for parent method compatibility
|
||||
* @return GridState_Data
|
||||
*/
|
||||
protected function getGridPagerState(GridField $gridField = null)
|
||||
{
|
||||
if (!$this->gridFieldState) {
|
||||
$state = $this->getGridField()->State->GridFieldConfigurablePaginator;
|
||||
|
||||
// SS 3.1 compatibility (missing __call)
|
||||
if (is_object($state->firstShown)) {
|
||||
$state->firstShown = 1;
|
||||
}
|
||||
if (is_object($state->pageSize)) {
|
||||
$state->pageSize = $this->getItemsPerPage();
|
||||
}
|
||||
|
||||
// Handle free input in the page number field
|
||||
$parentState = $this->getGridField()->State->GridFieldPaginator;
|
||||
if (is_object($parentState->currentPage)) {
|
||||
$parentState->currentPage = 0;
|
||||
}
|
||||
|
||||
if ($parentState->currentPage >= 1) {
|
||||
$state->firstShown = ($parentState->currentPage - 1) * $this->getItemsPerPage() + 1;
|
||||
$parentState->currentPage = null;
|
||||
}
|
||||
|
||||
$this->gridFieldState = $state;
|
||||
}
|
||||
|
||||
return $this->gridFieldState;
|
||||
}
|
||||
}
|
350
src/GridFieldEditableColumns.php
Normal file
350
src/GridFieldEditableColumns.php
Normal file
@ -0,0 +1,350 @@
|
||||
<?php
|
||||
|
||||
namespace Symbiote\GridFieldExtensions;
|
||||
|
||||
use Closure;
|
||||
use Exception;
|
||||
use SilverStripe\Control\Controller;
|
||||
use SilverStripe\Control\HTTPRequest;
|
||||
use SilverStripe\Control\HTTPResponse_Exception;
|
||||
use SilverStripe\Core\Injector\Injector;
|
||||
use SilverStripe\Forms\FieldList;
|
||||
use SilverStripe\Forms\Form;
|
||||
use SilverStripe\Forms\FormField;
|
||||
use SilverStripe\Forms\GridField\GridField;
|
||||
use SilverStripe\Forms\GridField\GridFieldDataColumns;
|
||||
use SilverStripe\Forms\GridField\GridField_HTMLProvider;
|
||||
use SilverStripe\Forms\GridField\GridField_SaveHandler;
|
||||
use SilverStripe\Forms\GridField\GridField_URLHandler;
|
||||
use SilverStripe\Forms\HTMLEditor\HTMLEditorField;
|
||||
use SilverStripe\Forms\LiteralField;
|
||||
use SilverStripe\Forms\ReadonlyField;
|
||||
use SilverStripe\ORM\ArrayList;
|
||||
use SilverStripe\ORM\DataList;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
use SilverStripe\ORM\DataObjectInterface;
|
||||
use SilverStripe\ORM\ManyManyList;
|
||||
use SilverStripe\ORM\ManyManyThroughList;
|
||||
|
||||
/**
|
||||
* Allows inline editing of grid field records without having to load a separate
|
||||
* edit interface.
|
||||
*
|
||||
* The form fields used can be configured by setting the value in {@link setDisplayFields()} to one
|
||||
* of the following forms:
|
||||
* - A Closure which returns the field instance.
|
||||
* - An array with a `callback` key pointing to a function which returns the field.
|
||||
* - An array with a `field` key->response specifying the field class to use.
|
||||
*/
|
||||
class GridFieldEditableColumns extends GridFieldDataColumns implements
|
||||
GridField_HTMLProvider,
|
||||
GridField_SaveHandler,
|
||||
GridField_URLHandler
|
||||
{
|
||||
/**
|
||||
* @skipUpgrade
|
||||
*/
|
||||
const POST_KEY = 'GridFieldEditableColumns';
|
||||
|
||||
private static $allowed_actions = array(
|
||||
'handleForm'
|
||||
);
|
||||
|
||||
/**
|
||||
* @var Form[]
|
||||
*/
|
||||
protected $forms = array();
|
||||
|
||||
public function getColumnContent($grid, $record, $col)
|
||||
{
|
||||
$fields = $this->getForm($grid, $record)->Fields();
|
||||
|
||||
if (!$this->displayFields) {
|
||||
// If setDisplayFields() not used, utilize $summary_fields
|
||||
// in a way similar to base class
|
||||
$colRelation = explode('.', $col ?? '');
|
||||
$value = $grid->getDataFieldValue($record, $colRelation[0]);
|
||||
$field = $fields->fieldByName($colRelation[0]);
|
||||
if (!$field || $field->isReadonly() || $field->isDisabled()) {
|
||||
return parent::getColumnContent($grid, $record, $col);
|
||||
}
|
||||
|
||||
// Ensure this field is available to edit on the record
|
||||
// (ie. Maybe its readonly due to certain circumstances, or removed and not editable)
|
||||
$cmsFields = $record->getCMSFields();
|
||||
$cmsField = $cmsFields->dataFieldByName($colRelation[0]);
|
||||
if (!$cmsField || $cmsField->isReadonly() || $cmsField->isDisabled()) {
|
||||
return parent::getColumnContent($grid, $record, $col);
|
||||
}
|
||||
$field = clone $field;
|
||||
} else {
|
||||
$value = $grid->getDataFieldValue($record, $col);
|
||||
$field = $fields->dataFieldByName($col);
|
||||
|
||||
// Fall back to previous logic
|
||||
if (!$field) {
|
||||
$rel = (strpos($col ?? '', '.') === false); // field references a relation value
|
||||
$field = ($rel) ? clone $fields->fieldByName($col) : ReadonlyField::create($col);
|
||||
}
|
||||
|
||||
if (!$field) {
|
||||
throw new Exception("Could not find the field '$col'");
|
||||
}
|
||||
}
|
||||
|
||||
if (array_key_exists($col, $this->fieldCasting ?? [])) {
|
||||
$value = $grid->getCastedValue($value, $this->fieldCasting[$col]);
|
||||
}
|
||||
|
||||
$value = $this->formatValue($grid, $record, $col, $value);
|
||||
|
||||
$field->setName($this->getFieldName($field->getName(), $grid, $record));
|
||||
$field->setValue($value);
|
||||
|
||||
if ($grid->isReadonly() || !$record->canEdit()) {
|
||||
$field = $field->performReadonlyTransformation();
|
||||
}
|
||||
|
||||
if ($field instanceof HtmlEditorField) {
|
||||
return $field->FieldHolder();
|
||||
}
|
||||
|
||||
return $field->forTemplate();
|
||||
}
|
||||
|
||||
public function getHTMLFragments($grid)
|
||||
{
|
||||
GridFieldExtensions::include_requirements();
|
||||
$grid->addExtraClass('ss-gridfield-editable');
|
||||
}
|
||||
|
||||
public function handleSave(GridField $grid, DataObjectInterface $record)
|
||||
{
|
||||
/** @var DataList $list */
|
||||
$list = $grid->getList();
|
||||
$value = $grid->Value();
|
||||
|
||||
if (!isset($value[self::POST_KEY]) || !is_array($value[self::POST_KEY])) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var GridFieldOrderableRows $sortable */
|
||||
$sortable = $grid->getConfig()->getComponentByType(GridFieldOrderableRows::class);
|
||||
|
||||
// Fetch the items before processing them
|
||||
$ids = array_keys($value[self::POST_KEY]);
|
||||
if (empty($ids)) {
|
||||
return;
|
||||
}
|
||||
$itemsCollection = ArrayList::create($list->filter('ID', $ids)->toArray());
|
||||
|
||||
foreach ($value[self::POST_KEY] as $id => $fields) {
|
||||
if (!is_numeric($id) || !is_array($fields)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find the item from the fetched collection of items
|
||||
$item = $itemsCollection->find('ID', $id);
|
||||
|
||||
// Skip not found item, or don't have any changed fields, or current user can't edit
|
||||
if (!$item || !$this->isChanged($item, $fields) || !$item->canEdit()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$extra = array();
|
||||
|
||||
$form = $this->getForm($grid, $item);
|
||||
$form->loadDataFrom($fields, Form::MERGE_CLEAR_MISSING);
|
||||
$form->saveInto($item);
|
||||
|
||||
|
||||
// Check if we are also sorting these records
|
||||
if ($sortable) {
|
||||
$sortField = $sortable->getSortField();
|
||||
if (isset($fields[$sortField])) {
|
||||
$item->setField($sortField, $fields[$sortField]);
|
||||
}
|
||||
}
|
||||
|
||||
if ($list instanceof ManyManyList || $list instanceof ManyManyThroughList) {
|
||||
$extra = array_intersect_key($form->getData() ?? [], (array) $list->getExtraFields());
|
||||
}
|
||||
|
||||
$item->write(false, false, false, true);
|
||||
$list->add($item, $extra);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param GridField $grid
|
||||
* @param HTTPRequest $request
|
||||
* @return Form
|
||||
* @throws HTTPResponse_Exception
|
||||
*/
|
||||
public function handleForm(GridField $grid, $request)
|
||||
{
|
||||
$id = $request->param('ID');
|
||||
$list = $grid->getList();
|
||||
|
||||
if (!ctype_digit($id)) {
|
||||
throw new HTTPResponse_Exception(null, 400);
|
||||
}
|
||||
|
||||
if (!$record = $list->byID($id)) {
|
||||
throw new HTTPResponse_Exception(null, 404);
|
||||
}
|
||||
|
||||
$form = $this->getForm($grid, $record);
|
||||
|
||||
foreach ($form->Fields() as $field) {
|
||||
$field->setName($this->getFieldName($field->getName(), $grid, $record));
|
||||
}
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
public function getURLHandlers($grid)
|
||||
{
|
||||
return array(
|
||||
'editable/form/$ID' => 'handleForm'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the field list for a record.
|
||||
*
|
||||
* @param GridField $grid
|
||||
* @param DataObjectInterface $record
|
||||
* @return FieldList
|
||||
* @throws Exception
|
||||
*/
|
||||
public function getFields(GridField $grid, DataObjectInterface $record)
|
||||
{
|
||||
$cols = $this->getDisplayFields($grid);
|
||||
$fields = FieldList::create();
|
||||
|
||||
/** @var DataList $list */
|
||||
$list = $grid->getList();
|
||||
$class = $list ? $list->dataClass() : null;
|
||||
|
||||
foreach ($cols as $col => $info) {
|
||||
$field = null;
|
||||
|
||||
if ($info instanceof Closure) {
|
||||
$field = call_user_func($info, $record, $col, $grid);
|
||||
} elseif (is_array($info)) {
|
||||
if (isset($info['callback'])) {
|
||||
$field = call_user_func($info['callback'], $record, $col, $grid);
|
||||
} elseif (isset($info['field'])) {
|
||||
if ($info['field'] == LiteralField::class) {
|
||||
$field = new $info['field']($col, null);
|
||||
} else {
|
||||
$field = new $info['field']($col);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$field instanceof FormField) {
|
||||
throw new Exception(sprintf(
|
||||
'The field for column "%s" is not a valid form field',
|
||||
$col
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if (!$field && ($list instanceof ManyManyList || $list instanceof ManyManyThroughList)) {
|
||||
$extra = $list->getExtraFields();
|
||||
|
||||
if ($extra && array_key_exists($col, $extra ?? [])) {
|
||||
$field = Injector::inst()->create($extra[$col], $col)->scaffoldFormField();
|
||||
}
|
||||
}
|
||||
|
||||
if (!$field) {
|
||||
if (!$this->displayFields) {
|
||||
// If setDisplayFields() not used, utilize $summary_fields
|
||||
// in a way similar to base class
|
||||
//
|
||||
// Allows use of 'MyBool.Nice' and 'MyHTML.NoHTML' so that
|
||||
// GridFields not using inline editing still look good or
|
||||
// revert to looking good in cases where the field isn't
|
||||
// available or is readonly
|
||||
//
|
||||
$colRelation = explode('.', $col ?? '');
|
||||
if ($class && $obj = DataObject::singleton($class)->dbObject($colRelation[0])) {
|
||||
$field = $obj->scaffoldFormField();
|
||||
} else {
|
||||
$field = ReadonlyField::create($colRelation[0]);
|
||||
}
|
||||
} elseif ($class && $obj = DataObject::singleton($class)->dbObject($col)) {
|
||||
$field = $obj->scaffoldFormField();
|
||||
} else {
|
||||
$field = ReadonlyField::create($col);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$field instanceof FormField) {
|
||||
throw new Exception(sprintf(
|
||||
'Invalid form field instance for column "%s"',
|
||||
$col
|
||||
));
|
||||
}
|
||||
|
||||
// Add CSS class for interactive fields
|
||||
if (!($field->isReadOnly() || $field instanceof LiteralField)) {
|
||||
$field->addExtraClass('editable-column-field');
|
||||
}
|
||||
|
||||
$fields->push($field);
|
||||
}
|
||||
|
||||
return $fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the form instance for a record.
|
||||
*
|
||||
* @param GridField $grid
|
||||
* @param DataObjectInterface $record
|
||||
* @return Form
|
||||
*/
|
||||
public function getForm(GridField $grid, DataObjectInterface $record)
|
||||
{
|
||||
$fields = $this->getFields($grid, $record);
|
||||
|
||||
$form = Form::create($grid, null, $fields, FieldList::create());
|
||||
$form->loadDataFrom($record);
|
||||
|
||||
$form->setFormAction(Controller::join_links(
|
||||
$grid->Link(),
|
||||
'editable/form',
|
||||
$record->ID
|
||||
));
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
protected function getFieldName($name, GridField $grid, DataObjectInterface $record)
|
||||
{
|
||||
return sprintf(
|
||||
'%s[%s][%s][%s]',
|
||||
$grid->getName(),
|
||||
self::POST_KEY,
|
||||
$record->ID,
|
||||
$name
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not an object in the grid field has changed data.
|
||||
*/
|
||||
private function isChanged(DataObject $item, array $fields): bool
|
||||
{
|
||||
foreach ($fields as $name => $value) {
|
||||
if ($item->getField($name) !== $value) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
17
src/GridFieldExtensions.php
Normal file
17
src/GridFieldExtensions.php
Normal file
@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace Symbiote\GridFieldExtensions;
|
||||
|
||||
use SilverStripe\View\Requirements;
|
||||
|
||||
/**
|
||||
* Utility functions for the grid fields extension module.
|
||||
*/
|
||||
class GridFieldExtensions
|
||||
{
|
||||
public static function include_requirements()
|
||||
{
|
||||
Requirements::css('symbiote/silverstripe-gridfieldextensions:css/GridFieldExtensions.css');
|
||||
Requirements::javascript('symbiote/silverstripe-gridfieldextensions:javascript/GridFieldExtensions.js');
|
||||
}
|
||||
}
|
82
src/GridFieldExternalLink.php
Normal file
82
src/GridFieldExternalLink.php
Normal file
@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace Symbiote\GridFieldExtensions;
|
||||
|
||||
use SilverStripe\Forms\GridField\GridFieldDataColumns;
|
||||
use SilverStripe\View\ArrayData;
|
||||
|
||||
/**
|
||||
* Displays a link to an external source referenced 'external link'
|
||||
*/
|
||||
class GridFieldExternalLink extends GridFieldDataColumns
|
||||
{
|
||||
|
||||
/**
|
||||
* Add a column for the actions
|
||||
*
|
||||
* @param type $gridField
|
||||
* @param array $columns
|
||||
*/
|
||||
public function augmentColumns($gridField, &$columns)
|
||||
{
|
||||
if (!in_array('Actions', $columns ?? [])) {
|
||||
$columns[] = 'Actions';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return any special attributes that will be used for FormField::create_tag()
|
||||
*
|
||||
* @param GridField $gridField
|
||||
* @param DataObject $record
|
||||
* @param string $columnName
|
||||
* @return array
|
||||
*/
|
||||
public function getColumnAttributes($gridField, $record, $columnName)
|
||||
{
|
||||
return array('class' => 'col-buttons');
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the title
|
||||
*
|
||||
* @param GridField $gridField
|
||||
* @param string $columnName
|
||||
* @return array
|
||||
*/
|
||||
public function getColumnMetadata($gridField, $columnName)
|
||||
{
|
||||
if ($columnName == 'Actions') {
|
||||
return array('title' => '');
|
||||
}
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Which columns are handled by this component
|
||||
*
|
||||
* @param type $gridField
|
||||
* @return type
|
||||
*/
|
||||
public function getColumnsHandled($gridField)
|
||||
{
|
||||
return array('Actions');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param GridField $gridField
|
||||
* @param DataObject $record
|
||||
* @param string $columnName
|
||||
*
|
||||
* @return string - the HTML for the column
|
||||
*/
|
||||
public function getColumnContent($gridField, $record, $columnName)
|
||||
{
|
||||
$data = ArrayData::create(array(
|
||||
'Link' => $record->hasMethod('getExternalLink') ? $record->getExternalLink() : $record->ExternalLink,
|
||||
'Text' => $record->hasMethod('getExternalLinkText') ? $record->getExternalLinkText() : 'External Link'
|
||||
));
|
||||
|
||||
return $data->renderWith('GridFieldExternalLink');
|
||||
}
|
||||
}
|
836
src/GridFieldOrderableRows.php
Executable file
836
src/GridFieldOrderableRows.php
Executable file
@ -0,0 +1,836 @@
|
||||
<?php
|
||||
|
||||
namespace Symbiote\GridFieldExtensions;
|
||||
|
||||
use Exception;
|
||||
use SilverStripe\Control\Controller;
|
||||
use SilverStripe\Control\HTTPRequest;
|
||||
use SilverStripe\Control\HTTPResponse_Exception;
|
||||
use SilverStripe\Control\RequestHandler;
|
||||
use SilverStripe\Core\ClassInfo;
|
||||
use SilverStripe\Core\Config\Config;
|
||||
use SilverStripe\Forms\GridField\GridField;
|
||||
use SilverStripe\Forms\GridField\GridField_ColumnProvider;
|
||||
use SilverStripe\Forms\GridField\GridField_DataManipulator;
|
||||
use SilverStripe\Forms\GridField\GridField_HTMLProvider;
|
||||
use SilverStripe\Forms\GridField\GridField_SaveHandler;
|
||||
use SilverStripe\Forms\GridField\GridField_URLHandler;
|
||||
use SilverStripe\Forms\GridField\GridFieldPaginator;
|
||||
use SilverStripe\Forms\HiddenField;
|
||||
use SilverStripe\ORM\ArrayList;
|
||||
use SilverStripe\ORM\DataList;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
use SilverStripe\ORM\DataObjectInterface;
|
||||
use SilverStripe\ORM\DataObjectSchema;
|
||||
use SilverStripe\ORM\DB;
|
||||
use SilverStripe\ORM\FieldType\DBDatetime;
|
||||
use SilverStripe\ORM\ManyManyList;
|
||||
use SilverStripe\ORM\ManyManyThroughList;
|
||||
use SilverStripe\ORM\ManyManyThroughQueryManipulator;
|
||||
use SilverStripe\ORM\SS_List;
|
||||
use SilverStripe\Versioned\Versioned;
|
||||
use SilverStripe\View\ViewableData;
|
||||
|
||||
/**
|
||||
* Allows grid field rows to be re-ordered via drag and drop. Both normal data
|
||||
* lists and many many lists can be ordered.
|
||||
*
|
||||
* If the grid field has not been sorted, this component will sort the data by
|
||||
* the sort field.
|
||||
*/
|
||||
class GridFieldOrderableRows extends RequestHandler implements
|
||||
GridField_ColumnProvider,
|
||||
GridField_DataManipulator,
|
||||
GridField_HTMLProvider,
|
||||
GridField_URLHandler,
|
||||
GridField_SaveHandler
|
||||
{
|
||||
|
||||
/**
|
||||
* @see $immediateUpdate
|
||||
* @var boolean
|
||||
*/
|
||||
private static $default_immediate_update = true;
|
||||
|
||||
private static $allowed_actions = array(
|
||||
'handleReorder',
|
||||
'handleMoveToPage'
|
||||
);
|
||||
|
||||
/**
|
||||
* The database field which specifies the sort, defaults to "Sort".
|
||||
*
|
||||
* @see setSortField()
|
||||
* @var string
|
||||
*/
|
||||
protected $sortField;
|
||||
|
||||
/**
|
||||
* If set to true, when an item is re-ordered, it will update on the
|
||||
* database and refresh the gridfield. When set to false, it will only
|
||||
* update the sort order when the record is saved.
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
protected $immediateUpdate;
|
||||
|
||||
/**
|
||||
* Extra sort fields to apply before the sort field.
|
||||
*
|
||||
* @see setExtraSortFields()
|
||||
* @var string|array
|
||||
*/
|
||||
protected $extraSortFields = null;
|
||||
|
||||
/**
|
||||
* If the items in the list are versioned and this is set to true, then
|
||||
* we will check to see if the version we're sorting is the latest published
|
||||
* version and if so then we will re-publish the item.
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
protected $republishLiveRecords = false;
|
||||
|
||||
/**
|
||||
* The number of the column containing the reorder handles
|
||||
*
|
||||
* @see setReorderColumnNumber()
|
||||
* @var int
|
||||
*/
|
||||
protected $reorderColumnNumber = 0;
|
||||
|
||||
/**
|
||||
* @param string $sortField
|
||||
*/
|
||||
public function __construct($sortField = 'Sort')
|
||||
{
|
||||
parent::__construct();
|
||||
$this->sortField = $sortField;
|
||||
$this->immediateUpdate = $this->config()->default_immediate_update;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getSortField()
|
||||
{
|
||||
return $this->sortField;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the field used to specify the sort.
|
||||
*
|
||||
* @param string $sortField
|
||||
* @return GridFieldOrderableRows $this
|
||||
*/
|
||||
public function setSortField($field)
|
||||
{
|
||||
$this->sortField = $field;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return boolean
|
||||
*/
|
||||
public function getImmediateUpdate()
|
||||
{
|
||||
return $this->immediateUpdate;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see $immediateUpdate
|
||||
* @param boolean $immediateUpdate
|
||||
* @return GridFieldOrderableRows $this
|
||||
*/
|
||||
public function setImmediateUpdate($bool)
|
||||
{
|
||||
$this->immediateUpdate = $bool;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|array
|
||||
*/
|
||||
public function getExtraSortFields()
|
||||
{
|
||||
return $this->extraSortFields;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see $republishLiveRecords
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function getRepublishLiveRecords()
|
||||
{
|
||||
return $this->republishLiveRecords;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see $republishLiveRecords
|
||||
*
|
||||
* @param boolean $bool
|
||||
* @return GridFieldOrderableRows $this
|
||||
*/
|
||||
public function setRepublishLiveRecords($bool)
|
||||
{
|
||||
$this->republishLiveRecords = $bool;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks to see if the relationship list is for a type of many_many
|
||||
*
|
||||
* @param SS_List $list
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function isManyMany(SS_List $list)
|
||||
{
|
||||
return $list instanceof ManyManyList || $list instanceof ManyManyThroughList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets extra sort fields to apply before the sort field.
|
||||
*
|
||||
* @param string|array $fields
|
||||
* @return GridFieldOrderableRows $this
|
||||
*/
|
||||
public function setExtraSortFields($fields)
|
||||
{
|
||||
$this->extraSortFields = $fields;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getReorderColumnNumber()
|
||||
{
|
||||
return $this->reorderColumnNumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the number of the column containing the reorder handles.
|
||||
*
|
||||
* @param int $colno
|
||||
* @return GridFieldOrderableRows $this
|
||||
*/
|
||||
public function setReorderColumnNumber($colno)
|
||||
{
|
||||
$this->reorderColumnNumber = $colno;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates sortable list
|
||||
*
|
||||
* @param SS_List $list
|
||||
* @throws Exception
|
||||
*/
|
||||
public function validateSortField(SS_List $list)
|
||||
{
|
||||
$field = $this->getSortField();
|
||||
|
||||
// Check extra fields on many many relation types
|
||||
if ($list instanceof ManyManyList) {
|
||||
$extra = $list->getExtraFields();
|
||||
|
||||
if ($extra && array_key_exists($field, $extra ?? [])) {
|
||||
return;
|
||||
}
|
||||
} elseif ($list instanceof ManyManyThroughList) {
|
||||
$manipulator = $this->getManyManyInspector($list);
|
||||
$fieldTable = DataObject::getSchema()->tableForField($manipulator->getJoinClass(), $field);
|
||||
if ($fieldTable) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$classes = ClassInfo::dataClassesFor($list->dataClass());
|
||||
|
||||
foreach ($classes as $class) {
|
||||
if (singleton($class)->hasDataBaseField($field)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Exception("Couldn't find the sort field '" . $field . "'");
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the table which contains the sort field.
|
||||
*
|
||||
* @param DataList $list
|
||||
* @return string
|
||||
*/
|
||||
public function getSortTable(SS_List $list)
|
||||
{
|
||||
$field = $this->getSortField();
|
||||
if ($list instanceof ManyManyList) {
|
||||
$extra = $list->getExtraFields();
|
||||
$table = $list->getJoinTable();
|
||||
if ($extra && array_key_exists($field, $extra ?? [])) {
|
||||
return $table;
|
||||
}
|
||||
} elseif ($list instanceof ManyManyThroughList) {
|
||||
return $this->getManyManyInspector($list)->getJoinAlias();
|
||||
}
|
||||
$classes = ClassInfo::dataClassesFor($list->dataClass());
|
||||
foreach ($classes as $class) {
|
||||
if (singleton($class)->hasDataBaseField($field)) {
|
||||
return DataObject::getSchema()->tableName($class);
|
||||
}
|
||||
}
|
||||
throw new Exception("Couldn't find the sort field '$field'");
|
||||
}
|
||||
|
||||
public function getURLHandlers($grid)
|
||||
{
|
||||
return array(
|
||||
'POST reorder' => 'handleReorder',
|
||||
'POST movetopage' => 'handleMoveToPage'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param GridField $field
|
||||
*/
|
||||
public function getHTMLFragments($field)
|
||||
{
|
||||
GridFieldExtensions::include_requirements();
|
||||
|
||||
$field->addExtraClass('ss-gridfield-orderable');
|
||||
$field->setAttribute('data-immediate-update', (string)(int)$this->immediateUpdate);
|
||||
$field->setAttribute('data-url-reorder', $field->Link('reorder'));
|
||||
$field->setAttribute('data-url-movetopage', $field->Link('movetopage'));
|
||||
}
|
||||
|
||||
public function augmentColumns($grid, &$cols)
|
||||
{
|
||||
if (!in_array('Reorder', $cols ?? []) && $grid->getState()->GridFieldOrderableRows->enabled) {
|
||||
array_splice($cols, $this->reorderColumnNumber ?? 0, 0, 'Reorder');
|
||||
}
|
||||
}
|
||||
|
||||
public function getColumnsHandled($grid)
|
||||
{
|
||||
return array('Reorder');
|
||||
}
|
||||
|
||||
public function getColumnContent($grid, $record, $col)
|
||||
{
|
||||
// In case you are using GridFieldEditableColumns, this ensures that
|
||||
// the correct sort order is saved. If you are not using that component,
|
||||
// this will be ignored by other components, but will still work for this.
|
||||
$sortFieldName = sprintf(
|
||||
'%s[GridFieldEditableColumns][%s][%s]',
|
||||
$grid->getName(),
|
||||
$record->ID,
|
||||
$this->getSortField()
|
||||
);
|
||||
|
||||
// Default: Get the sort field directly from the current record
|
||||
$currentSortValue = $record->getField($this->getSortField());
|
||||
|
||||
$list = $grid->getList();
|
||||
if ($list instanceof ManyManyThroughList) {
|
||||
// In a many many through list we should get the current sort order from the relationship
|
||||
// if it exists, not directly from the record
|
||||
$throughListSorts = $this->getSortValuesFromManyManyThroughList($list, $this->getSortField());
|
||||
|
||||
if (array_key_exists($record->ID, $throughListSorts ?? [])) {
|
||||
$currentSortValue = $throughListSorts[$record->ID];
|
||||
}
|
||||
}
|
||||
|
||||
$sortField = HiddenField::create($sortFieldName, false, $currentSortValue);
|
||||
$sortField->addExtraClass('ss-orderable-hidden-sort');
|
||||
$sortField->setForm($grid->getForm());
|
||||
|
||||
return ViewableData::create()->customise(array(
|
||||
'SortField' => $sortField
|
||||
))->renderWith('Symbiote\\GridFieldExtensions\\GridFieldOrderableRowsDragHandle');
|
||||
}
|
||||
|
||||
public function getColumnAttributes($grid, $record, $col)
|
||||
{
|
||||
return array('class' => 'col-reorder');
|
||||
}
|
||||
|
||||
public function getColumnMetadata($grid, $col)
|
||||
{
|
||||
if ($fieldLabels = singleton($grid->getModelClass())->fieldLabels()) {
|
||||
return array('title' => isset($fieldLabels['Reorder']) ? $fieldLabels['Reorder'] : '');
|
||||
}
|
||||
|
||||
return array('title' => '');
|
||||
}
|
||||
|
||||
public function getManipulatedData(GridField $grid, SS_List $list)
|
||||
{
|
||||
$state = $grid->getState();
|
||||
$sorted = (bool) ((string) $state->GridFieldSortableHeader->SortColumn);
|
||||
|
||||
// If the data has not been sorted by the user, then sort it by the
|
||||
// sort column, otherwise disable reordering.
|
||||
$state->GridFieldOrderableRows->enabled = !$sorted;
|
||||
|
||||
if (!$sorted) {
|
||||
$sortterm = '';
|
||||
if ($this->extraSortFields) {
|
||||
if (is_array($this->extraSortFields)) {
|
||||
foreach ($this->extraSortFields as $col => $dir) {
|
||||
$sortterm .= "$col $dir, ";
|
||||
}
|
||||
} else {
|
||||
$sortterm = $this->extraSortFields.', ';
|
||||
}
|
||||
}
|
||||
if ($list instanceof ArrayList) {
|
||||
// Fix bug in 3.1.3+ where ArrayList doesn't account for quotes
|
||||
$sortterm .= $this->getSortTable($list).'.'.$this->getSortField();
|
||||
} else {
|
||||
$sortterm .= '"'.$this->getSortTable($list).'"."'.$this->getSortField().'"';
|
||||
|
||||
if ($list instanceof DataList) {
|
||||
$classname = $list->dataClass();
|
||||
if ($defaultSort = Config::inst()->get($classname, 'default_sort')) {
|
||||
if (is_array($defaultSort)) {
|
||||
$defaultSortArray = [];
|
||||
foreach ($defaultSort as $column => $direction) {
|
||||
$defaultSortArray[] = "\"$column\" $direction";
|
||||
}
|
||||
$defaultSort = implode(', ', $defaultSortArray);
|
||||
}
|
||||
// Append the default sort to the end of the sort string
|
||||
// This may result in redundancy... but it seems to work
|
||||
$sortterm .= ($sortterm ? ', ' : '') . $defaultSort;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $list->sort($sortterm);
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles requests to reorder a set of IDs in a specific order.
|
||||
*
|
||||
* @param GridField $grid
|
||||
* @param HTTPRequest $request
|
||||
* @return string
|
||||
* @throws HTTPResponse_Exception
|
||||
*/
|
||||
public function handleReorder($grid, $request)
|
||||
{
|
||||
if (!$this->immediateUpdate) {
|
||||
$this->httpError(400);
|
||||
}
|
||||
$list = $grid->getList();
|
||||
$modelClass = $grid->getModelClass();
|
||||
$isManyMany = $this->isManyMany($list);
|
||||
if ($isManyMany && !singleton($modelClass)->canView()) {
|
||||
$this->httpError(403);
|
||||
} elseif (!$isManyMany && !singleton($modelClass)->canEdit()) {
|
||||
$this->httpError(403);
|
||||
}
|
||||
|
||||
// Save any un-committed changes to the gridfield
|
||||
if (($form = $grid->getForm()) && ($record = $form->getRecord())) {
|
||||
$form->loadDataFrom($request->requestVars(), true);
|
||||
$grid->saveInto($record);
|
||||
}
|
||||
|
||||
// Get records from the `GridFieldEditableColumns` column
|
||||
$gridFieldName = $grid->getName();
|
||||
if (strpos($gridFieldName ?? '', '.') !== false) {
|
||||
$gridFieldName = str_replace('.', '_', $gridFieldName ?? '');
|
||||
}
|
||||
|
||||
$data = $request->postVar($gridFieldName);
|
||||
$sortedIDs = $this->getSortedIDs($data);
|
||||
if (!$this->executeReorder($grid, $sortedIDs)) {
|
||||
$this->httpError(400);
|
||||
}
|
||||
|
||||
Controller::curr()->getResponse()->addHeader(
|
||||
'X-Status',
|
||||
rawurlencode(_t(__CLASS__ . '.REORDERED', 'Records reordered.'))
|
||||
);
|
||||
return $grid->FieldHolder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get mapping of sort value to item ID from posted data (gridfield list state), ordered by sort value.
|
||||
*
|
||||
* @param array $data Raw posted data
|
||||
* @return array [sortIndex => recordID]
|
||||
*/
|
||||
protected function getSortedIDs($data)
|
||||
{
|
||||
if (empty($data['GridFieldEditableColumns'])) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$sortedIDs = array();
|
||||
foreach ($data['GridFieldEditableColumns'] as $id => $recordData) {
|
||||
$sortValue = $recordData[$this->sortField];
|
||||
$sortedIDs[$sortValue] = $id;
|
||||
}
|
||||
ksort($sortedIDs);
|
||||
return $sortedIDs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles requests to move an item to the previous or next page.
|
||||
*/
|
||||
public function handleMoveToPage(GridField $grid, $request)
|
||||
{
|
||||
if (!$paginator = $grid->getConfig()->getComponentByType(GridFieldPaginator::class)) {
|
||||
$this->httpError(404, 'Paginator component not found');
|
||||
}
|
||||
|
||||
$move = $request->postVar('move');
|
||||
$field = $this->getSortField();
|
||||
|
||||
$list = $grid->getList();
|
||||
$manip = $grid->getManipulatedList();
|
||||
|
||||
$existing = $manip->map('ID', $field)->toArray();
|
||||
$values = $existing;
|
||||
$order = array();
|
||||
|
||||
$id = isset($move['id']) ? (int) $move['id'] : null;
|
||||
$to = isset($move['page']) ? $move['page'] : null;
|
||||
|
||||
if (!isset($values[$id])) {
|
||||
$this->httpError(400, 'Invalid item ID');
|
||||
}
|
||||
|
||||
$this->populateSortValues($list);
|
||||
|
||||
$page = ((int) $grid->getState()->GridFieldPaginator->currentPage) ?: 1;
|
||||
$per = $paginator->getItemsPerPage();
|
||||
|
||||
if ($to == 'prev') {
|
||||
$swap = $list->limit(1, ($page - 1) * $per - 1)->first();
|
||||
$values[$swap->ID] = $swap->$field;
|
||||
|
||||
$order[] = $id;
|
||||
$order[] = $swap->ID;
|
||||
|
||||
foreach ($existing as $_id => $sort) {
|
||||
if ($id != $_id) {
|
||||
$order[] = $_id;
|
||||
}
|
||||
}
|
||||
} elseif ($to == 'next') {
|
||||
$swap = $list->limit(1, $page * $per)->first();
|
||||
$values[$swap->ID] = $swap->$field;
|
||||
|
||||
foreach ($existing as $_id => $sort) {
|
||||
if ($id != $_id) {
|
||||
$order[] = $_id;
|
||||
}
|
||||
}
|
||||
|
||||
$order[] = $swap->ID;
|
||||
$order[] = $id;
|
||||
} else {
|
||||
$this->httpError(400, 'Invalid page target');
|
||||
}
|
||||
|
||||
$this->reorderItems($list, $values, $order);
|
||||
|
||||
return $grid->FieldHolder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle saving when 'immediateUpdate' is disabled, otherwise this isn't
|
||||
* necessary for the default sort mode.
|
||||
*/
|
||||
public function handleSave(GridField $grid, DataObjectInterface $record)
|
||||
{
|
||||
if (!$this->immediateUpdate) {
|
||||
$value = $grid->Value();
|
||||
$sortedIDs = $this->getSortedIDs($value);
|
||||
if ($sortedIDs) {
|
||||
$this->executeReorder($grid, $sortedIDs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param GridField $grid
|
||||
* @param array $sortedIDs List of IDS, where the key is the sort field value to save
|
||||
* @return bool
|
||||
*/
|
||||
protected function executeReorder(GridField $grid, $sortedIDs)
|
||||
{
|
||||
if (!is_array($sortedIDs) || empty($sortedIDs)) {
|
||||
return false;
|
||||
}
|
||||
$sortField = $this->getSortField();
|
||||
|
||||
$sortterm = '';
|
||||
if ($this->extraSortFields) {
|
||||
if (is_array($this->extraSortFields)) {
|
||||
foreach ($this->extraSortFields as $col => $dir) {
|
||||
$sortterm .= "$col $dir, ";
|
||||
}
|
||||
} else {
|
||||
$sortterm = $this->extraSortFields.', ';
|
||||
}
|
||||
}
|
||||
$list = $grid->getList();
|
||||
$sortterm .= '"'.$this->getSortTable($list).'"."'.$sortField.'"';
|
||||
$items = $list->filter('ID', $sortedIDs)->sort($sortterm);
|
||||
|
||||
// Ensure that each provided ID corresponded to an actual object.
|
||||
if (count($items ?? []) != count($sortedIDs ?? [])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Populate each object we are sorting with a sort value.
|
||||
$this->populateSortValues($items);
|
||||
|
||||
// Generate the current sort values.
|
||||
if ($items instanceof ManyManyList) {
|
||||
$current = array();
|
||||
foreach ($items->toArray() as $record) {
|
||||
// NOTE: _SortColumn0 is the first ->sort() field
|
||||
// used by SS when functions are detected in a SELECT
|
||||
// or CASE WHEN.
|
||||
if (isset($record->_SortColumn0)) {
|
||||
$current[$record->ID] = $record->_SortColumn0;
|
||||
} else {
|
||||
$current[$record->ID] = $record->$sortField;
|
||||
}
|
||||
}
|
||||
} elseif ($items instanceof ManyManyThroughList) {
|
||||
$current = $this->getSortValuesFromManyManyThroughList($list, $sortField);
|
||||
} else {
|
||||
$current = $items->map('ID', $sortField)->toArray();
|
||||
}
|
||||
|
||||
// Perform the actual re-ordering.
|
||||
$this->reorderItems($list, $current, $sortedIDs);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param SS_List $list
|
||||
* @param array $values **UNUSED** [listItemID => currentSortValue];
|
||||
* @param array $sortedIDs [newSortValue => listItemID]
|
||||
*/
|
||||
protected function reorderItems($list, array $values, array $sortedIDs)
|
||||
{
|
||||
$this->extend('onBeforeReorderItems', $list, $values, $sortedIDs);
|
||||
|
||||
// setup
|
||||
$sortField = $this->getSortField();
|
||||
// The problem is that $sortedIDs is a list of the _related_ item IDs, which causes trouble
|
||||
// with ManyManyThrough, where we need the ID of the _join_ item in order to set the value.
|
||||
$itemToSortReference = ($list instanceof ManyManyThroughList) ? 'getJoin' : 'Me';
|
||||
$currentSortList = $list->map('ID', $itemToSortReference)->toArray();
|
||||
|
||||
// sanity check.
|
||||
$this->validateSortField($list);
|
||||
|
||||
// ManyManyList extra fields aren't easily updated via the ORM, and so they need to be updated through an SQL
|
||||
// Query
|
||||
if ($list instanceof ManyManyList) {
|
||||
$sortTable = $this->getSortTable($list);
|
||||
|
||||
// Loop through each item, and update the sort values which do not match to order the objects.
|
||||
foreach ($sortedIDs as $newSortValue => $targetRecordID) {
|
||||
if ($currentSortList[$targetRecordID]->$sortField != $newSortValue) {
|
||||
DB::query(sprintf(
|
||||
'UPDATE "%s" SET "%s" = %d WHERE %s',
|
||||
$sortTable,
|
||||
$sortField,
|
||||
$newSortValue,
|
||||
$this->getSortTableClauseForIds($list, $targetRecordID)
|
||||
));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// For versioned objects, modify them with the ORM so that the
|
||||
// *_Versions table is updated. This ensures re-ordering works
|
||||
// similar to the SiteTree where you change the position, and then
|
||||
// you go into the record and publish it.
|
||||
foreach ($sortedIDs as $newSortValue => $targetRecordID) {
|
||||
// either the list data class (has_many, (belongs_)many_many)
|
||||
// or the intermediary join class (many_many through)
|
||||
$record = $currentSortList[$targetRecordID];
|
||||
|
||||
if ($record->$sortField != $newSortValue) {
|
||||
$record->$sortField = $newSortValue;
|
||||
|
||||
// We need to do this before writing otherwith isLiveVersion() will always be false
|
||||
$shouldRepublish = $this->getRepublishLiveRecords() && $record->isLiveVersion();
|
||||
|
||||
// Write our staged record and publish if required
|
||||
$record->write();
|
||||
if ($shouldRepublish) {
|
||||
$record->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->extend('onAfterReorderItems', $list, $values, $sortedIDs);
|
||||
}
|
||||
|
||||
protected function populateSortValues(DataList $list)
|
||||
{
|
||||
$list = clone $list;
|
||||
$field = $this->getSortField();
|
||||
$table = $this->getSortTable($list);
|
||||
$clause = sprintf('"%s"."%s" = 0', $table, $this->getSortField());
|
||||
$now = DBDatetime::now()->Rfc2822();
|
||||
$additionalSQL = '';
|
||||
$baseTable = DataObject::getSchema()->baseDataTable($list->dataClass());
|
||||
|
||||
$isBaseTable = ($baseTable == $table);
|
||||
if (!$list instanceof ManyManyList && $isBaseTable) {
|
||||
$additionalSQL = ", \"LastEdited\" = '$now'";
|
||||
}
|
||||
|
||||
foreach ($list->where($clause)->column('ID') as $id) {
|
||||
$max = DB::query(sprintf('SELECT MAX("%s") + 1 FROM "%s"', $field, $table));
|
||||
$max = $max->value();
|
||||
|
||||
DB::query(sprintf(
|
||||
'UPDATE "%s" SET "%s" = %d%s WHERE %s',
|
||||
$table,
|
||||
$field,
|
||||
$max,
|
||||
$additionalSQL,
|
||||
$this->getSortTableClauseForIds($list, $id)
|
||||
));
|
||||
|
||||
if (!$isBaseTable && !$this->isManyMany($list)) {
|
||||
DB::query(sprintf(
|
||||
'UPDATE "%s" SET "LastEdited" = \'%s\' WHERE %s',
|
||||
$baseTable,
|
||||
$now,
|
||||
$this->getSortTableClauseForIds($list, $id)
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Forms a WHERE clause for the table the sort column is defined on.
|
||||
* e.g. ID = 5
|
||||
* e.g. ID IN(5, 8, 10)
|
||||
* e.g. SortOrder = 5 AND RelatedThing.ID = 3
|
||||
* e.g. SortOrder IN(5, 8, 10) AND RelatedThing.ID = 3
|
||||
*
|
||||
* @param DataList $list
|
||||
* @param int|string|array $ids a single number, or array of numbers
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getSortTableClauseForIds(DataList $list, $ids)
|
||||
{
|
||||
if (is_array($ids)) {
|
||||
$value = 'IN (' . implode(', ', array_map('intval', $ids ?? [])) . ')';
|
||||
} else {
|
||||
$value = '= ' . (int) $ids;
|
||||
}
|
||||
|
||||
if ($this->isManyMany($list)) {
|
||||
$introspector = $this->getManyManyInspector($list);
|
||||
$extra = $list instanceof ManyManyList ?
|
||||
$introspector->getExtraFields() :
|
||||
DataObjectSchema::create()->fieldSpecs($introspector->getJoinClass(), DataObjectSchema::DB_ONLY);
|
||||
$key = $introspector->getLocalKey();
|
||||
$foreignKey = $this->getManyManyInspectorForeignKey($introspector);
|
||||
$foreignID = (int) $list->getForeignID();
|
||||
|
||||
if ($extra && array_key_exists($this->getSortField(), $extra ?? [])) {
|
||||
return sprintf(
|
||||
'"%s" %s AND "%s" = %d',
|
||||
$key,
|
||||
$value,
|
||||
$foreignKey,
|
||||
$foreignID
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return "\"ID\" $value";
|
||||
}
|
||||
|
||||
/**
|
||||
* A ManyManyList defines functions such as getLocalKey, however on ManyManyThroughList
|
||||
* these functions are moved to ManyManyThroughQueryManipulator, but otherwise retain
|
||||
* the same signature.
|
||||
*
|
||||
* @param ManyManyList|ManyManyThroughList $list
|
||||
*
|
||||
* @return ManyManyList|ManyManyThroughQueryManipulator
|
||||
*/
|
||||
protected function getManyManyInspector($list)
|
||||
{
|
||||
$inspector = $list;
|
||||
if ($list instanceof ManyManyThroughList) {
|
||||
foreach ($list->dataQuery()->getDataQueryManipulators() as $manipulator) {
|
||||
if ($manipulator instanceof ManyManyThroughQueryManipulator) {
|
||||
$inspector = $manipulator;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $inspector;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Depending on the list inspector and the list itself (ManyMany vs ManyManyThrough), the method to obtain
|
||||
* the foreign key may be different.
|
||||
*
|
||||
* @param $inspector
|
||||
* @return string
|
||||
*/
|
||||
private function getManyManyInspectorForeignKey($inspector)
|
||||
{
|
||||
if (($inspector instanceof ManyManyThroughQueryManipulator) && (method_exists($inspector, 'getForeignIDKey'))) {
|
||||
// This method has been introduced in framework 4.1
|
||||
return $inspector->getForeignIDKey();
|
||||
}
|
||||
|
||||
return $inspector->getForeignKey();
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to get sort orders from a many many through list relationship record, rather than the current
|
||||
* record itself.
|
||||
*
|
||||
* @param ManyManyList|ManyManyThroughList $list
|
||||
* @return int[] Sort orders for the
|
||||
*/
|
||||
protected function getSortValuesFromManyManyThroughList($list, $sortField)
|
||||
{
|
||||
$manipulator = $this->getManyManyInspector($list);
|
||||
|
||||
// Find the foreign key name, ID and class to look up
|
||||
$joinClass = $manipulator->getJoinClass();
|
||||
$fromRelationName = $this->getManyManyInspectorForeignKey($manipulator);
|
||||
$toRelationName = $manipulator->getLocalKey();
|
||||
|
||||
// Create a list of the MMTL relations
|
||||
$sortlist = DataList::create($joinClass)->filter([
|
||||
$toRelationName => $list->column('ID'),
|
||||
// first() is safe as there are earlier checks to ensure our list to sort is valid
|
||||
$fromRelationName => $list->first()->getJoin()->$fromRelationName,
|
||||
]);
|
||||
|
||||
return $sortlist->map($toRelationName, $sortField)->toArray();
|
||||
}
|
||||
}
|
176
src/GridFieldRequestHandler.php
Normal file
176
src/GridFieldRequestHandler.php
Normal file
@ -0,0 +1,176 @@
|
||||
<?php
|
||||
|
||||
namespace Symbiote\GridFieldExtensions;
|
||||
|
||||
use SilverStripe\Admin\LeftAndMain;
|
||||
use SilverStripe\Control\Controller;
|
||||
use SilverStripe\Control\RequestHandler;
|
||||
use SilverStripe\Forms\FieldList;
|
||||
use SilverStripe\Forms\Form;
|
||||
use SilverStripe\Forms\GridField\GridField;
|
||||
use SilverStripe\Forms\GridField\GridFieldComponent;
|
||||
use SilverStripe\Forms\GridField\GridFieldDetailForm_ItemRequest;
|
||||
use SilverStripe\Forms\Tab;
|
||||
use SilverStripe\Forms\TabSet;
|
||||
use SilverStripe\ORM\ArrayList;
|
||||
|
||||
/**
|
||||
* A base utility class for request handlers which present a grid field detail
|
||||
* view.
|
||||
*
|
||||
* This class provides some useful defaults for grid field detail views, such
|
||||
* as tabs, breadcrumbs and a back link. Much of this code is extracted from the
|
||||
* detail form.
|
||||
*/
|
||||
abstract class GridFieldRequestHandler extends RequestHandler
|
||||
{
|
||||
|
||||
private static $allowed_actions = array(
|
||||
'Form'
|
||||
);
|
||||
|
||||
/**
|
||||
* @var GridField
|
||||
*/
|
||||
protected $grid;
|
||||
|
||||
/**
|
||||
* @var GridFieldComponent
|
||||
*/
|
||||
protected $component;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $name;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $template = __CLASS__;
|
||||
|
||||
public function __construct(GridField $grid, GridFieldComponent $component, $name)
|
||||
{
|
||||
$this->grid = $grid;
|
||||
$this->component = $component;
|
||||
$this->name = $name;
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function index($request)
|
||||
{
|
||||
$result = $this->renderWith($this->template);
|
||||
|
||||
if ($request->isAjax()) {
|
||||
return $result;
|
||||
} else {
|
||||
return $this->getTopLevelController()->customise(array(
|
||||
'Content' => $result
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
public function Link($action = null)
|
||||
{
|
||||
return Controller::join_links($this->grid->Link(), $this->name, $action);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method should be overloaded to build out the detail form.
|
||||
*
|
||||
* @return Form
|
||||
*/
|
||||
public function Form()
|
||||
{
|
||||
$form = Form::create(
|
||||
$this,
|
||||
'SilverStripe\\Forms\\Form',
|
||||
FieldList::create($root = TabSet::create('Root', Tab::create('Main'))),
|
||||
FieldList::create()
|
||||
);
|
||||
|
||||
if ($this->getTopLevelController() instanceof LeftAndMain) {
|
||||
$form->setTemplate('LeftAndMain_EditForm');
|
||||
$form->addExtraClass('cms-content cms-edit-form cms-tabset center');
|
||||
$form->setAttribute('data-pjax-fragment', 'CurrentForm Content');
|
||||
|
||||
$root->setTemplate('CMSTabSet');
|
||||
$form->Backlink = $this->getBackLink();
|
||||
}
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Controller
|
||||
*/
|
||||
public function getController()
|
||||
{
|
||||
return $this->grid->getForm()->getController();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $template
|
||||
*/
|
||||
public function setTemplate($template)
|
||||
{
|
||||
$this->template = $template;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getTemplate()
|
||||
{
|
||||
return $this->template;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ArrayList
|
||||
*/
|
||||
public function getBreadcrumbs()
|
||||
{
|
||||
$controller = $this->getController();
|
||||
|
||||
if ($controller->hasMethod('Breadcrumbs')) {
|
||||
return $controller->Breadcrumbs();
|
||||
} else {
|
||||
return ArrayList::create();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function getBackLink()
|
||||
{
|
||||
$controller = $this->getTopLevelController();
|
||||
|
||||
if ($controller->hasMethod('Backlink')) {
|
||||
return $controller->Backlink();
|
||||
} else {
|
||||
return $controller->Link();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Controller
|
||||
*/
|
||||
protected function getTopLevelController()
|
||||
{
|
||||
$controller = $this->getController();
|
||||
|
||||
while ($controller) {
|
||||
if ($controller instanceof GridFieldRequestHandler) {
|
||||
$controller = $controller->getController();
|
||||
} elseif ($controller instanceof GridFieldDetailForm_ItemRequest) {
|
||||
$controller = $controller->getController();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $controller;
|
||||
}
|
||||
}
|
33
src/GridFieldTitleHeader.php
Normal file
33
src/GridFieldTitleHeader.php
Normal file
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace Symbiote\GridFieldExtensions;
|
||||
|
||||
use SilverStripe\Forms\GridField\AbstractGridFieldComponent;
|
||||
use SilverStripe\Forms\GridField\GridField_HTMLProvider;
|
||||
use SilverStripe\ORM\ArrayList;
|
||||
use SilverStripe\View\ArrayData;
|
||||
|
||||
/**
|
||||
* A simple header which displays column titles.
|
||||
*/
|
||||
class GridFieldTitleHeader extends AbstractGridFieldComponent implements GridField_HTMLProvider
|
||||
{
|
||||
|
||||
public function getHTMLFragments($grid)
|
||||
{
|
||||
$cols = ArrayList::create();
|
||||
|
||||
foreach ($grid->getColumns() as $name) {
|
||||
$meta = $grid->getColumnMetadata($name);
|
||||
|
||||
$cols->push(ArrayData::create(array(
|
||||
'Name' => $name,
|
||||
'Title' => $meta['title']
|
||||
)));
|
||||
}
|
||||
|
||||
return array(
|
||||
'header' => $cols->renderWith(__CLASS__)
|
||||
);
|
||||
}
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
<a href="$Link" class="action ss-ui-button ui-button add-existing-search" data-icon="magnifier">
|
||||
$Title
|
||||
</a>
|
@ -1,36 +0,0 @@
|
||||
$SearchForm
|
||||
|
||||
<h3><% _t("RESULTS", "Results") %></h3>
|
||||
<div class="add-existing-search-results">
|
||||
<% if $Items %>
|
||||
<ul class="add-existing-search-items" data-add-link="$Link('add')">
|
||||
<% loop $Items %>
|
||||
<li class="$EvenOdd"><a href="#" data-id="$ID">$Title</a></li>
|
||||
<% end_loop %>
|
||||
</ul>
|
||||
<% else %>
|
||||
<p><% _t("NOITEMS", "There are no items.") %></p>
|
||||
<% end_if %>
|
||||
|
||||
<% if $Items.MoreThanOnePage %>
|
||||
<ul class="add-existing-search-pagination">
|
||||
<% if $Items.NotFirstPage %>
|
||||
<li><a href="$Items.PrevLink">«</a></li>
|
||||
<% end_if %>
|
||||
|
||||
<% loop $Items.PaginationSummary(4) %>
|
||||
<% if $CurrentBool %>
|
||||
<li class="current">$PageNum</li>
|
||||
<% else_if $Link %>
|
||||
<li><a href="$Link">$PageNum</a></li>
|
||||
<% else %>
|
||||
<li>…</li>
|
||||
<% end_if %>
|
||||
<% end_loop %>
|
||||
|
||||
<% if $Items.NotLastPage %>
|
||||
<li><a href="$Items.NextLink">»</a></li>
|
||||
<%end_if %>
|
||||
</ul>
|
||||
<% end_if %>
|
||||
</div>
|
@ -1,3 +0,0 @@
|
||||
<button href="$Link" class="ss-gridfield-add-new-inline ss-ui-action-constructive ss-ui-button" data-icon="add">
|
||||
$Title
|
||||
</button>
|
@ -1,7 +0,0 @@
|
||||
<div class="ss-gridfield-add-new-multi-class">
|
||||
$ClassField.FieldHolder
|
||||
|
||||
<a href="#" data-href="$Link" class="ss-ui-action-constructive ss-ui-button" data-icon="add">
|
||||
$Title
|
||||
</a>
|
||||
</div>
|
@ -1,2 +0,0 @@
|
||||
<span class="handle"><span class="icon"></span></span>
|
||||
$SortField
|
@ -0,0 +1,3 @@
|
||||
<a href="$Link" class="$Classes">
|
||||
<span class="btn__title">$Title</span>
|
||||
</a>
|
@ -0,0 +1,56 @@
|
||||
$SearchForm
|
||||
|
||||
<h3><%t GridFieldExtensions.RESULTS "Results" %></h3>
|
||||
<div class="add-existing-search-results">
|
||||
<% if $Items %>
|
||||
<ul class="list-group add-existing-search-items" data-add-link="$Link('add')">
|
||||
<% loop $Items %>
|
||||
<li class="$EvenOdd list-group-item list-group-item-action">
|
||||
<a href="#" data-id="$ID">$Title</a>
|
||||
</li>
|
||||
<% end_loop %>
|
||||
</ul>
|
||||
<% else %>
|
||||
<p><%t GridFieldExtensions.NOITEMS "There are no items." %></p>
|
||||
<% end_if %>
|
||||
|
||||
<% if $Items.MoreThanOnePage %>
|
||||
<ul class="pagination add-existing-search-pagination">
|
||||
<% if $Items.NotFirstPage %>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="$Items.PrevLink">
|
||||
<span aria-hidden="true">«</span>
|
||||
<span class="sr-only"><%t GridFieldExtensions.PREVIOUS "Previous" %></span>
|
||||
</a>
|
||||
</li>
|
||||
<% end_if %>
|
||||
|
||||
<% loop $Items.PaginationSummary(2) %>
|
||||
<% if $CurrentBool %>
|
||||
<li class="page-item active">
|
||||
<a class="page-link" href="#">
|
||||
$PageNum <span class="sr-only"><%t GridFieldExtensions.CURRENT "(current)" %></span>
|
||||
</a>
|
||||
</li>
|
||||
<% else_if $Link %>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="$Link">
|
||||
$PageNum
|
||||
</a>
|
||||
</li>
|
||||
<% else %>
|
||||
<li class="page-item disabled">
|
||||
<a class="page-link" href="#">…</a>
|
||||
</li>
|
||||
<% end_if %>
|
||||
<% end_loop %>
|
||||
|
||||
<% if $Items.NotLastPage %>
|
||||
<a class="page-link" href="$Items.NextLink">
|
||||
<span aria-hidden="true">»</span>
|
||||
<span class="sr-only"><%t GridFieldExtensions.Next "Next" %></span>
|
||||
</a>
|
||||
<%end_if %>
|
||||
</ul>
|
||||
<% end_if %>
|
||||
</div>
|
@ -0,0 +1,3 @@
|
||||
<button href="$Link" class="ss-gridfield-add-new-inline btn btn-primary font-icon-plus-circled">
|
||||
$Title
|
||||
</button>
|
@ -3,7 +3,7 @@
|
||||
<% loop $Me %>
|
||||
<% if $IsActions %>
|
||||
<td$Attributes>
|
||||
<button class="ss-gridfield-delete-inline gridfield-button-delete ss-ui-button" data-icon="cross-circle"></button>
|
||||
<button class="ss-gridfield-delete-inline gridfield-button-delete action gridfield-button-delete btn--icon-md font-icon-trash-bin btn--no-text grid-field__icon-action form-group--no-label"></button>
|
||||
</td>
|
||||
<% else %>
|
||||
<td$Attributes>$Content</td>
|
@ -0,0 +1,5 @@
|
||||
<div class="ss-gridfield-add-new-multi-class">
|
||||
$ClassField.FieldHolder
|
||||
|
||||
<a href="#" data-href="$Link" data-add-multiclass class="btn btn-primary font-icon-plus btn__addnewmulticlass" data-icon="add">$Title</a>
|
||||
</div>
|
@ -0,0 +1,30 @@
|
||||
<tr>
|
||||
<td class="grid-field__paginator bottom-all" colspan="$Colspan">
|
||||
<span class="pagination-page-size">
|
||||
<%t Symbiote\\GridFieldExtensions\\GridFieldConfigurablePaginator.SHOW 'Show' is 'Verb. Example: Show 1 of 2' %>
|
||||
<select name="$PageSizesName" class="pagination-page-size-select no-change-track" data-skip-autofocus="true">
|
||||
<% loop $PageSizes %>
|
||||
<option <% if $Selected %>selected="selected"<% end_if %>>$Size</option>
|
||||
<% end_loop %>
|
||||
</select>
|
||||
$PageSizesSubmit
|
||||
</span>
|
||||
<% if not $OnlyOnePage %>
|
||||
<div class="grid-field__paginator__controls datagrid-pagination">
|
||||
$FirstPage $PreviousPage
|
||||
<span class="pagination-page-number">
|
||||
<%t SilverStripe\\Forms\\GridField\\GridFieldPaginator.Page 'Page' %>
|
||||
<input class="text no-change-track" value="$CurrentPageNum" data-skip-autofocus="true" />
|
||||
<%t SilverStripe\\Forms\\GridField\\GridFieldPaginator.OF 'of' is 'Example: View 1 of 2' %>
|
||||
$NumPages
|
||||
</span>
|
||||
$NextPage $LastPage
|
||||
</div>
|
||||
<% end_if %>
|
||||
<span class="grid-field__paginator_numbers pagination-records-number">
|
||||
{$FirstShownRecord}–{$LastShownRecord}
|
||||
<%t SilverStripe\\Forms\\GridField\\GridFieldPaginator.OF 'of' is 'Example: View 1 of 2' %>
|
||||
$NumRecords
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
@ -0,0 +1,2 @@
|
||||
<div class="handle"><i class="icon font-icon-drag-handle"></i></div>
|
||||
$SortField
|
@ -1,58 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace Symbiote\GridFieldExtensions\Tests;
|
||||
|
||||
use SilverStripe\Dev\SapphireTest;
|
||||
use SilverStripe\Dev\TestOnly;
|
||||
use SilverStripe\Forms\GridField\GridField;
|
||||
use Symbiote\GridFieldExtensions\GridFieldAddNewMultiClass;
|
||||
use Symbiote\GridFieldExtensions\Tests\Stub\StubA;
|
||||
use Symbiote\GridFieldExtensions\Tests\Stub\StubB;
|
||||
use Symbiote\GridFieldExtensions\Tests\Stub\StubC;
|
||||
|
||||
/**
|
||||
* Tests for {@link GridFieldAddNewMultiClass}.
|
||||
*/
|
||||
class GridFieldAddNewMultiClassTest extends SapphireTest {
|
||||
class GridFieldAddNewMultiClassTest extends SapphireTest
|
||||
{
|
||||
|
||||
public function testGetClasses() {
|
||||
$grid = new GridField('TestGridField');
|
||||
$grid->setModelClass('GridFieldAddNewMultiClassTest_A');
|
||||
public function testGetClasses()
|
||||
{
|
||||
$grid = new GridField('TestGridField');
|
||||
$grid->setModelClass(StubA::class);
|
||||
|
||||
$component = new GridFieldAddNewMultiClass();
|
||||
$component = new GridFieldAddNewMultiClass();
|
||||
|
||||
$this->assertEquals(
|
||||
array(
|
||||
'GridFieldAddNewMultiClassTest_A' => 'A',
|
||||
'GridFieldAddNewMultiClassTest_B' => 'B',
|
||||
'GridFieldAddNewMultiClassTest_C' => 'C'
|
||||
),
|
||||
$component->getClasses($grid),
|
||||
'Subclasses are populated by default and sorted'
|
||||
);
|
||||
$this->assertEquals(
|
||||
array(
|
||||
'Symbiote-GridFieldExtensions-Tests-Stub-StubA' => 'A',
|
||||
'Symbiote-GridFieldExtensions-Tests-Stub-StubB' => 'B',
|
||||
'Symbiote-GridFieldExtensions-Tests-Stub-StubC' => 'C'
|
||||
),
|
||||
$component->getClasses($grid),
|
||||
'Subclasses are populated by default and sorted'
|
||||
);
|
||||
|
||||
$component->setClasses(array(
|
||||
'GridFieldAddNewMultiClassTest_B' => 'Custom Title',
|
||||
'GridFieldAddNewMultiClassTest_A'
|
||||
));
|
||||
|
||||
$this->assertEquals(
|
||||
array(
|
||||
'GridFieldAddNewMultiClassTest_B' => 'Custom Title',
|
||||
'GridFieldAddNewMultiClassTest_A' => 'A'
|
||||
),
|
||||
$component->getClasses($grid),
|
||||
'Sorting and custom titles can be specified'
|
||||
);
|
||||
}
|
||||
$component->setClasses(array(
|
||||
StubB::class => 'Custom Title',
|
||||
StubA::class
|
||||
));
|
||||
|
||||
$this->assertEquals(
|
||||
array(
|
||||
'Symbiote-GridFieldExtensions-Tests-Stub-StubB' => 'Custom Title',
|
||||
'Symbiote-GridFieldExtensions-Tests-Stub-StubA' => 'A'
|
||||
),
|
||||
$component->getClasses($grid),
|
||||
'Sorting and custom titles can be specified'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**#@+
|
||||
* @ignore
|
||||
*/
|
||||
|
||||
class GridFieldAddNewMultiClassTest_A implements TestOnly {
|
||||
public function i18n_singular_name() {
|
||||
$class = get_class($this);
|
||||
return substr($class, strpos($class, '_') + 1);
|
||||
}
|
||||
|
||||
public function canCreate() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class GridFieldAddNewMultiClassTest_B extends GridFieldAddNewMultiClassTest_A implements TestOnly {}
|
||||
class GridFieldAddNewMultiClassTest_C extends GridFieldAddNewMultiClassTest_A implements TestOnly {}
|
||||
|
||||
/**#@-*/
|
||||
|
@ -1,60 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripeAustralia\Test;
|
||||
namespace Symbiote\GridFieldExtensions\Tests;
|
||||
|
||||
use SapphireTest;
|
||||
use SS_HTTPRequest;
|
||||
use Form, Controller, FieldList;
|
||||
use GridField, GridFieldDetailForm, GridFieldAddNewMultiClass;
|
||||
use SilverStripe\Control\Controller;
|
||||
use SilverStripe\Control\HTTPRequest;
|
||||
use SilverStripe\Dev\SapphireTest;
|
||||
use SilverStripe\Dev\TestOnly;
|
||||
use SilverStripe\Forms\FieldList;
|
||||
use SilverStripe\Forms\Form;
|
||||
use SilverStripe\Forms\GridField\GridField;
|
||||
use SilverStripe\Forms\GridField\GridFieldDetailForm;
|
||||
use Symbiote\GridFieldExtensions\GridFieldAddNewMultiClass;
|
||||
use Symbiote\GridFieldExtensions\GridFieldAddNewMultiClassHandler;
|
||||
use Symbiote\GridFieldExtensions\Tests\Stub\NamespacedClass;
|
||||
|
||||
class GridFieldAddNewMultiClassWithNamespacesTest extends SapphireTest {
|
||||
class GridFieldAddNewMultiClassWithNamespacesTest extends SapphireTest
|
||||
{
|
||||
|
||||
public function testGetClassesWithNamespaces() {
|
||||
$grid = new GridField('TestGridField');
|
||||
$grid->setModelClass('SilverStripeAustralia\\Test\\NamespacedClass');
|
||||
public function testGetClassesWithNamespaces()
|
||||
{
|
||||
$grid = new GridField('TestGridField');
|
||||
$grid->setModelClass(NamespacedClass::class);
|
||||
|
||||
$component = new GridFieldAddNewMultiClass();
|
||||
$component = new GridFieldAddNewMultiClass();
|
||||
|
||||
$this->assertEquals(
|
||||
array(
|
||||
'SilverStripeAustralia-Test-NamespacedClass' => 'NamespacedClass'
|
||||
),
|
||||
$component->getClasses($grid),
|
||||
'Namespaced classes are sanitised'
|
||||
);
|
||||
}
|
||||
$this->assertEquals(
|
||||
array(
|
||||
'Symbiote-GridFieldExtensions-Tests-Stub-NamespacedClass' => 'NamespacedClass'
|
||||
),
|
||||
$component->getClasses($grid),
|
||||
'Namespaced classes are sanitised'
|
||||
);
|
||||
}
|
||||
|
||||
public function testHandleAddWithNamespaces() {
|
||||
$grid = new GridField('TestGridField');
|
||||
$grid->getConfig()->addComponent(new GridFieldDetailForm());
|
||||
$grid->setModelClass('SilverStripeAustralia\\Test\\NamespacedClass');
|
||||
$grid->setForm(Form::create('test', Controller::create(), FieldList::create(), FieldList::create()));
|
||||
public function testHandleAddWithNamespaces()
|
||||
{
|
||||
$grid = new GridField('TestGridField');
|
||||
$grid->getConfig()->addComponent(new GridFieldDetailForm());
|
||||
$grid->setModelClass(NamespacedClass::class);
|
||||
$grid->setForm(Form::create(Controller::create(), 'test', FieldList::create(), FieldList::create()));
|
||||
|
||||
$request = new SS_HTTPRequest('POST', 'test');
|
||||
$request->setRouteParams(array('ClassName' => 'SilverStripeAustralia-Test-NamespacedClass'));
|
||||
$request = new HTTPRequest('POST', 'test');
|
||||
$request->setRouteParams(array('ClassName' => 'Symbiote-GridFieldExtensions-Tests-Stub-NamespacedClass'));
|
||||
|
||||
$component = new GridFieldAddNewMultiClass();
|
||||
$response = $component->handleAdd($grid, $request);
|
||||
|
||||
$record = new \ReflectionProperty('GridFieldAddNewMultiClassHandler', 'record');
|
||||
$record->setAccessible(true);
|
||||
$this->assertInstanceOf('SilverStripeAustralia\\Test\\NamespacedClass', $record->getValue($response));
|
||||
}
|
||||
$component = new GridFieldAddNewMultiClass();
|
||||
$response = $component->handleAdd($grid, $request);
|
||||
|
||||
$record = new \ReflectionProperty(GridFieldAddNewMultiClassHandler::class, 'record');
|
||||
$record->setAccessible(true);
|
||||
$this->assertInstanceOf(NamespacedClass::class, $record->getValue($response));
|
||||
}
|
||||
}
|
||||
|
||||
/**#@+
|
||||
* @ignore
|
||||
*/
|
||||
|
||||
class NamespacedClass {
|
||||
public function i18n_singular_name() {
|
||||
return 'NamespacedClass';
|
||||
}
|
||||
|
||||
public function canCreate() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**#@-*/
|
||||
|
238
tests/GridFieldConfigurablePaginatorTest.php
Normal file
238
tests/GridFieldConfigurablePaginatorTest.php
Normal file
@ -0,0 +1,238 @@
|
||||
<?php
|
||||
|
||||
namespace Symbiote\GridFieldExtensions\Tests;
|
||||
|
||||
use SilverStripe\Core\Config\Config;
|
||||
use SilverStripe\Dev\SapphireTest;
|
||||
use SilverStripe\Forms\GridField\GridField;
|
||||
use SilverStripe\Forms\GridField\GridField_FormAction;
|
||||
use SilverStripe\ORM\ArrayList;
|
||||
use Symbiote\GridFieldExtensions\GridFieldConfigurablePaginator;
|
||||
|
||||
class GridFieldConfigurablePaginatorTest extends SapphireTest
|
||||
{
|
||||
/**
|
||||
* @var GridField
|
||||
*/
|
||||
protected $gridField;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
// Some dummy GridField list data
|
||||
$data = ArrayList::create();
|
||||
for ($i = 1; $i <= 130; $i++) {
|
||||
$data->push(array('ID' => $i));
|
||||
}
|
||||
|
||||
$this->gridField = GridField::create('Mock', null, $data);
|
||||
}
|
||||
|
||||
public function testGetTotalRecords()
|
||||
{
|
||||
$paginator = new GridFieldConfigurablePaginator;
|
||||
$paginator->setGridField($this->gridField);
|
||||
|
||||
$this->assertSame(130, $paginator->getTotalRecords());
|
||||
}
|
||||
|
||||
public function testGetFirstShown()
|
||||
{
|
||||
$paginator = new GridFieldConfigurablePaginator;
|
||||
$paginator->setGridField($this->gridField);
|
||||
|
||||
// No state
|
||||
$this->assertSame(1, $paginator->getFirstShown());
|
||||
|
||||
// With a state
|
||||
$paginator->setFirstShown(123);
|
||||
$this->assertSame(123, $paginator->getFirstShown());
|
||||
|
||||
// Too high!
|
||||
$paginator->setFirstShown(234);
|
||||
$this->assertSame(1, $paginator->getFirstShown());
|
||||
}
|
||||
|
||||
public function testGetLastShown()
|
||||
{
|
||||
$paginator = new GridFieldConfigurablePaginator(20, array(10, 20, 30));
|
||||
$paginator->setGridField($this->gridField);
|
||||
|
||||
$this->assertSame(20, $paginator->getLastShown());
|
||||
|
||||
$paginator->setFirstShown(5);
|
||||
$this->assertSame(24, $paginator->getLastShown());
|
||||
}
|
||||
|
||||
public function testGetTotalPages()
|
||||
{
|
||||
$paginator = new GridFieldConfigurablePaginator(20, array(20, 40, 60));
|
||||
$paginator->setGridField($this->gridField);
|
||||
|
||||
// Default calculation
|
||||
$this->assertSame(7, $paginator->getTotalPages());
|
||||
|
||||
// With a standard "first shown" record number, e.g. page 2
|
||||
$paginator->setFirstShown(21);
|
||||
$this->assertSame(7, $paginator->getTotalPages());
|
||||
|
||||
// Non-standard "first shown", e.g. when a page size is changed at page 3. In this case the first page is
|
||||
// 20 records, the second page is 7 records, third page 20 records, etc
|
||||
$paginator->setFirstShown(27);
|
||||
$this->assertSame(8, $paginator->getTotalPages());
|
||||
|
||||
// ... and when the page size has also been changed. In this case the first page is 57 records, second page
|
||||
// 60 records and last page is 13 records
|
||||
$paginator->setFirstShown(57);
|
||||
$paginator->setItemsPerPage(60);
|
||||
$this->assertSame(3, $paginator->getTotalPages());
|
||||
}
|
||||
|
||||
public function testItemsPerPageIsSetToFirstInPageSizesListWhenChanged()
|
||||
{
|
||||
$paginator = new GridFieldConfigurablePaginator(20, array(20, 40, 60));
|
||||
$paginator->setGridField($this->gridField);
|
||||
|
||||
// Initial state, should be what was provided to the constructor
|
||||
$this->assertSame(20, $paginator->getItemsPerPage());
|
||||
|
||||
$paginator->setPageSizes(array(50, 100, 200));
|
||||
|
||||
// Set via public API, should now be set to 50
|
||||
$this->assertSame(50, $paginator->getItemsPerPage());
|
||||
}
|
||||
|
||||
public function testGetCurrentPreviousAndNextPages()
|
||||
{
|
||||
$paginator = new GridFieldConfigurablePaginator(20, array(20, 40, 60));
|
||||
$paginator->setGridField($this->gridField);
|
||||
|
||||
// No page selected (first page)
|
||||
$this->assertSame(1, $paginator->getCurrentPage());
|
||||
$this->assertSame(1, $paginator->getPreviousPage());
|
||||
$this->assertSame(2, $paginator->getNextPage());
|
||||
|
||||
// Second page
|
||||
$paginator->setFirstShown(21);
|
||||
$this->assertSame(2, $paginator->getCurrentPage());
|
||||
$this->assertSame(1, $paginator->getPreviousPage());
|
||||
$this->assertSame(3, $paginator->getNextPage());
|
||||
|
||||
// Third page
|
||||
$paginator->setFirstShown(41);
|
||||
$this->assertSame(3, $paginator->getCurrentPage());
|
||||
$this->assertSame(2, $paginator->getPreviousPage());
|
||||
$this->assertSame(4, $paginator->getNextPage());
|
||||
|
||||
// Fourth page, partial record count
|
||||
$paginator->setFirstShown(42);
|
||||
$this->assertSame(4, $paginator->getCurrentPage());
|
||||
$this->assertSame(3, $paginator->getPreviousPage());
|
||||
$this->assertSame(5, $paginator->getNextPage());
|
||||
|
||||
// Last page (default paging)
|
||||
$paginator->setFirstShown(121);
|
||||
$this->assertSame(7, $paginator->getCurrentPage());
|
||||
$this->assertSame(6, $paginator->getPreviousPage());
|
||||
$this->assertSame(7, $paginator->getNextPage());
|
||||
|
||||
// Non-standard page size should recalculate the page numbers to be relative to the page size
|
||||
$paginator->setFirstShown(121);
|
||||
$paginator->setItemsPerPage(60);
|
||||
$this->assertSame(3, $paginator->getCurrentPage());
|
||||
$this->assertSame(2, $paginator->getPreviousPage());
|
||||
$this->assertSame(3, $paginator->getNextPage());
|
||||
}
|
||||
|
||||
public function testPageSizesAreConfigurable()
|
||||
{
|
||||
// Via constructor
|
||||
$paginator = new GridFieldConfigurablePaginator(3, array(2, 4, 6));
|
||||
$this->assertSame(3, $paginator->getItemsPerPage());
|
||||
$this->assertSame(array(2, 4, 6), $paginator->getPageSizes());
|
||||
|
||||
// Via public API
|
||||
$paginator->setPageSizes(array(10, 20, 30));
|
||||
$this->assertSame(array(10, 20, 30), $paginator->getPageSizes());
|
||||
|
||||
// Via default configuration
|
||||
$paginator = new GridFieldConfigurablePaginator;
|
||||
$default = Config::inst()->get(GridFieldConfigurablePaginator::class, 'default_page_sizes');
|
||||
$this->assertSame($default, $paginator->getPageSizes());
|
||||
}
|
||||
|
||||
public function testGetPageSizesAsList()
|
||||
{
|
||||
$paginator = new GridFieldConfigurablePaginator(10, array(10, 20, 30));
|
||||
$this->assertListEquals(array(
|
||||
array('Size' => '10', 'Selected' => true),
|
||||
array('Size' => '20', 'Selected' => false),
|
||||
array('Size' => '30', 'Selected' => false),
|
||||
), $paginator->getPageSizesAsList());
|
||||
}
|
||||
|
||||
public function testGetGridFieldThrowsExceptionWhenNotSet()
|
||||
{
|
||||
$this->expectException(\Exception::class);
|
||||
$this->expectExceptionMessage('No GridField available yet for this request!');
|
||||
$paginator = new GridFieldConfigurablePaginator;
|
||||
$paginator->getGridField();
|
||||
}
|
||||
|
||||
public function testGetPagerActions()
|
||||
{
|
||||
$controls = array(
|
||||
'prev' => array(
|
||||
'title' => 'Previous',
|
||||
'args' => array(
|
||||
'next-page' => 123,
|
||||
'first-shown' => 234
|
||||
),
|
||||
'extra-class' => 'ss-gridfield-previouspage',
|
||||
'disable-previous' => false
|
||||
),
|
||||
'next' => array(
|
||||
'title' => 'Next',
|
||||
'args' => array(
|
||||
'next-page' => 234,
|
||||
'first-shown' => 123
|
||||
),
|
||||
'extra-class' => 'ss-gridfield-nextpage',
|
||||
'disable-next' => true
|
||||
)
|
||||
);
|
||||
|
||||
$paginator = new GridFieldConfigurablePaginator;
|
||||
$paginator->setGridField($this->gridField);
|
||||
$result = $paginator->getPagerActions($controls, $this->gridField);
|
||||
|
||||
$this->assertCount(2, $result);
|
||||
$this->assertArrayHasKey('next', $result);
|
||||
$this->assertContainsOnlyInstancesOf(GridField_FormAction::class, $result);
|
||||
|
||||
$this->assertFalse($result['prev']->isDisabled());
|
||||
|
||||
$this->assertTrue((bool) $result['next']->hasClass('ss-gridfield-nextpage'));
|
||||
$this->assertTrue($result['next']->isDisabled());
|
||||
}
|
||||
|
||||
public function testSinglePageWithLotsOfItems()
|
||||
{
|
||||
$paginator = new GridFieldConfigurablePaginator(null, array(100, 200, 300));
|
||||
$this->assertSame(100, $paginator->getItemsPerPage());
|
||||
}
|
||||
|
||||
/**
|
||||
* Set something to the GridField's paginator state data
|
||||
*
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
* @return $this
|
||||
*/
|
||||
protected function setState($key, $value)
|
||||
{
|
||||
$this->gridField->State->GridFieldConfigurablePaginator->$key = $value;
|
||||
return $this;
|
||||
}
|
||||
}
|
94
tests/GridFieldEditableColumnsTest.php
Normal file
94
tests/GridFieldEditableColumnsTest.php
Normal file
@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace Symbiote\GridFieldExtensions\Tests;
|
||||
|
||||
use Symbiote\GridFieldExtensions\Tests\Stub\TestController;
|
||||
use Symbiote\GridFieldExtensions\Tests\Stub\StubUnorderable;
|
||||
use Symbiote\GridFieldExtensions\GridFieldEditableColumns;
|
||||
use SilverStripe\ORM\FieldType\DBHTMLText;
|
||||
use SilverStripe\Forms\TextField;
|
||||
use SilverStripe\Forms\GridField\GridField;
|
||||
use SilverStripe\Forms\Form;
|
||||
use SilverStripe\Forms\FieldList;
|
||||
use SilverStripe\Dev\SapphireTest;
|
||||
|
||||
class GridFieldEditableColumnsTest extends SapphireTest
|
||||
{
|
||||
private function getMockGrid()
|
||||
{
|
||||
$controller = new TestController('Test');
|
||||
$form = new Form($controller, 'TestForm', new FieldList(
|
||||
$grid = new GridField('TestGridField')
|
||||
), new FieldList());
|
||||
$grid->setModelClass(StubUnorderable::class);
|
||||
$grid->setList(StubUnorderable::get());
|
||||
return $grid;
|
||||
}
|
||||
|
||||
private function getMockRecord($id, $title)
|
||||
{
|
||||
$record = new StubUnorderable();
|
||||
$record->ID = $id;
|
||||
$record->Title = $title;
|
||||
return $record;
|
||||
}
|
||||
|
||||
public function testProvidesEditableFieldsInColumns()
|
||||
{
|
||||
$grid = $this->getMockGrid();
|
||||
$component = new GridFieldEditableColumns();
|
||||
$record = $this->getMockRecord(100, "foo");
|
||||
|
||||
$this->assertEquals(
|
||||
[ 'Title' ],
|
||||
$component->getColumnsHandled($grid)
|
||||
);
|
||||
|
||||
$record->setCanEdit(true);
|
||||
$column = $component->getColumnContent($grid, $record, 'Title');
|
||||
|
||||
$this->assertInstanceOf(DBHTMLText::class, $column);
|
||||
$this->assertMatchesRegularExpression(
|
||||
'/<input type="text" name="TestGridField\[GridFieldEditableColumns\]\[100\]\[Title\]" value="foo"[^>]*>/',
|
||||
$column->getValue()
|
||||
);
|
||||
}
|
||||
|
||||
public function testProvidesReadonlyColumnsForNoneditableRecords()
|
||||
{
|
||||
$grid = $this->getMockGrid();
|
||||
$component = new GridFieldEditableColumns();
|
||||
$record = $this->getMockRecord(100, "testval");
|
||||
|
||||
$record->setCanEdit(false);
|
||||
$column = $component->getColumnContent($grid, $record, 'Title');
|
||||
|
||||
$this->assertInstanceOf(DBHTMLText::class, $column);
|
||||
$this->assertMatchesRegularExpression(
|
||||
'/<span[^>]*>\s*testval\s*<\/span>/',
|
||||
$column->getValue()
|
||||
);
|
||||
}
|
||||
|
||||
public function testProvidesReadonlyColumnsForReadonlyGrids()
|
||||
{
|
||||
$grid = $this->getMockGrid();
|
||||
$component = new GridFieldEditableColumns();
|
||||
$record = $this->getMockRecord(100, "testval");
|
||||
|
||||
$record->setCanEdit(true);
|
||||
$grid = $grid->performReadonlyTransformation();
|
||||
|
||||
if (!$grid instanceof GridField) {
|
||||
$this->markTestSkipped('silverstripe/framework <4.2.2 doesn\'t support readonly GridFields');
|
||||
}
|
||||
|
||||
$column = $component->getColumnContent($grid, $record, 'Title');
|
||||
|
||||
$this->assertInstanceOf(DBHTMLText::class, $column);
|
||||
$this->assertMatchesRegularExpression(
|
||||
'/<span[^>]*>\s*testval\s*<\/span>/',
|
||||
$column->getValue()
|
||||
);
|
||||
}
|
||||
}
|
@ -1,124 +1,364 @@
|
||||
<?php
|
||||
|
||||
namespace Symbiote\GridFieldExtensions\Tests;
|
||||
|
||||
use ReflectionMethod;
|
||||
use SilverStripe\Dev\SapphireTest;
|
||||
use SilverStripe\Forms\GridField\GridField;
|
||||
use SilverStripe\Forms\GridField\GridFieldConfig_RelationEditor;
|
||||
use SilverStripe\ORM\DataList;
|
||||
use Symbiote\GridFieldExtensions\GridFieldOrderableRows;
|
||||
use Symbiote\GridFieldExtensions\Tests\Stub\PolymorphM2MMapper;
|
||||
use Symbiote\GridFieldExtensions\Tests\Stub\PolymorphM2MParent;
|
||||
use Symbiote\GridFieldExtensions\Tests\Stub\StubOrderableChild;
|
||||
use Symbiote\GridFieldExtensions\Tests\Stub\StubOrdered;
|
||||
use Symbiote\GridFieldExtensions\Tests\Stub\StubOrderedVersioned;
|
||||
use Symbiote\GridFieldExtensions\Tests\Stub\StubParent;
|
||||
use Symbiote\GridFieldExtensions\Tests\Stub\StubSubclass;
|
||||
use Symbiote\GridFieldExtensions\Tests\Stub\StubSubclassOrderedVersioned;
|
||||
use Symbiote\GridFieldExtensions\Tests\Stub\StubUnorderable;
|
||||
use Symbiote\GridFieldExtensions\Tests\Stub\ThroughBelongs;
|
||||
use Symbiote\GridFieldExtensions\Tests\Stub\ThroughBelongsVersioned;
|
||||
use Symbiote\GridFieldExtensions\Tests\Stub\ThroughDefiner;
|
||||
use Symbiote\GridFieldExtensions\Tests\Stub\ThroughDefinerVersioned;
|
||||
use Symbiote\GridFieldExtensions\Tests\Stub\ThroughIntermediary;
|
||||
use Symbiote\GridFieldExtensions\Tests\Stub\TitleObject;
|
||||
use Symbiote\GridFieldExtensions\Tests\Stub\TitleSortedObject;
|
||||
use Symbiote\GridFieldExtensions\Tests\Stub\TitleArraySortedObject;
|
||||
use Symbiote\GridFieldExtensions\Tests\Stub\ThroughIntermediaryVersioned;
|
||||
|
||||
/**
|
||||
* Tests for the {@link GridFieldOrderableRows} component.
|
||||
*/
|
||||
class GridFieldOrderableRowsTest extends SapphireTest {
|
||||
class GridFieldOrderableRowsTest extends SapphireTest
|
||||
{
|
||||
protected static $fixture_file = [
|
||||
'GridFieldOrderableRowsTest.yml',
|
||||
'OrderableRowsThroughTest.yml',
|
||||
// 'OrderablePolymorphicManyToMany.yml' // TODO: introduce this tests in the next minor release
|
||||
];
|
||||
|
||||
protected $usesDatabase = true;
|
||||
protected static $extra_dataobjects = [
|
||||
// PolymorphM2MChild::class,
|
||||
// PolymorphM2MMapper::class,
|
||||
// PolymorphM2MParent::class,
|
||||
StubParent::class,
|
||||
StubOrdered::class,
|
||||
StubSubclass::class,
|
||||
StubUnorderable::class,
|
||||
StubOrderableChild::class,
|
||||
StubOrderedVersioned::class,
|
||||
StubSubclassOrderedVersioned::class,
|
||||
ThroughDefiner::class,
|
||||
ThroughIntermediary::class,
|
||||
ThroughBelongs::class,
|
||||
TitleObject::class,
|
||||
TitleSortedObject::class,
|
||||
TitleArraySortedObject::class,
|
||||
ThroughDefinerVersioned::class,
|
||||
ThroughIntermediaryVersioned::class,
|
||||
ThroughBelongsVersioned::class,
|
||||
];
|
||||
|
||||
protected static $fixture_file = 'GridFieldOrderableRowsTest.yml';
|
||||
public function reorderItemsProvider()
|
||||
{
|
||||
return [
|
||||
[StubParent::class . '.parent', 'MyHasMany', 'Sort'],
|
||||
[StubParent::class . '.parent', 'MyHasManySubclass', 'Sort'],
|
||||
[StubParent::class . '.parent-subclass-ordered-versioned', 'MyHasManySubclassOrderedVersioned', 'Sort'],
|
||||
[StubParent::class . '.parent', 'MyManyMany', 'ManyManySort'],
|
||||
[StubParent::class . '.parent', 'MyManyManyVersioned', 'ManyManySort'],
|
||||
[ThroughDefiner::class . '.DefinerOne', 'Belongings', 'Sort'],
|
||||
[ThroughDefinerVersioned::class . '.DefinerOne', 'Belongings', 'Sort'],
|
||||
// [PolymorphM2MParent::class . '.ParentOne', 'Children', 'Sort']
|
||||
];
|
||||
}
|
||||
|
||||
protected $extraDataObjects = array(
|
||||
'GridFieldOrderableRowsTest_Parent',
|
||||
'GridFieldOrderableRowsTest_Ordered',
|
||||
'GridFieldOrderableRowsTest_Subclass',
|
||||
);
|
||||
/**
|
||||
* @dataProvider reorderItemsProvider
|
||||
*/
|
||||
public function testReorderItems($fixtureID, $relationName, $sortName)
|
||||
{
|
||||
$orderable = new GridFieldOrderableRows($sortName);
|
||||
$reflection = new ReflectionMethod($orderable, 'executeReorder');
|
||||
$reflection->setAccessible(true);
|
||||
|
||||
public function testReorderItems() {
|
||||
$orderable = new GridFieldOrderableRows('ManyManySort');
|
||||
$reflection = new ReflectionMethod($orderable, 'executeReorder');
|
||||
$reflection->setAccessible(true);
|
||||
$config = new GridFieldConfig_RelationEditor();
|
||||
$config->addComponent($orderable);
|
||||
|
||||
$parent = $this->objFromFixture('GridFieldOrderableRowsTest_Parent', 'parent');
|
||||
list($parentClass, $parentInstanceID) = explode('.', $fixtureID ?? '');
|
||||
$parent = $this->objFromFixture($parentClass, $parentInstanceID);
|
||||
|
||||
$config = new GridFieldConfig_RelationEditor();
|
||||
$config->addComponent($orderable);
|
||||
$grid = new GridField(
|
||||
$relationName,
|
||||
'Testing Many Many',
|
||||
$parent->$relationName()->sort($sortName),
|
||||
$config
|
||||
);
|
||||
|
||||
$grid = new GridField(
|
||||
'MyManyMany',
|
||||
'My Many Many',
|
||||
$parent->MyManyMany()->sort('ManyManySort'),
|
||||
$config
|
||||
);
|
||||
$originalOrder = $parent->$relationName()->sort($sortName)->column('ID');
|
||||
$desiredOrder = [];
|
||||
|
||||
$originalOrder = $parent->MyManyMany()->sort('ManyManySort')->column('ID');
|
||||
$desiredOrder = array();
|
||||
// Make order non-contiguous, and 1-based
|
||||
foreach (array_reverse($originalOrder ?? []) as $index => $id) {
|
||||
$desiredOrder[$index * 2 + 1] = $id;
|
||||
}
|
||||
|
||||
// Make order non-contiguous, and 1-based
|
||||
foreach(array_reverse($originalOrder) as $index => $id) {
|
||||
$desiredOrder[$index * 2 + 1] = $id;
|
||||
}
|
||||
$this->assertNotEquals($originalOrder, $desiredOrder);
|
||||
|
||||
$this->assertNotEquals($originalOrder, $desiredOrder);
|
||||
$reflection->invoke($orderable, $grid, $desiredOrder);
|
||||
|
||||
$reflection->invoke($orderable, $grid, $desiredOrder);
|
||||
$newOrder = $parent->$relationName()->sort($sortName)->map($sortName, 'ID')->toArray();
|
||||
|
||||
$newOrder = $parent->MyManyMany()->sort('ManyManySort')->map('ManyManySort', 'ID')->toArray();
|
||||
$this->assertEquals($desiredOrder, $newOrder);
|
||||
}
|
||||
|
||||
$this->assertEquals($desiredOrder, $newOrder);
|
||||
public function testManyManyThroughListSortOrdersAreUsedForInitialRender()
|
||||
{
|
||||
/** @var ThroughDefiner $record */
|
||||
$record = $this->objFromFixture(ThroughDefiner::class, 'DefinerOne');
|
||||
|
||||
}
|
||||
$orderable = new GridFieldOrderableRows('Sort');
|
||||
$config = new GridFieldConfig_RelationEditor();
|
||||
$config->addComponent($orderable);
|
||||
|
||||
/**
|
||||
* @covers GridFieldOrderableRows::getSortTable
|
||||
*/
|
||||
public function testGetSortTable() {
|
||||
$orderable = new GridFieldOrderableRows();
|
||||
$grid = new GridField(
|
||||
'Belongings',
|
||||
'Testing Many Many',
|
||||
$record->Belongings()->sort('Sort'),
|
||||
$config
|
||||
);
|
||||
|
||||
$parent = new GridFieldOrderableRowsTest_Parent();
|
||||
$parent->write();
|
||||
// Get the first record, which would be the first one to have column contents generated
|
||||
/** @var ThroughIntermediary $expected */
|
||||
$intermediary = $this->objFromFixture(ThroughIntermediary::class, 'One');
|
||||
|
||||
$this->assertEquals(
|
||||
'GridFieldOrderableRowsTest_Ordered',
|
||||
$orderable->getSortTable($parent->MyHasMany())
|
||||
);
|
||||
$result = $orderable->getColumnContent($grid, $record, 'irrelevant');
|
||||
|
||||
$this->assertEquals(
|
||||
'GridFieldOrderableRowsTest_Ordered',
|
||||
$orderable->getSortTable($parent->MyHasManySubclass())
|
||||
);
|
||||
$this->assertStringContainsString(
|
||||
'Belongings[GridFieldEditableColumns][' . $record->ID . '][Sort]',
|
||||
$result,
|
||||
'The field name is indexed under the record\'s ID'
|
||||
);
|
||||
$this->assertStringContainsString(
|
||||
'value="' . $intermediary->Sort . '"',
|
||||
$result,
|
||||
'The value comes from the MMTL intermediary Sort value'
|
||||
);
|
||||
}
|
||||
|
||||
$this->assertEquals(
|
||||
'GridFieldOrderableRowsTest_Ordered',
|
||||
$orderable->getSortTable($parent->MyManyMany())
|
||||
);
|
||||
public function testPolymorphicManyManyListSortOrdersAreUsedForInitialRender()
|
||||
{
|
||||
$this->markTestSkipped('TODO: Introduce this test in the next minor release (3.3)');
|
||||
|
||||
$this->assertEquals(
|
||||
'GridFieldOrderableRowsTest_Parent_MyManyMany',
|
||||
$orderable->setSortField('ManyManySort')->getSortTable($parent->MyManyMany())
|
||||
);
|
||||
}
|
||||
$record = $this->objFromFixture(PolymorphM2MParent::class, 'ParentOne');
|
||||
|
||||
$orderable = new GridFieldOrderableRows('Sort');
|
||||
$config = new GridFieldConfig_RelationEditor();
|
||||
$config->addComponent($orderable);
|
||||
|
||||
$grid = new GridField(
|
||||
'Children',
|
||||
'Testing Polymorphic Many Many',
|
||||
$record->Children()->sort('Sort'),
|
||||
$config
|
||||
);
|
||||
|
||||
// Get the first record, which would be the first one to have column contents generated
|
||||
$intermediary = $this->objFromFixture(PolymorphM2MMapper::class, 'MapP1ToC1');
|
||||
|
||||
$result = $orderable->getColumnContent($grid, $record, 'irrelevant');
|
||||
|
||||
$this->assertStringContainsString(
|
||||
'Children[GridFieldEditableColumns][' . $record->ID . '][Sort]',
|
||||
$result,
|
||||
'The field name is indexed under the record\'s ID'
|
||||
);
|
||||
$this->assertStringContainsString(
|
||||
'value="' . $intermediary->Sort . '"',
|
||||
$result,
|
||||
'The value comes from the MMTL intermediary Sort value'
|
||||
);
|
||||
}
|
||||
|
||||
public function testSortableChildClass()
|
||||
{
|
||||
$orderable = new GridFieldOrderableRows('Sort');
|
||||
$reflection = new ReflectionMethod($orderable, 'executeReorder');
|
||||
$reflection->setAccessible(true);
|
||||
|
||||
$parent = $this->objFromFixture(StubOrdered::class, 'nestedtest');
|
||||
|
||||
$config = new GridFieldConfig_RelationEditor();
|
||||
$config->addComponent($orderable);
|
||||
|
||||
$grid = new GridField(
|
||||
'Children',
|
||||
'Children',
|
||||
$parent->Children(),
|
||||
$config
|
||||
);
|
||||
|
||||
$originalOrder = $parent->Children()->column('ID');
|
||||
$desiredOrder = array_reverse($originalOrder ?? []);
|
||||
|
||||
$this->assertNotEquals($originalOrder, $desiredOrder);
|
||||
|
||||
$reflection->invoke($orderable, $grid, $desiredOrder);
|
||||
|
||||
$newOrder = $parent->Children()->column('ID');
|
||||
|
||||
$this->assertEquals($desiredOrder, $newOrder);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers \Symbiote\GridFieldExtensions\GridFieldOrderableRows::getSortTable
|
||||
*/
|
||||
public function testGetSortTable()
|
||||
{
|
||||
$orderable = new GridFieldOrderableRows();
|
||||
|
||||
$parent = new StubParent();
|
||||
$parent->write();
|
||||
|
||||
$this->assertEquals(
|
||||
'StubOrdered',
|
||||
$orderable->getSortTable($parent->MyHasMany())
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
'StubOrdered',
|
||||
$orderable->getSortTable($parent->MyHasManySubclass())
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
'StubOrdered',
|
||||
$orderable->getSortTable($parent->MyManyMany())
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
'StubParent_MyManyMany',
|
||||
$orderable->setSortField('ManyManySort')->getSortTable($parent->MyManyMany())
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
'StubOrderedVersioned',
|
||||
$orderable->setSortField('Sort')->getSortTable($parent->MyHasManySubclassOrderedVersioned())
|
||||
);
|
||||
}
|
||||
|
||||
public function testReorderItemsSubclassVersioned()
|
||||
{
|
||||
$orderable = new GridFieldOrderableRows('Sort');
|
||||
$reflection = new ReflectionMethod($orderable, 'executeReorder');
|
||||
$reflection->setAccessible(true);
|
||||
|
||||
$parent = $this->objFromFixture(StubParent::class, 'parent-subclass-ordered-versioned');
|
||||
|
||||
// make sure all items are published
|
||||
foreach ($parent->MyHasManySubclassOrderedVersioned() as $item) {
|
||||
$item->publishRecursive();
|
||||
}
|
||||
|
||||
// there should be no difference between stages at this point
|
||||
$differenceFound = false;
|
||||
foreach ($parent->MyHasManySubclassOrderedVersioned() as $item) {
|
||||
/** @var StubSubclassOrderedVersioned|Versioned $item */
|
||||
if ($item->stagesDiffer()) {
|
||||
$this->fail('Unexpected difference found on stages');
|
||||
}
|
||||
}
|
||||
|
||||
// reorder items
|
||||
$config = new GridFieldConfig_RelationEditor();
|
||||
$config->addComponent($orderable);
|
||||
|
||||
$grid = new GridField(
|
||||
'TestField',
|
||||
'TestField',
|
||||
$parent->MyHasManySubclassOrderedVersioned()->sort('Sort', 'ASC'),
|
||||
$config
|
||||
);
|
||||
|
||||
$originalOrder = $parent->MyHasManySubclassOrderedVersioned()
|
||||
->sort('Sort', 'ASC')
|
||||
->column('ID');
|
||||
|
||||
$desiredOrder = [];
|
||||
|
||||
// Make order non-contiguous, and 1-based
|
||||
foreach (array_reverse($originalOrder ?? []) as $index => $id) {
|
||||
$desiredOrder[$index * 2 + 1] = $id;
|
||||
}
|
||||
|
||||
$this->assertNotEquals($originalOrder, $desiredOrder);
|
||||
|
||||
$reflection->invoke($orderable, $grid, $desiredOrder);
|
||||
|
||||
$newOrder = $parent->MyHasManySubclassOrderedVersioned()
|
||||
->sort('Sort', 'ASC')
|
||||
->map('Sort', 'ID')
|
||||
->toArray();
|
||||
|
||||
$this->assertEquals($desiredOrder, $newOrder);
|
||||
|
||||
// reorder should have been handled as versioned - there should be a difference between stages now
|
||||
$differenceFound = false;
|
||||
foreach ($parent->MyHasManySubclassOrderedVersioned() as $item) {
|
||||
if ($item->stagesDiffer()) {
|
||||
$differenceFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertTrue($differenceFound);
|
||||
}
|
||||
|
||||
public function testGetManipulatedDataWithoutDefaultSort()
|
||||
{
|
||||
$sortedList = $this->getTitleSortedListForManipuatedData(TitleObject::class, [
|
||||
['Title' => 'C'],
|
||||
['Title' => 'A'],
|
||||
['Title' => 'B'],
|
||||
]);
|
||||
$this->assertSame(['A', 'B', 'C'], $sortedList->column('Title'));
|
||||
}
|
||||
|
||||
public function testGetManipulatedDataWithDefaultSort()
|
||||
{
|
||||
$sortedList = $this->getTitleSortedListForManipuatedData(TitleSortedObject::class, [
|
||||
['Title' => 'Z', 'Iden' => 'C', 'DefaultSort' => 3],
|
||||
['Title' => 'Z', 'Iden' => 'A', 'DefaultSort' => 2],
|
||||
['Title' => 'Z', 'Iden' => 'B', 'DefaultSort' => 1],
|
||||
]);
|
||||
$this->assertSame(['B', 'A', 'C'], $sortedList->column('Iden'));
|
||||
}
|
||||
|
||||
|
||||
public function testGetManipulatedDataWithDefaultSortArray()
|
||||
{
|
||||
$sortedList = $this->getTitleSortedListForManipuatedData(TitleArraySortedObject::class, [
|
||||
['Title' => 'X', 'Iden' => 'C', 'OtherSort' => 3],
|
||||
['Title' => 'Z', 'Iden' => 'A', 'OtherSort' => 2],
|
||||
['Title' => 'Z', 'Iden' => 'B', 'OtherSort' => 1],
|
||||
]);
|
||||
$this->assertSame(['C', 'B', 'A'], $sortedList->column('Iden'));
|
||||
}
|
||||
|
||||
private function getTitleSortedListForManipuatedData(string $dataClass, array $data): DataList
|
||||
{
|
||||
$list = new DataList($dataClass);
|
||||
foreach ($data as $values) {
|
||||
$item = new $dataClass();
|
||||
$item->update($values);
|
||||
$item->write();
|
||||
$list->add($item);
|
||||
}
|
||||
$orderable = new GridFieldOrderableRows('Title');
|
||||
$config = new GridFieldConfig_RelationEditor();
|
||||
$config->addComponent($orderable);
|
||||
$grid = new GridField('MyName', 'MyTitle', $list, $config);
|
||||
$sortedList = $orderable->getManipulatedData($grid, $list);
|
||||
return $sortedList;
|
||||
}
|
||||
}
|
||||
|
||||
/**#@+
|
||||
* @ignore
|
||||
*/
|
||||
|
||||
class GridFieldOrderableRowsTest_Parent extends DataObject implements TestOnly {
|
||||
|
||||
private static $has_many = array(
|
||||
'MyHasMany' => 'GridFieldOrderableRowsTest_Ordered',
|
||||
'MyHasManySubclass' => 'GridFieldOrderableRowsTest_Subclass'
|
||||
);
|
||||
|
||||
private static $many_many = array(
|
||||
'MyManyMany' => 'GridFieldOrderableRowsTest_Ordered'
|
||||
);
|
||||
|
||||
private static $many_many_extraFields = array(
|
||||
'MyManyMany' => array('ManyManySort' => 'Int')
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
class GridFieldOrderableRowsTest_Ordered extends DataObject implements TestOnly {
|
||||
|
||||
private static $db = array(
|
||||
'Sort' => 'Int'
|
||||
);
|
||||
|
||||
private static $has_one = array(
|
||||
'Parent' => 'GridFieldOrderableRowsTest_Parent'
|
||||
);
|
||||
|
||||
private static $belongs_many_many =array(
|
||||
'MyManyMany' => 'GridFieldOrderableRowsTest_Parent',
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
class GridFieldOrderableRowsTest_Subclass extends GridFieldOrderableRowsTest_Ordered implements TestOnly {
|
||||
}
|
||||
|
||||
/**#@-*/
|
||||
|
@ -1,22 +1,102 @@
|
||||
GridFieldOrderableRowsTest_Ordered:
|
||||
Symbiote\GridFieldExtensions\Tests\Stub\StubOrderableChild:
|
||||
item1:
|
||||
Sort: 1
|
||||
item2:
|
||||
Sort: 2
|
||||
item3:
|
||||
Sort: 3
|
||||
item4:
|
||||
Sort: 4
|
||||
|
||||
Symbiote\GridFieldExtensions\Tests\Stub\StubOrdered:
|
||||
item1:
|
||||
Sort: 1
|
||||
item2:
|
||||
Sort: 2
|
||||
item3:
|
||||
Sort: 3
|
||||
item4:
|
||||
Sort: 4
|
||||
item5:
|
||||
Sort: 5
|
||||
item6:
|
||||
GridFieldOrderableRowsTest_Parent:
|
||||
Sort: 6
|
||||
nestedtest:
|
||||
Children:
|
||||
- =>Symbiote\GridFieldExtensions\Tests\Stub\StubOrderableChild.item1
|
||||
- =>Symbiote\GridFieldExtensions\Tests\Stub\StubOrderableChild.item2
|
||||
- =>Symbiote\GridFieldExtensions\Tests\Stub\StubOrderableChild.item3
|
||||
- =>Symbiote\GridFieldExtensions\Tests\Stub\StubOrderableChild.item4
|
||||
|
||||
Symbiote\GridFieldExtensions\Tests\Stub\StubSubclass:
|
||||
item1:
|
||||
Sort: 1
|
||||
item2:
|
||||
Sort: 2
|
||||
item3:
|
||||
Sort: 3
|
||||
item4:
|
||||
Sort: 4
|
||||
item5:
|
||||
Sort: 5
|
||||
item6:
|
||||
Sort: 6
|
||||
|
||||
Symbiote\GridFieldExtensions\Tests\Stub\StubSubclassOrderedVersioned:
|
||||
item1:
|
||||
ExtraField: 1
|
||||
Sort: 1
|
||||
item2:
|
||||
ExtraField: 2
|
||||
Sort: 2
|
||||
item3:
|
||||
ExtraField: 3
|
||||
Sort: 3
|
||||
item4:
|
||||
ExtraField: 4
|
||||
Sort: 4
|
||||
|
||||
Symbiote\GridFieldExtensions\Tests\Stub\StubParent:
|
||||
parent:
|
||||
MyManyMany:
|
||||
- 0: =>GridFieldOrderableRowsTest_Ordered.item1
|
||||
- =>Symbiote\GridFieldExtensions\Tests\Stub\StubOrdered.item1:
|
||||
ManyManySort: 1
|
||||
- 1: =>GridFieldOrderableRowsTest_Ordered.item2
|
||||
- =>Symbiote\GridFieldExtensions\Tests\Stub\StubOrdered.item2:
|
||||
ManyManySort: 1
|
||||
- 2: =>GridFieldOrderableRowsTest_Ordered.item3
|
||||
- =>Symbiote\GridFieldExtensions\Tests\Stub\StubOrdered.item3:
|
||||
ManyManySort: 2
|
||||
- 3: =>GridFieldOrderableRowsTest_Ordered.item4
|
||||
- =>Symbiote\GridFieldExtensions\Tests\Stub\StubOrdered.item4:
|
||||
ManyManySort: 2
|
||||
- 4: =>GridFieldOrderableRowsTest_Ordered.item5
|
||||
- =>Symbiote\GridFieldExtensions\Tests\Stub\StubOrdered.item5:
|
||||
ManyManySort: 108
|
||||
- 5: =>GridFieldOrderableRowsTest_Ordered.item6
|
||||
- =>Symbiote\GridFieldExtensions\Tests\Stub\StubOrdered.item6:
|
||||
ManyManySort: 108
|
||||
MyManyManyVersioned:
|
||||
- =>Symbiote\GridFieldExtensions\Tests\Stub\StubSubclassOrderedVersioned.item1:
|
||||
ManyManySort: 1
|
||||
- =>Symbiote\GridFieldExtensions\Tests\Stub\StubSubclassOrderedVersioned.item2:
|
||||
ManyManySort: 1
|
||||
- =>Symbiote\GridFieldExtensions\Tests\Stub\StubSubclassOrderedVersioned.item3:
|
||||
ManyManySort: 108
|
||||
- =>Symbiote\GridFieldExtensions\Tests\Stub\StubSubclassOrderedVersioned.item4:
|
||||
ManyManySort: 108
|
||||
MyHasMany:
|
||||
- =>Symbiote\GridFieldExtensions\Tests\Stub\StubOrdered.item1
|
||||
- =>Symbiote\GridFieldExtensions\Tests\Stub\StubOrdered.item2
|
||||
- =>Symbiote\GridFieldExtensions\Tests\Stub\StubOrdered.item3
|
||||
- =>Symbiote\GridFieldExtensions\Tests\Stub\StubOrdered.item4
|
||||
- =>Symbiote\GridFieldExtensions\Tests\Stub\StubOrdered.item5
|
||||
- =>Symbiote\GridFieldExtensions\Tests\Stub\StubOrdered.item6
|
||||
MyHasManySubclass:
|
||||
- =>Symbiote\GridFieldExtensions\Tests\Stub\StubSubclass.item1
|
||||
- =>Symbiote\GridFieldExtensions\Tests\Stub\StubSubclass.item2
|
||||
- =>Symbiote\GridFieldExtensions\Tests\Stub\StubSubclass.item3
|
||||
- =>Symbiote\GridFieldExtensions\Tests\Stub\StubSubclass.item4
|
||||
- =>Symbiote\GridFieldExtensions\Tests\Stub\StubSubclass.item5
|
||||
- =>Symbiote\GridFieldExtensions\Tests\Stub\StubSubclass.item6
|
||||
parent-subclass-ordered-versioned:
|
||||
MyHasManySubclassOrderedVersioned:
|
||||
- =>Symbiote\GridFieldExtensions\Tests\Stub\StubSubclassOrderedVersioned.item1
|
||||
- =>Symbiote\GridFieldExtensions\Tests\Stub\StubSubclassOrderedVersioned.item2
|
||||
- =>Symbiote\GridFieldExtensions\Tests\Stub\StubSubclassOrderedVersioned.item3
|
||||
- =>Symbiote\GridFieldExtensions\Tests\Stub\StubSubclassOrderedVersioned.item4
|
||||
|
28
tests/OrderablePolymorphicManyToMany.yml
Normal file
28
tests/OrderablePolymorphicManyToMany.yml
Normal file
@ -0,0 +1,28 @@
|
||||
Symbiote\GridFieldExtensions\Tests\Stub\PolymorphM2MParent:
|
||||
ParentOne:
|
||||
ParentTwo:
|
||||
|
||||
Symbiote\GridFieldExtensions\Tests\Stub\PolymorphM2MChild:
|
||||
ChildOne:
|
||||
ChildTwo:
|
||||
|
||||
Symbiote\GridFieldExtensions\Tests\Stub\PolymorphM2MMapper:
|
||||
MapP1ToC1:
|
||||
Parent: '=>Symbiote\GridFieldExtensions\Tests\Stub\PolymorphM2MParent.ParentOne'
|
||||
Child: '=>Symbiote\GridFieldExtensions\Tests\Stub\PolymorphM2MChild.ChildOne'
|
||||
Sort: 1
|
||||
|
||||
MapP1ToC2:
|
||||
Parent: '=>Symbiote\GridFieldExtensions\Tests\Stub\PolymorphM2MParent.ParentOne'
|
||||
Child: '=>Symbiote\GridFieldExtensions\Tests\Stub\PolymorphM2MChild.ChildTwo'
|
||||
Sort: 2
|
||||
|
||||
MapP2ToC1:
|
||||
Parent: '=>Symbiote\GridFieldExtensions\Tests\Stub\PolymorphM2MParent.ParentTwo'
|
||||
Child: '=>Symbiote\GridFieldExtensions\Tests\Stub\PolymorphM2MChild.ChildOne'
|
||||
Sort: 2
|
||||
|
||||
MapP2ToC2:
|
||||
Parent: '=>Symbiote\GridFieldExtensions\Tests\Stub\PolymorphM2MParent.ParentTwo'
|
||||
Child: '=>Symbiote\GridFieldExtensions\Tests\Stub\PolymorphM2MChild.ChildTwo'
|
||||
Sort: 1
|
61
tests/OrderableRowsThroughTest.yml
Normal file
61
tests/OrderableRowsThroughTest.yml
Normal file
@ -0,0 +1,61 @@
|
||||
Symbiote\GridFieldExtensions\Tests\Stub\ThroughDefiner:
|
||||
DefinerOne:
|
||||
DefinerTwo:
|
||||
|
||||
Symbiote\GridFieldExtensions\Tests\Stub\ThroughBelongs:
|
||||
BelongsOne:
|
||||
BelongsTwo:
|
||||
BelongsThree:
|
||||
|
||||
Symbiote\GridFieldExtensions\Tests\Stub\ThroughIntermediary:
|
||||
One:
|
||||
Defining: =>Symbiote\GridFieldExtensions\Tests\Stub\ThroughDefiner.DefinerOne
|
||||
Belonging: =>Symbiote\GridFieldExtensions\Tests\Stub\ThroughBelongs.BelongsOne
|
||||
Sort: 3
|
||||
Two:
|
||||
Defining: =>Symbiote\GridFieldExtensions\Tests\Stub\ThroughDefiner.DefinerOne
|
||||
Belonging: =>Symbiote\GridFieldExtensions\Tests\Stub\ThroughBelongs.BelongsTwo
|
||||
Sort: 2
|
||||
Three:
|
||||
Defining: =>Symbiote\GridFieldExtensions\Tests\Stub\ThroughDefiner.DefinerOne
|
||||
Belonging: =>Symbiote\GridFieldExtensions\Tests\Stub\ThroughBelongs.BelongsThree
|
||||
Sort: 1
|
||||
Four:
|
||||
Defining: =>Symbiote\GridFieldExtensions\Tests\Stub\ThroughDefiner.DefinerTwo
|
||||
Belonging: =>Symbiote\GridFieldExtensions\Tests\Stub\ThroughBelongs.BelongsTwo
|
||||
Sort: 1
|
||||
Five:
|
||||
Defining: =>Symbiote\GridFieldExtensions\Tests\Stub\ThroughDefiner.DefinerTwo
|
||||
Belonging: =>Symbiote\GridFieldExtensions\Tests\Stub\ThroughBelongs.BelongsThree
|
||||
Sort: 2
|
||||
|
||||
Symbiote\GridFieldExtensions\Tests\Stub\ThroughDefinerVersioned:
|
||||
DefinerOne:
|
||||
DefinerTwo:
|
||||
|
||||
Symbiote\GridFieldExtensions\Tests\Stub\ThroughBelongsVersioned:
|
||||
BelongsOne:
|
||||
BelongsTwo:
|
||||
BelongsThree:
|
||||
|
||||
Symbiote\GridFieldExtensions\Tests\Stub\ThroughIntermediaryVersioned:
|
||||
One:
|
||||
Defining: =>Symbiote\GridFieldExtensions\Tests\Stub\ThroughDefinerVersioned.DefinerOne
|
||||
Belonging: =>Symbiote\GridFieldExtensions\Tests\Stub\ThroughBelongsVersioned.BelongsOne
|
||||
Sort: 3
|
||||
Two:
|
||||
Defining: =>Symbiote\GridFieldExtensions\Tests\Stub\ThroughDefinerVersioned.DefinerOne
|
||||
Belonging: =>Symbiote\GridFieldExtensions\Tests\Stub\ThroughBelongsVersioned.BelongsTwo
|
||||
Sort: 2
|
||||
Three:
|
||||
Defining: =>Symbiote\GridFieldExtensions\Tests\Stub\ThroughDefinerVersioned.DefinerOne
|
||||
Belonging: =>Symbiote\GridFieldExtensions\Tests\Stub\ThroughBelongsVersioned.BelongsThree
|
||||
Sort: 1
|
||||
Four:
|
||||
Defining: =>Symbiote\GridFieldExtensions\Tests\Stub\ThroughDefinerVersioned.DefinerTwo
|
||||
Belonging: =>Symbiote\GridFieldExtensions\Tests\Stub\ThroughBelongsVersioned.BelongsTwo
|
||||
Sort: 1
|
||||
Five:
|
||||
Defining: =>Symbiote\GridFieldExtensions\Tests\Stub\ThroughDefinerVersioned.DefinerTwo
|
||||
Belonging: =>Symbiote\GridFieldExtensions\Tests\Stub\ThroughBelongsVersioned.BelongsThree
|
||||
Sort: 2
|
126
tests/OrderableRowsThroughVersionedTest.php
Normal file
126
tests/OrderableRowsThroughVersionedTest.php
Normal file
@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
namespace Symbiote\GridFieldExtensions\Tests;
|
||||
|
||||
use ReflectionMethod;
|
||||
use SilverStripe\Dev\SapphireTest;
|
||||
use SilverStripe\Versioned\Versioned;
|
||||
use SilverStripe\Forms\GridField\GridField;
|
||||
use SilverStripe\Forms\GridField\GridFieldConfig_RelationEditor;
|
||||
use Symbiote\GridFieldExtensions\Tests\Stub\ThroughDefiner;
|
||||
use Symbiote\GridFieldExtensions\Tests\Stub\ThroughIntermediary;
|
||||
use Symbiote\GridFieldExtensions\Tests\Stub\ThroughBelongs;
|
||||
use Symbiote\GridFieldExtensions\GridFieldOrderableRows;
|
||||
|
||||
class OrderableRowsThroughVersionedTest extends SapphireTest
|
||||
{
|
||||
protected static $fixture_file = 'OrderableRowsThroughTest.yml';
|
||||
|
||||
protected static $extra_dataobjects = [
|
||||
ThroughDefiner::class,
|
||||
ThroughIntermediary::class,
|
||||
ThroughBelongs::class,
|
||||
];
|
||||
|
||||
protected static $required_extensions = [
|
||||
ThroughDefiner::class => [Versioned::class],
|
||||
ThroughIntermediary::class => [Versioned::class],
|
||||
ThroughBelongs::class => [Versioned::class],
|
||||
];
|
||||
|
||||
protected $originalReadingMode;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->orignalReadingMode = Versioned::get_reading_mode();
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
Versioned::set_reading_mode($this->originalReadingMode);
|
||||
unset($this->originalReadingMode);
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
/**
|
||||
* Basically the same as GridFieldOrderableRowsTest::testReorderItems
|
||||
* but with some Versioned calls & checks mixed in.
|
||||
*/
|
||||
public function testReorderingSavesAndPublishes()
|
||||
{
|
||||
$parent = $this->objFromFixture(ThroughDefiner::class, 'DefinerOne');
|
||||
$relationName = 'Belongings';
|
||||
$sortName = 'Sort';
|
||||
|
||||
$orderable = new GridFieldOrderableRows($sortName);
|
||||
$reflection = new ReflectionMethod($orderable, 'executeReorder');
|
||||
$reflection->setAccessible(true);
|
||||
|
||||
$config = new GridFieldConfig_RelationEditor();
|
||||
$config->addComponent($orderable);
|
||||
|
||||
// This test data is versioned - ensure we're all published
|
||||
$parent->publishRecursive();
|
||||
// there should be no difference between stages at this point
|
||||
foreach ($parent->$relationName() as $item) {
|
||||
$this->assertFalse(
|
||||
$item->getJoin()->stagesDiffer(),
|
||||
'No records should be different from their published versions'
|
||||
);
|
||||
}
|
||||
|
||||
$grid = new GridField(
|
||||
'Belongings',
|
||||
'Testing Many Many',
|
||||
$parent->$relationName()->sort($sortName),
|
||||
$config
|
||||
);
|
||||
|
||||
$originalOrder = $parent->$relationName()->sort($sortName)->column('ID');
|
||||
// Ring (un)shift by one, e.g. 3,2,1 becomes 1,3,2.
|
||||
// then string key our new order starting at 1
|
||||
$desiredOrder = array_values($originalOrder ?? []);
|
||||
array_unshift($desiredOrder, array_pop($desiredOrder));
|
||||
$desiredOrder = array_combine(
|
||||
range('1', count($desiredOrder ?? [])),
|
||||
$desiredOrder ?? []
|
||||
);
|
||||
$this->assertNotEquals($originalOrder, $desiredOrder);
|
||||
|
||||
// Perform the reorder
|
||||
$reflection->invoke($orderable, $grid, $desiredOrder);
|
||||
|
||||
// Verify draft stage has reordered
|
||||
Versioned::set_stage(Versioned::DRAFT);
|
||||
$newOrder = $parent->$relationName()->sort($sortName)->map($sortName, 'ID')->toArray();
|
||||
$this->assertEquals($desiredOrder, $newOrder);
|
||||
|
||||
// reorder should have been handled as versioned - there should be a difference between stages now
|
||||
// by using a ring style shift every item should have a new sort (thus a new version).
|
||||
$differenceFound = false;
|
||||
foreach ($parent->$relationName() as $item) {
|
||||
if ($item->getJoin()->stagesDiffer()) {
|
||||
$differenceFound = true;
|
||||
}
|
||||
}
|
||||
$this->assertTrue($differenceFound, 'All records should have changes in draft');
|
||||
|
||||
// Verify live stage has NOT reordered
|
||||
Versioned::set_stage(Versioned::LIVE);
|
||||
$sameOrder = $parent->$relationName()->sort($sortName)->column('ID');
|
||||
$this->assertEquals($originalOrder, $sameOrder);
|
||||
|
||||
$parent->publishRecursive();
|
||||
|
||||
foreach ($parent->$relationName() as $item) {
|
||||
$this->assertFalse(
|
||||
$item->getJoin()->stagesDiffer(),
|
||||
'No records should be different from their published versions anymore'
|
||||
);
|
||||
}
|
||||
|
||||
$newLiveOrder = $parent->$relationName()->sort($sortName)->map($sortName, 'ID')->toArray();
|
||||
$this->assertEquals($desiredOrder, $newLiveOrder);
|
||||
}
|
||||
}
|
18
tests/Stub/NamespacedClass.php
Normal file
18
tests/Stub/NamespacedClass.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace Symbiote\GridFieldExtensions\Tests\Stub;
|
||||
|
||||
use Silverstripe\Dev\TestOnly;
|
||||
|
||||
class NamespacedClass implements TestOnly
|
||||
{
|
||||
public function i18n_singular_name()
|
||||
{
|
||||
return 'NamespacedClass';
|
||||
}
|
||||
|
||||
public function canCreate()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
15
tests/Stub/PolymorphM2MChild.php
Normal file
15
tests/Stub/PolymorphM2MChild.php
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace Symbiote\GridFieldExtensions\Tests\Stub;
|
||||
|
||||
use SilverStripe\Dev\TestOnly;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
class PolymorphM2MChild extends DataObject implements TestOnly
|
||||
{
|
||||
private static $table_name = 'TestOnly_PolymorphM2MChild';
|
||||
|
||||
private static $has_many = [
|
||||
'Parents' => PolymorphM2MMapper::class
|
||||
];
|
||||
}
|
22
tests/Stub/PolymorphM2MMapper.php
Normal file
22
tests/Stub/PolymorphM2MMapper.php
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace Symbiote\GridFieldExtensions\Tests\Stub;
|
||||
|
||||
use SilverStripe\Dev\TestOnly;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
class PolymorphM2MMapper extends DataObject implements TestOnly
|
||||
{
|
||||
private static $table_name = 'TestOnly_PolymorphM2MMapper';
|
||||
|
||||
private static $db = [
|
||||
'Sort' => 'Int'
|
||||
];
|
||||
|
||||
private static $has_one = [
|
||||
'Parent' => DataObject::class, // PolymorphM2MParent
|
||||
'Child' => PolymorphM2MChild::class,
|
||||
];
|
||||
|
||||
private static $default_sort = '"Sort" ASC';
|
||||
}
|
19
tests/Stub/PolymorphM2MParent.php
Normal file
19
tests/Stub/PolymorphM2MParent.php
Normal file
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace Symbiote\GridFieldExtensions\Tests\Stub;
|
||||
|
||||
use SilverStripe\Dev\TestOnly;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
class PolymorphM2MParent extends DataObject implements TestOnly
|
||||
{
|
||||
private static $table_name = 'TableOnly_PolymorphM2MParent';
|
||||
|
||||
private static $many_many = [
|
||||
'Children' => [
|
||||
'through' => PolymorphM2MMapper::class,
|
||||
'from' => 'Parent',
|
||||
'to' => 'Child',
|
||||
]
|
||||
];
|
||||
}
|
19
tests/Stub/StubA.php
Normal file
19
tests/Stub/StubA.php
Normal file
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace Symbiote\GridFieldExtensions\Tests\Stub;
|
||||
|
||||
use SilverStripe\Dev\TestOnly;
|
||||
|
||||
class StubA implements TestOnly
|
||||
{
|
||||
public function i18n_singular_name()
|
||||
{
|
||||
$class = get_class($this);
|
||||
return substr($class ?? '', -1);
|
||||
}
|
||||
|
||||
public function canCreate()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
9
tests/Stub/StubB.php
Normal file
9
tests/Stub/StubB.php
Normal file
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Symbiote\GridFieldExtensions\Tests\Stub;
|
||||
|
||||
use SilverStripe\Dev\TestOnly;
|
||||
|
||||
class StubB extends StubA implements TestOnly
|
||||
{
|
||||
}
|
9
tests/Stub/StubC.php
Normal file
9
tests/Stub/StubC.php
Normal file
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Symbiote\GridFieldExtensions\Tests\Stub;
|
||||
|
||||
use SilverStripe\Dev\TestOnly;
|
||||
|
||||
class StubC extends StubA implements TestOnly
|
||||
{
|
||||
}
|
18
tests/Stub/StubOrderableChild.php
Normal file
18
tests/Stub/StubOrderableChild.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace Symbiote\GridFieldExtensions\Tests\Stub;
|
||||
|
||||
class StubOrderableChild extends StubUnorderable
|
||||
{
|
||||
private static $db = [
|
||||
'Sort' => 'Int',
|
||||
];
|
||||
|
||||
private static $has_one = [
|
||||
'Parent' => StubOrdered::class,
|
||||
];
|
||||
|
||||
private static $default_sort = '"Sort" ASC';
|
||||
|
||||
private static $table_name = 'StubOrderableChild';
|
||||
}
|
27
tests/Stub/StubOrdered.php
Normal file
27
tests/Stub/StubOrdered.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Symbiote\GridFieldExtensions\Tests\Stub;
|
||||
|
||||
use SilverStripe\Dev\TestOnly;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
class StubOrdered extends DataObject implements TestOnly
|
||||
{
|
||||
private static $db = array(
|
||||
'Sort' => 'Int'
|
||||
);
|
||||
|
||||
private static $has_one = array(
|
||||
'Parent' => StubParent::class
|
||||
);
|
||||
|
||||
private static $has_many = array(
|
||||
'Children' => StubOrderableChild::class,
|
||||
);
|
||||
|
||||
private static $belongs_many_many =array(
|
||||
'MyManyMany' => StubParent::class,
|
||||
);
|
||||
|
||||
private static $table_name = 'StubOrdered';
|
||||
}
|
33
tests/Stub/StubOrderedVersioned.php
Normal file
33
tests/Stub/StubOrderedVersioned.php
Normal file
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace Symbiote\GridFieldExtensions\Tests\Stub;
|
||||
|
||||
use SilverStripe\Dev\TestOnly;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
use SilverStripe\Versioned\Versioned;
|
||||
|
||||
/**
|
||||
* Class StubOrderedVersioned
|
||||
* @package Symbiote\GridFieldExtensions\Tests\Stub
|
||||
*/
|
||||
class StubOrderedVersioned extends DataObject implements TestOnly
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private static $table_name = 'StubOrderedVersioned';
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private static $extensions = [
|
||||
Versioned::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private static $db = [
|
||||
'Sort' => 'Int',
|
||||
];
|
||||
}
|
27
tests/Stub/StubParent.php
Normal file
27
tests/Stub/StubParent.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Symbiote\GridFieldExtensions\Tests\Stub;
|
||||
|
||||
use SilverStripe\Dev\TestOnly;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
class StubParent extends DataObject implements TestOnly
|
||||
{
|
||||
private static $has_many = [
|
||||
'MyHasMany' => StubOrdered::class,
|
||||
'MyHasManySubclass' => StubSubclass::class,
|
||||
'MyHasManySubclassOrderedVersioned' => StubSubclassOrderedVersioned::class,
|
||||
];
|
||||
|
||||
private static $many_many = [
|
||||
'MyManyMany' => StubOrdered::class,
|
||||
'MyManyManyVersioned' => StubSubclassOrderedVersioned::class,
|
||||
];
|
||||
|
||||
private static $many_many_extraFields = [
|
||||
'MyManyMany' => ['ManyManySort' => 'Int'],
|
||||
'MyManyManyVersioned' => ['ManyManySort' => 'Int'],
|
||||
];
|
||||
|
||||
private static $table_name = 'StubParent';
|
||||
}
|
11
tests/Stub/StubSubclass.php
Normal file
11
tests/Stub/StubSubclass.php
Normal file
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace Symbiote\GridFieldExtensions\Tests\Stub;
|
||||
|
||||
use SilverStripe\Dev\TestOnly;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
class StubSubclass extends StubOrdered implements TestOnly
|
||||
{
|
||||
private static $table_name = 'StubSubclass';
|
||||
}
|
33
tests/Stub/StubSubclassOrderedVersioned.php
Normal file
33
tests/Stub/StubSubclassOrderedVersioned.php
Normal file
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace Symbiote\GridFieldExtensions\Tests\Stub;
|
||||
|
||||
use SilverStripe\Dev\TestOnly;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
use SilverStripe\Versioned\Versioned;
|
||||
|
||||
/**
|
||||
* Class StubOrderedVersioned
|
||||
* @package Symbiote\GridFieldExtensions\Tests\Stub
|
||||
*/
|
||||
class StubSubclassOrderedVersioned extends StubOrderedVersioned
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private static $table_name = 'StubSubclassOrderedVersioned';
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private static $db = [
|
||||
'ExtraField' => 'Int',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private static $has_one = [
|
||||
'Parent' => StubParent::class,
|
||||
];
|
||||
}
|
27
tests/Stub/StubUnorderable.php
Normal file
27
tests/Stub/StubUnorderable.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Symbiote\GridFieldExtensions\Tests\Stub;
|
||||
|
||||
use SilverStripe\Dev\TestOnly;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
class StubUnorderable extends DataObject implements TestOnly
|
||||
{
|
||||
private static $db = [
|
||||
'Title' => 'Varchar',
|
||||
];
|
||||
|
||||
private static $table_name = 'StubUnorderable';
|
||||
|
||||
private $canEdit = false;
|
||||
|
||||
public function setCanEdit($canEdit)
|
||||
{
|
||||
$this->canEdit = $canEdit;
|
||||
}
|
||||
|
||||
public function canEdit($member = null)
|
||||
{
|
||||
return $this->canEdit;
|
||||
}
|
||||
}
|
10
tests/Stub/TestController.php
Normal file
10
tests/Stub/TestController.php
Normal file
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace Symbiote\GridFieldExtensions\Tests\Stub;
|
||||
|
||||
use SilverStripe\Control\Controller;
|
||||
|
||||
class TestController extends Controller
|
||||
{
|
||||
private static $url_segment = 'test';
|
||||
}
|
15
tests/Stub/ThroughBelongs.php
Normal file
15
tests/Stub/ThroughBelongs.php
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace Symbiote\GridFieldExtensions\Tests\Stub;
|
||||
|
||||
use SilverStripe\ORM\DataObject;
|
||||
use SilverStripe\Dev\TestOnly;
|
||||
|
||||
class ThroughBelongs extends DataObject implements TestOnly
|
||||
{
|
||||
private static $table_name = 'BelongsThrough';
|
||||
|
||||
private static $belongs_many_many = [
|
||||
'Definers' => ThroughDefiner::class,
|
||||
];
|
||||
}
|
25
tests/Stub/ThroughBelongsVersioned.php
Normal file
25
tests/Stub/ThroughBelongsVersioned.php
Normal file
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace Symbiote\GridFieldExtensions\Tests\Stub;
|
||||
|
||||
use SilverStripe\Dev\TestOnly;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
use SilverStripe\ORM\ManyManyList;
|
||||
use SilverStripe\Versioned\Versioned;
|
||||
|
||||
/**
|
||||
* @method ManyManyList|ThroughDefinerVersioned[] Definers()
|
||||
* @mixin Versioned
|
||||
*/
|
||||
class ThroughBelongsVersioned extends DataObject implements TestOnly
|
||||
{
|
||||
private static string $table_name = 'ThroughBelongsVersioned';
|
||||
|
||||
private static array $belongs_many_many = [
|
||||
'Definers' => ThroughDefinerVersioned::class,
|
||||
];
|
||||
|
||||
private static array $extensions = [
|
||||
Versioned::class,
|
||||
];
|
||||
}
|
23
tests/Stub/ThroughDefiner.php
Normal file
23
tests/Stub/ThroughDefiner.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace Symbiote\GridFieldExtensions\Tests\Stub;
|
||||
|
||||
use SilverStripe\ORM\DataObject;
|
||||
use SilverStripe\Dev\TestOnly;
|
||||
|
||||
class ThroughDefiner extends DataObject implements TestOnly
|
||||
{
|
||||
private static $table_name = 'ManyThrough';
|
||||
|
||||
private static $many_many = [
|
||||
'Belongings' => [
|
||||
'through' => ThroughIntermediary::class,
|
||||
'from' => 'Defining',
|
||||
'to' => 'Belonging',
|
||||
]
|
||||
];
|
||||
|
||||
private static $owns = [
|
||||
'Belongings'
|
||||
];
|
||||
}
|
33
tests/Stub/ThroughDefinerVersioned.php
Normal file
33
tests/Stub/ThroughDefinerVersioned.php
Normal file
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace Symbiote\GridFieldExtensions\Tests\Stub;
|
||||
|
||||
use SilverStripe\Dev\TestOnly;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
use SilverStripe\ORM\ManyManyThroughList;
|
||||
use SilverStripe\Versioned\Versioned;
|
||||
|
||||
/**
|
||||
* @method ManyManyThroughList|ThroughIntermediaryVersioned Belongings()
|
||||
* @mixin Versioned
|
||||
*/
|
||||
class ThroughDefinerVersioned extends DataObject implements TestOnly
|
||||
{
|
||||
private static string $table_name = 'ThroughDefinerVersioned';
|
||||
|
||||
private static array $many_many = [
|
||||
'Belongings' => [
|
||||
'through' => ThroughIntermediaryVersioned::class,
|
||||
'from' => 'Defining',
|
||||
'to' => 'Belonging',
|
||||
]
|
||||
];
|
||||
|
||||
private static array $owns = [
|
||||
'Belongings'
|
||||
];
|
||||
|
||||
private static array $extensions = [
|
||||
Versioned::class,
|
||||
];
|
||||
}
|
20
tests/Stub/ThroughIntermediary.php
Normal file
20
tests/Stub/ThroughIntermediary.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace Symbiote\GridFieldExtensions\Tests\Stub;
|
||||
|
||||
use SilverStripe\Dev\TestOnly;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
class ThroughIntermediary extends DataObject implements TestOnly
|
||||
{
|
||||
private static $table_name = 'IntermediaryThrough';
|
||||
|
||||
private static $db = [
|
||||
'Sort' => 'Int',
|
||||
];
|
||||
|
||||
private static $has_one = [
|
||||
'Defining' => ThroughDefiner::class,
|
||||
'Belonging' => ThroughBelongs::class,
|
||||
];
|
||||
}
|
32
tests/Stub/ThroughIntermediaryVersioned.php
Normal file
32
tests/Stub/ThroughIntermediaryVersioned.php
Normal file
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace Symbiote\GridFieldExtensions\Tests\Stub;
|
||||
|
||||
use SilverStripe\Dev\TestOnly;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
use SilverStripe\Versioned\Versioned;
|
||||
|
||||
/**
|
||||
* @property int $DefiningID
|
||||
* @property int $BelongingID
|
||||
* @method ThroughDefinerVersioned Defining()
|
||||
* @method ThroughBelongsVersioned Belonging()
|
||||
* @mixin Versioned
|
||||
*/
|
||||
class ThroughIntermediaryVersioned extends DataObject implements TestOnly
|
||||
{
|
||||
private static string $table_name = 'ThroughIntermediaryVersioned';
|
||||
|
||||
private static array $db = [
|
||||
'Sort' => 'Int',
|
||||
];
|
||||
|
||||
private static array $has_one = [
|
||||
'Defining' => ThroughDefinerVersioned::class,
|
||||
'Belonging' => ThroughBelongsVersioned::class,
|
||||
];
|
||||
|
||||
private static array $extensions = [
|
||||
Versioned::class,
|
||||
];
|
||||
}
|
22
tests/Stub/TitleArraySortedObject.php
Normal file
22
tests/Stub/TitleArraySortedObject.php
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace Symbiote\GridFieldExtensions\Tests\Stub;
|
||||
|
||||
use SilverStripe\Dev\TestOnly;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
class TitleArraySortedObject extends DataObject implements TestOnly
|
||||
{
|
||||
private static $db = [
|
||||
'Title' => 'Varchar',
|
||||
'Iden' => 'Varchar',
|
||||
'OtherSort' => 'Int'
|
||||
];
|
||||
|
||||
private static array $default_sort = [
|
||||
'Title' => 'ASC',
|
||||
'OtherSort' => 'ASC',
|
||||
];
|
||||
|
||||
private static $table_name = 'TitleArraySortedObject';
|
||||
}
|
15
tests/Stub/TitleObject.php
Normal file
15
tests/Stub/TitleObject.php
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace Symbiote\GridFieldExtensions\Tests\Stub;
|
||||
|
||||
use SilverStripe\Dev\TestOnly;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
class TitleObject extends DataObject implements TestOnly
|
||||
{
|
||||
private static $db = [
|
||||
'Title' => 'Varchar',
|
||||
];
|
||||
|
||||
private static $table_name = 'TitleObject';
|
||||
}
|
19
tests/Stub/TitleSortedObject.php
Normal file
19
tests/Stub/TitleSortedObject.php
Normal file
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace Symbiote\GridFieldExtensions\Tests\Stub;
|
||||
|
||||
use SilverStripe\Dev\TestOnly;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
class TitleSortedObject extends DataObject implements TestOnly
|
||||
{
|
||||
private static $db = [
|
||||
'Title' => 'Varchar',
|
||||
'Iden' => 'Varchar',
|
||||
'DefaultSort' => 'Int'
|
||||
];
|
||||
|
||||
private static $default_sort = '"DefaultSort" ASC';
|
||||
|
||||
private static $table_name = 'TitleSortedObject';
|
||||
}
|
Loading…
Reference in New Issue
Block a user