Compare commits

...

259 Commits
2.0.0 ... 3

Author SHA1 Message Date
Guy Sartorelli d851ce562c
Merge pull request #395 from creative-commoners/pulls/3/manual-workflows
MNT Upaded scheduled workflows on default branch
2024-05-23 11:56:27 +12:00
Steve Boyd 018ab87515 MNT Upaded scheduled workflows on default branch 2024-05-22 16:36:45 +12:00
Guy Sartorelli d34c92dba7
Merge branch '3.6' into 3 2024-02-27 13:44:19 +13:00
Guy Sartorelli c84d8afa7a
TLN Update translations (#381) 2024-02-07 16:15:40 +13:00
Tom Oude Rengerink 78fa8ba747
ENH Make toast message "Records reordered." localisable (#321)
Co-authored-by: Tom Oude Rengerink <tom@cube.nl>
2024-01-24 11:41:14 +13:00
Guy Sartorelli 62734535a0
MNT Run module-standardiser (#377) 2023-12-21 16:33:13 +13:00
Guy Sartorelli 5ed8c95fda
Merge branch '3.6' into 3 2023-12-19 13:58:41 +13:00
Guy Sartorelli 377dc1ef93
Merge pull request #358 from Sitetools/link_color_fix
Make link color white.
2023-12-19 13:57:29 +13:00
Robin 1637e78ea1
Make link color white. 2023-12-19 13:54:08 +13:00
Guy Sartorelli 9c70b0175b
Merge branch '3.6' into 3 2023-08-30 10:18:58 +12:00
Loz Calver e7d7178719
Merge pull request #372 from creative-commoners/pulls/3.6/manymanythrough-editablecolumns
FIX Allow editing extra fields from ManyManyThroughList.
2023-08-21 09:06:49 +01:00
Guy Sartorelli 1e5a1e8056
ENH Update translations (#373) 2023-08-21 13:21:15 +12:00
Guy Sartorelli 47171ee4c2
FIX Allow editing extra fields from ManyManyThroughList. 2023-08-21 09:02:42 +12:00
Sabina Talipova a2ea473369
MNT Replaced SearchContext::getQuery limit param with null (#367) 2023-07-05 17:20:56 +12:00
Steve Boyd 829cacff0b Merge branch '3.6' into 3 2023-06-16 12:22:25 +12:00
Guy Sartorelli 1284046e10
Merge pull request #365 from creative-commoners/pulls/3.6/tx-1686725015
ENH Update translations
2023-06-15 10:06:20 +12:00
Steve Boyd 665d23170b ENH Update translations 2023-06-14 18:43:35 +12:00
Steve Boyd f2adec35d3 Merge branch '3.6' into 3 2023-05-31 14:31:33 +12:00
Sabina Talipova 504b3bd822
Merge pull request #364 from creative-commoners/pulls/3.6/tx-1685417567
ENH Update translations
2023-05-31 11:29:33 +12:00
Steve Boyd 89a7301b67 ENH Update translations 2023-05-30 15:32:47 +12:00
Guy Sartorelli 6d46483e32
Merge branch '3.6' into 3 2023-04-06 10:41:11 +12:00
Guy Sartorelli bb6c0396f1
Merge branch '3.5' into 3.6 2023-04-06 10:40:35 +12:00
Guy Sartorelli bd783a4a24
Merge pull request #361 from creative-commoners/pulls/3.5/arrays
FIX Handle arrays in default_sort
2023-04-06 10:39:18 +12:00
Steve Boyd 17f92d08ba FIX Handle arrays in default_sort 2023-04-06 10:30:12 +12:00
Maxime Rainville 49a607d747
Merge pull request #360 from creative-commoners/pulls/3/dispatch-ci
MNT Use gha-dispatch-ci
2023-03-23 12:04:25 +13:00
Steve Boyd 5566b05c9d MNT Use gha-dispatch-ci 2023-03-21 12:05:45 +13:00
Guy Sartorelli b0addcb5bd
ENH Update translations (#359) 2023-03-09 14:53:24 +13:00
Maxime Rainville d5d438c5cc
Merge pull request #334 from josephlewisnz/feature/republish-live-records-3
Feature/republish live records 3
2022-11-18 15:38:05 +13:00
Sabina Talipova e9f202f003
Merge pull request #351 from creative-commoners/pulls/3/stop-depr
API Stop using deprecated API
2022-11-11 12:06:57 +13:00
Steve Boyd aa6ac7a1a2 API Stop using deprecated API 2022-11-03 11:45:10 +13:00
Chris Penny 23a5154c97
ENH: Update reorderItems() to use ORM where possible (#336)
* ENH: Update reorderItems() to use ORM where possible

* Increase test coverage of orderable rows
2022-08-04 10:51:40 +12:00
Steve Boyd 7a8f244df0 Merge branch '3.4' into 3 2022-08-03 14:26:22 +12:00
Steve Boyd 378b3af799 Merge branch '3.3' into 3.4 2022-08-03 14:26:18 +12:00
Guy Sartorelli 77e811459e
Merge pull request #346 from creative-commoners/pulls/3.3/standardise-modules
MNT Standardise modules
2022-08-03 14:19:05 +12:00
Steve Boyd aec306bae2 MNT Standardise modules 2022-08-03 13:54:28 +12:00
Guy Sartorelli 4d99f83bbf
Merge pull request #347 from creative-commoners/pulls/3.4/remove-travis
MNT Remove travis
2022-08-03 12:30:03 +12:00
Steve Boyd 2cfe6db9a7 MNT Remove travis 2022-08-03 12:12:02 +12:00
bumbus f56bf67e40
FIX Fixes GridFieldEditableColumns::isChanged method for non-string values (e.g. arrays for has_many / many_many relations) (#343)
* fixes GridFieldEditableColumns::isChanged method for non-string values (e.g. arrays for has_many / many_many relations)

* revert formatting

Co-authored-by: Bumbus <sf@arillo.ch>
2022-08-01 13:22:09 +12:00
Guy Sartorelli acacac2563
Merge pull request #329 from creative-commoners/pulls/3/default-sort-unit-test
MNT Add unit tests for default sort
2022-08-01 11:25:29 +12:00
Steve Boyd dc99a8e04d Merge branch '3.4' into 3 2022-07-25 11:51:34 +12:00
Steve Boyd 7f90426987 Merge branch '3.3' into 3.4 2022-07-25 11:51:30 +12:00
Guy Sartorelli c6dfea0598
Merge pull request #345 from creative-commoners/pulls/3.3/module-standards
MNT Use GitHub Actions CI
2022-07-15 13:59:06 +12:00
Steve Boyd cfc764e3b5 MNT Use GitHub Actions CI 2022-07-15 13:57:33 +12:00
Steve Boyd 1574be36db Merge branch '3.4' into 3 2022-06-16 13:37:02 +12:00
Steve Boyd 0405d919fc
Merge pull request #341 from silverstripeltd/pull/performance-fixes
ENH: Add performance fixes on saving editable columns
2022-06-16 13:35:24 +12:00
Mo Alsharaf 547ec8aa50 ENH: Add performance fixes on saving editable columns
- Use one query to fetch all items needed to be saved.
- Only save items that are changed.
2022-06-16 13:02:16 +12:00
Steve Boyd 71d60b0734
Merge pull request #338 from creative-commoners/pulls/3/php81
ENH PHP 8.1 compatibility
2022-04-22 16:48:56 +12:00
Steve Boyd 93e4379c3d ENH PHP 8.1 compatibility 2022-04-13 17:44:44 +12:00
Guy Sartorelli ec93c994f8
NEW Extend new AbstractGridFieldComponent class (#332)
This makes this module's `GridFieldComponent` classes `Injectable`, and allows any future enhancements in the new abstract class to automatically apply without requiring additional changes in this module.

The class is introduced in silverstripe/framework 4.11.0 so the dependency constraint needs to be updated.

Also update docs to encourage use of dependency injection.
2022-03-04 10:12:24 +13:00
Guy Sartorelli 8c4e924bfa
ENH: Prefer dependency injection over use of `new` keyword. (#333)
* ENH: Prefer dependency injection over use of `new` keyword.

* MNT Fix phpcs linting error.
2022-02-18 16:44:54 +13:00
josephlewisnz 2928504b3c removed param 2022-02-18 08:39:59 +13:00
josephlewisnz 8e77095de0 updated to create syntax 2022-02-17 13:10:44 +13:00
josephlewisnz 07c97e45b2 Added the ability to autopublish items that are already live 2022-02-17 12:53:13 +13:00
Daniel Hensby 8e9ee0bace
Merge pull request #330 from creative-commoners/pulls/3/php74
DEP Set PHP 7.4 as the minimum version
2022-02-10 11:36:30 +00:00
Steve Boyd 78da0c31fe DEP Set PHP 7.4 as the minimum version 2022-02-10 17:59:26 +13:00
Steve Boyd 607a9bcdf9 MNT Add unit tests for default sort 2022-02-02 15:36:25 +13:00
David Toews 6d6cf4a225
ENH applies default sort order to CMS ordering (#325)
* (cms interface) applies default sort order to CMS ordering

This allows secondary sort ordering to be applied when it exists and when the objects have not been sorted manually

* (sort logic) adds additional checks for non DataList or unsorted cases

* Update src/GridFieldOrderableRows.php

Co-authored-by: Michal Kleiner <mk@011.nz>

Co-authored-by: Michal Kleiner <mk@011.nz>
2022-02-02 15:29:57 +13:00
Maxime Rainville baa6a8d147
Merge pull request #327 from creative-commoners/pulls/3/sapphire-test-nine
API phpunit 9 support
2021-11-09 13:14:44 +13:00
Steve Boyd 5e3037bd2f API phpunit 9 support 2021-11-09 11:43:41 +13:00
Steve Boyd b52fd75f0e
Update README.md 2021-06-18 14:03:25 +12:00
Marcus 7738a0b89b
Merge pull request #319 from ntd/inline-row-with-defaults
FIX: use default values in new inline rows (#64)
2021-05-13 10:11:36 +10:00
Marcus 719c0ed547
Merge pull request #302 from lpostiglione/patch-1
Fix support for dot notation in GridFieldOrderableRows
2021-05-13 10:09:52 +10:00
Nicola Fontana 3d4986432c FIX: use default values in new inline rows (#64) 2021-03-24 20:32:02 +01:00
Steve Boyd 74126ed6a7 Merge branch '3.2' into 3 2021-03-20 14:14:24 +13:00
Steve Boyd 6ee8d6ccb2
Merge pull request #316 from chromos33/patch-1
Update index.md
2021-03-19 12:30:45 +13:00
Serge Latyntsev acd1a37122
Merge pull request #318 from creative-commoners/pulls/3.2/travis-shared
MNT Travis shared config
2021-02-11 16:57:59 +13:00
Steve Boyd 700e5adf53 MNT Travis shared config 2021-01-20 14:52:20 +13:00
chromos33 d6357ec187
Update index.md
Updated doc to use "::class" instead of Literals to reflect SS4 changes
2020-12-23 11:26:38 +01:00
Robbie Averill 3a9fd3c928
Merge pull request #314 from sminnee/pulls/312-fix-editable-readonly
FIX: Fixed handling of uneditable records in GridFieldEditableColumns
2020-09-10 14:08:29 -07:00
Sam Minnee 6becfc2f89 NEW: Add test for GridFieldEditableColumns
Also adds a .gitignore to ignore files created by in-module testing.
2020-09-08 14:57:59 +12:00
Sam Minnee 229a23a2f4 FIX: Transfrom editable columns to readonly on a readonly gridfield also. 2020-09-08 14:57:59 +12:00
Sam Minnee 08f89ea4b5 FIX: Fixed handling of uneditable records in GridFieldEditableColumns
Fixes #312
2020-08-07 12:30:21 +12:00
Garion Herman c77d9d1de3
Merge pull request #313 from creative-commoners/pulls/3.2/column-content-fallback
FIX Reinstate previous field fetch logic as fallback
2020-07-30 16:46:06 +12:00
Garion Herman 4105d6330f FIX Reinstate previous field fetch logic as fallback
Fixes some cases where a field is not retrievable via dataFieldByName()
2020-07-30 10:54:53 +12:00
Marcus d671788b19
Merge pull request #301 from oilee80/3
Add Multi Class to inline add button
2020-06-17 16:44:15 +10:00
Marcus 2fc085bbd0
Merge pull request #294 from sminnee/allow-inline-dot-syntax
FIX: Let GridFieldEditableColumns edit relations via dot syntax
2020-06-17 16:42:26 +10:00
Nathan 306ad52a28
Merge pull request #310 from symbiote/nglasl-patch-1
PHP 7.4
2020-06-10 11:38:10 +10:00
Nathan 3aaf4479ce PHP 7.4 2020-06-10 11:31:21 +10:00
Steve Boyd 5024b93f1d
Merge pull request #306 from sunnysideup/patch-1
MINOR: better composer keywords
2020-06-02 13:25:05 +12:00
Nicolaas f8439f1b67
MINOR: better composer keywords 2020-05-29 12:47:16 +12:00
Serge Latyntsev 66d5e043ed
Merge pull request #296 from chrometoasters/manymanythrough_ploymorphic_support
FIX to make GridFieldOrderableRows support polymorphic ManyManyThroughList
2020-05-18 15:38:49 +12:00
Michal Kleiner 9944b67632 FIX GridFieldOrderableRows to support polymorphic ManyManyThroughList 2020-05-18 15:33:43 +12:00
Luca Postiglione 4d94b2748d
Added support for dot notation in GridField name
When you want to have a GridField for a sub-relation on a relation of your DataObject you use dot notation, this breaks the functionality of GridFieldOrderableRows because in the POST request the dot is replaced by an underscore.
2020-01-29 12:57:30 +01:00
Lee Bradley 4afe5dcfd3 Tweaks to pass tests 2019-12-12 16:18:09 +00:00
Lee Bradley 04c42273cf Add Multi Class to inline add button
Raised in Issue #300
2019-12-12 16:10:11 +00:00
Garion Herman 8e37021317
Merge pull request #299 from creative-commoners/pulls/3/travis-ci
Travis config update
2019-11-25 17:10:34 +13:00
Serge Latyntcev a710c81941 Travis config update 2019-11-25 16:58:44 +13:00
Serge Latyntcev d0cf736174 Travis config update 2019-11-25 16:48:52 +13:00
Serge Latyntcev 0dc0c5614d Merge branch '3.2' into 3 2019-11-25 16:02:34 +13:00
Sam Minnee 0b37e97b42 FIX: Fix insertion of new records in many-many-through list.
This change is specially important when using dot-syntax fieldnames to
access the join record of a many-many-through.
2019-08-26 18:11:36 +12:00
Sam Minnee d357479421 FIX: Let GridFieldEditableColumns edit relations via dot syntax
This is a companion to https://github.com/silverstripe/silverstripe-framework/pull/9192 to provide the same functionality for inline
editing in GridFields

A valuable use of this is editing fields in the join-object of
a many-many-through relation.
2019-08-26 12:37:19 +12:00
Guy Marriott 668b297a30
Merge pull request #291 from kinglozzer/inline-delete
FIX: Unable to delete inline-added rows before saving them
2019-04-11 08:37:22 +12:00
Loz Calver f22531bf6a FIX: Unable to delete inline-added rows before saving them 2019-04-10 17:09:13 +01:00
Robbie Averill 6a26d76c6f
Merge pull request #283 from kinglozzer/many-many-through-the-fire-and-flames
FIX: Remove unnecessary "version mismatch" restriction (fixes #282)
2019-04-05 09:44:32 +13:00
Loz Calver 600a39e428 FIX: Remove unnecessary "version mismatch" restriction (fixes #282) 2019-04-05 09:37:41 +13:00
Robbie Averill d97bbbddce
Merge pull request #287 from ivoba/3.2-fix-GridFieldConfigurablePaginator
fixed unload modal in GridFieldConfigurablePaginator
2019-03-01 15:44:59 +13:00
Ivo Bathke 088e7f16a8 FIX Fixed unload modal in GridFieldConfigurablePaginator 2019-03-01 15:39:26 +13:00
Robbie Averill 76541b27f2 Add PHP 7.3 to Travis builds and use recipe-cms instead of recipe-core
Fixes the version incompatibility with silverstripe/versioned and its new interfaces
2019-03-01 15:38:49 +13:00
Daniel Hensby 56d1adf64a
Merge pull request #274 from creative-commoners/pulls/3.2/checkbox-position
FIX Position inline editable checkbox fields relative rather than absolute
2018-10-19 22:52:48 +01:00
Robbie Averill 3876527913 FIX Position inline editable checkbox fields relative rather than absolute 2018-10-19 21:38:19 +02:00
Robbie Averill 4263c9d970
FIX Disable change tracking on configurable paginator inputs
See https://github.com/silverstripe/silverstripe-framework/pull/8486
2018-10-17 11:36:45 +02:00
Dylan Wagstaff 6e922fcec0
Merge pull request #272 from creative-commoners/pulls/3.2/correct-sort-orders-for-mmtl
FIX Orderable rows now respects actual MMTL sort orders instead of incrementing from SiteTree
2018-09-30 21:57:24 +13:00
Robbie Averill 968b807918 Add SS 4.3.x to Travis matrix and move Postgres to 2.1.x-dev with SS 4.2 or newer 2018-09-28 18:24:50 +02:00
Robbie Averill 78c63c6725 Add test for getting the correct Sort order from MMTL items, fix incorrect test class name 2018-09-28 17:59:44 +02:00
Robbie Averill b221134ce1 FIX Orderable rows now respects actual MMTL sort orders instead of incrementing from SiteTree 2018-09-28 17:38:38 +02:00
Robbie Averill a88ac60033
Update branch alias for 3.3.x-dev 2018-09-28 17:34:58 +02:00
Robbie Averill a034bd5973 Remove obsolete branch alias 2018-07-04 09:30:03 +12:00
Dylan Wagstaff 7355a4a816 Merge branch '3' into 'master' 2018-07-02 11:35:52 +12:00
Dylan Wagstaff bba8547054 Add unit tests for new ManyManyThrough support
The previous commit (9fa9ef89) added support for the new SilverStripe 4
feature of Many Many relationships through an intermediary object. After
much head scratching and community testing, the solution was proven to
work, however had no automated tests to confirm as such. This commit
rectifies that by testing both versioned and unversioned DataObjects in
a many_many through style relationship. Some minor tidy and comments
were also added as per feedback on the functionality code changes.
2018-07-02 11:28:15 +12:00
Dylan Wagstaff 95f0acb0f4 NEW Add support for ManyManyThrough relations
Previously relationships defiend as many_many came in a special type
of RelationList - however now this can be one of two types of
RelationList depending on the type of definition, with both being
valid many_many relationships.

This had the unfortunate side effect of seeing the OrderableRows
component in (the least) cease functioning correctly. No longer.

This also has the fortunate bonus of allowing a many_many relationship to
be versioned; where previously while each item in the relationship could
be versioned, the relationship itself could not.
2018-07-02 11:25:16 +12:00
Dylan Wagstaff a4d752c911 Merge up 3.1 into 3 2018-07-02 11:19:54 +12:00
Robbie Averill 7fdfe234fd
Merge pull request #260 from creative-commoners/pulls/3.1/many-many-reordering-fix
NEW Add support for ManyManyThrough relations
2018-07-02 09:59:51 +12:00
Guy Marriott 838e8d765e
Merge pull request #265 from creative-commoners/pulls/3.1/travis-recipes
Add various recipe versions to Travis build matrix
2018-06-28 13:42:03 +12:00
Dylan Wagstaff b6130c4e11 Add unit tests for new ManyManyThrough support
The previous commit (9fa9ef89) added support for the new SilverStripe 4
feature of Many Many relationships through an intermediary object. After
much head scratching and community testing, the solution was proven to
work, however had no automated tests to confirm as such. This commit
rectifies that by testing both versioned and unversioned DataObjects in
a many_many through style relationship. Some minor tidy and comments
were also added as per feedback on the functionality code changes.
2018-06-26 14:41:39 +12:00
Robbie Averill 688c29ba04 Add various recipe versions to Travis build matrix 2018-06-25 14:16:05 +12:00
Robbie Averill 5b14abb221
Merge pull request #264 from creative-commoners/pulls/master/add-supported-module-badge
Add supported module badge to readme
2018-06-18 10:45:47 +12:00
Dylan Wagstaff 939415a444 Add supported module badge to readme 2018-06-15 17:53:40 +12:00
Robbie Averill 19b4c6a7fa
Merge pull request #263 from DrMartinGonzo/patch-2
Fix "+" icon in add new inline button
2018-06-13 09:57:45 +12:00
Martin Portevin 41f6267c23 Fix "+" icon in add new inline button 2018-06-13 09:51:21 +12:00
Dylan Wagstaff 9fa9ef8903 NEW Add support for ManyManyThrough relations
Previously relationships defiend as many_many came in a special type
of RelationList - however now this can be one of two types of
RelationList depending on the type of definition, with both being
valid many_many relationships.

This had the unfortunate side effect of seeing the OrderableRows
component in (the least) cease functioning correctly. No longer.

This also has the fortunate bonus of allowing a many_many relationship to
be versioned; where previously while each item in the relationship could
be versioned, the relationship itself could not.
2018-06-01 16:23:00 +12:00
Robbie Averill e898711add
Merge pull request #255 from n8-sd/feature/allow_onAfterReorderItems_sortedIDs
Allow onAfterreorderItems extra variables.
2018-05-11 15:33:18 +12:00
Daniel Hensby ba0d23ab5e
Merge pull request #256 from silverstripe-terraformers/feature/reorder_fix_versioned 2018-04-27 11:55:57 +01:00
Mojmir Fendek 6b47b6d63a Corrected a case when sort column is located in a table that belongs to ancestor class of the item. 2018-04-27 10:00:34 +12:00
Nathan SD e15f1766bf Allow onAfterreorderItems extra variables. 2018-04-24 14:25:13 +12:00
Robbie Averill b93a8f28ac
Merge pull request #250 from deracs/master
Can't reorder many many versioned classes
2018-03-06 16:14:22 +13:00
Michael Goldsmith 9009a83ae1 Revert back to previous version check to allow many many version objects to pass through the non-ORM method 2018-03-06 14:45:29 +13:00
Daniel Hensby 8e24be2660
Merge pull request #248 from silverstripe-terraformers/bugfix-update-preview-on-reorder
Update CMS preview when re-ordering rows
2018-02-27 10:09:00 +00:00
Bernard Hamlin e7825cd0a3 Update CMS preview when re-ordering rows 2018-02-27 16:13:06 +13:00
Daniel Hensby cbffc5b935
Merge branch '3' 2018-02-21 13:30:39 +00:00
Daniel Hensby 8c82965e7e
Merge branch '2' into 3 2018-02-21 13:28:13 +00:00
Daniel Hensby 96af48a2fe
Merge branch '3.1' into 3 2018-02-21 11:55:43 +00:00
Daniel Hensby 0974e36d57
Merge branch '3.0' into 3.1 2018-02-21 11:49:19 +00:00
Daniel Hensby 51dd4cab65
Merge branch '2.0' into 3.0 2018-02-21 11:25:09 +00:00
Daniel Hensby 30ae7e4037
Merge branch '2.0' into 2 2018-02-21 09:25:13 +00:00
Daniel Hensby 5945ce105f
Merge branch '1.5' into 2.0 2018-02-21 09:24:30 +00:00
Daniel Hensby e20ead2624
Merge branch '1.4' into 1.5 2018-02-21 09:23:34 +00:00
Robbie Averill e2bfcd1d9f Merge branch '2.0' into 2 2018-02-21 16:23:28 +13:00
Robbie Averill b192551f30 Merge branch '3' 2018-02-12 11:13:37 +13:00
Robbie Averill ed51125b91 Merge branch '3.1' into 3 2018-02-12 11:12:12 +13:00
Priyashantha 6d8b41ee5a Fixed GridFieldOrderableRows issue when data class is Versioned and relation is has_many (#243)
* Fixed GridFieldOrderableRows issue when data class is Versioned and relation is has_many

* just compare table names rather than updating existing getSortTable() func
2018-02-12 11:08:47 +13:00
Robbie Averill 37a206cf46
Merge pull request #244 from zanderwar/patch-1
Added installation instruction
2018-02-11 14:43:22 +13:00
Reece Alexander 9d4c87f5c7
Added installation instruction 2018-02-11 05:48:09 +10:00
Priyashantha d1021ace51 Fixed GridFieldOrderableRows issue when data class is Versioned and relation is has_many (#243)
* Fixed GridFieldOrderableRows issue when data class is Versioned and relation is has_many

* just compare table names rather than updating existing getSortTable() func
2018-01-30 20:40:59 +00:00
Daniel Hensby 509abf1532
Merge pull request #233 from silverstripe-terraformers/feature/SS4_upgrade_fixes 2018-01-26 16:34:31 +00:00
Daniel Hensby 64b6fd04f3
Merge pull request #238 from creative-commoners/pulls/3.1/fix-searchbutton
FIX Update GridFieldAddExistinSearchButton and dialog to use Bootstrap classes
2018-01-26 16:31:31 +00:00
Daniel Hensby 4955b9c4b9
Merge pull request #239 from creative-commoners/pulls/3.1/add-7.2
Add PHP 7.2 to Travis build matrix, add individual phpcs ruleset
2018-01-26 13:48:10 +00:00
Daniel Hensby 6ef71280d1
Merge pull request #240 from creative-commoners/pulls/3.1/add-new-multi-class-width
FIX Remove excessive width on add new multi class dropdown
2018-01-26 13:47:15 +00:00
Robbie Averill cd1fdada5d FIX Remove excessive width on add new multi class dropdown
This removes a width definition added in da2e75f
2018-01-26 14:05:16 +13:00
Robbie Averill 24db0da047 Fix PSR-2 rule violations and incorrect docblock return type 2018-01-26 13:40:06 +13:00
Robbie Averill b29db7a4e7 Add PHP 7.2 to Travis build matrix, add individual phpcs ruleset 2018-01-26 13:37:44 +13:00
Robbie Averill 6bf1a05975 Update branch alias for 4.x-dev 2018-01-26 12:06:49 +13:00
Robbie Averill 8e8c1db7bf Merge branch '3' 2018-01-26 12:06:24 +13:00
Robbie Averill e67065a780 Update branch alias for 3.2.x-dev 2018-01-26 12:05:25 +13:00
Martin Portevin af0f342c3b Fix stopPropagation on click if no GridFieldEditableColumns
stopPropagation shouldn't fire if no GridFieldEditableColumns was added. It forces user to click on edit button on the far right instead of directly being able to click anywhere on the row.
2018-01-26 12:02:29 +13:00
Robbie Averill 978fda31d4
Merge pull request #129 from helpfulrobot/add-standard-code-of-conduct-file
Added standard code of conduct file
2018-01-26 11:55:52 +13:00
Robbie Averill f540f247bb
Merge pull request #235 from DrMartinGonzo/patch-1
Fix stopPropagation on click if no GridFieldEditableColumns
2018-01-26 11:54:36 +13:00
Robbie Averill eb9093a3ac Update translations 2018-01-26 11:53:55 +13:00
Robbie Averill 0a16566471 FIX Convert broken add-existing-search-form styles to use Bootstrap lists and pagination 2018-01-26 11:53:40 +13:00
Robbie Averill 8ba5c3435e FIX Switch Add Existing from jQuery UI to use Bootstrap styles 2018-01-26 11:01:24 +13:00
Robbie Averill 14c0080853 Update branch alias for 3.x-dev 2018-01-18 11:45:47 +13:00
Robbie Averill eee01960e8 Merge branch '3.1' 2018-01-18 11:45:08 +13:00
Robbie Averill 692b9df70c Merge branch '3.0' into 3.1 2018-01-18 11:43:46 +13:00
Robbie Averill 38c1168bea Remove obsolete branch alias 2018-01-18 11:43:27 +13:00
Robbie Averill 84241738b6 Remove obsolete branch alias 2018-01-18 11:42:59 +13:00
Martin Portevin 30a4f34fbf
Fix stopPropagation on click if no GridFieldEditableColumns
stopPropagation shouldn't fire if no GridFieldEditableColumns was added. It forces user to click on edit button on the far right instead of directly being able to click anywhere on the row.
2018-01-14 08:44:17 +01:00
Mojmir Fendek 735cc01270 SS4 upgrade fixes. 2018-01-12 12:00:20 +13:00
Robbie Averill dc34dbe781
Merge pull request #234 from nspyke/css-typo-fix
FIX CSS typo's
2018-01-11 14:54:11 +13:00
Nik Spijkerman 6bcb983605 FIX CSS typo's 2018-01-11 12:52:57 +13:00
Robbie Averill 6096a5ae53
Merge pull request #197 from flamytwista/patch-1
Russian Translation
2018-01-10 13:55:37 +13:00
Taras Yemtsov 102768763a Russian Translation 2018-01-10 13:50:08 +13:00
Dylan Wagstaff 18b7032211
Merge pull request #231 from creative-commoners/pulls/3.0/fix-configurable-paginator-styles
FIX Update template classes and styles for configurable paginator
2017-12-01 10:50:33 +13:00
Robbie Averill ade9bb50f4 FIX Update template classes and styles for configurable paginator 2017-12-01 10:41:17 +13:00
Dylan Wagstaff 0203e33117
Merge pull request #224 from creative-commoners/pulls/3.0/multiclass-ux-updates
FIX Minor UX tweak to add new multi class button, removing empty space and padding
2017-11-09 10:21:05 +13:00
Robbie Averill fdbac60300
Remove PHP 5.3 from Travis builds 2017-11-08 15:22:13 +13:00
Robbie Averill 4580d0b913 FIX Minor UX tweak to add new multi class button, removing empty space and padding 2017-11-08 12:59:14 +13:00
Robbie Averill da67666c22
Merge pull request #220 from christopherdarling/patch-1
DOCS fix removeComponentsByType example
2017-11-08 12:43:44 +13:00
Christopher Darling d5960b5efb
DOCS fix removeComponentsByType example
missing namespace
2017-11-06 12:06:00 +00:00
Dylan Wagstaff 40ed8c7f49 Merge pull request #205 from creative-commoners/pulls/3.0/fix-closure-import
FIX Import Closure class for type checking, and only set sort if the sort field exists
2017-10-26 14:37:52 +13:00
Dylan Wagstaff a7c5283bdd Merge branch 'master' into pulls/3.0/fix-closure-import 2017-10-26 14:34:43 +13:00
Robbie Averill 834547e5c0 Merge pull request #219 from martinduparc/patch-1
Fixed link to SS3 compatible branch
2017-10-26 09:13:45 +13:00
Martin D 931e014a0e Fixed link to SS3 compatible branch 2017-10-25 13:22:35 -04:00
Daniel Hensby 091954d987 Merge pull request #218 from creative-commoners/pulls/3.0/vendorise
API Install GridFieldExtensions to vendor folder, remove get_module_dir
2017-10-11 15:52:29 +01:00
Robbie Averill efdf9dcc13 API Install GridFieldExtensions to vendor folder, remove get_module_dir 2017-10-11 09:34:02 +13:00
Robbie Averill c120dc23ce Merge pull request #217 from creative-commoners/pulls/vendorise-ci
FIX convert CI bootstrap references to new their new locations in vendor
2017-10-05 12:44:54 +13:00
Dylan Wagstaff e98226fe3d FIX convert CI bootstrap references to new their new locations in vendor 2017-10-05 10:07:55 +13:00
Daniel Hensby ab28212d86 Merge pull request #204 from creative-commoners/pulls/3.0/fix-editable-columns-js
FIX Update selector for editable columns Javascript handler to match GridField.js in core
2017-09-28 16:53:34 +01:00
Robbie Averill ee9bd6b234
FIX Update selector for editable columns Javascript handler to match GridField.js in core 2017-09-28 16:45:39 +01:00
Robbie Averill ab6dfabb8a Merge pull request #215 from dhensby/pulls/lastedit-bug
TEST Cover base table last edited update
2017-09-26 00:05:58 +13:00
Daniel Hensby 23c0a58e48
TEST Cover base table last edited update 2017-09-25 11:56:34 +01:00
Robbie Averill b6f03225d0 Merge pull request #216 from dhensby/pulls/2.0/decouple-db-now
Use Datetime::now() for last edited updates in OrderableRows
2017-09-23 07:58:58 +12:00
Daniel Hensby 9d4ac960a7
Use Datetime::now() for last edited updates in OrderableRows 2017-09-22 13:55:14 +01:00
Daniel Hensby f764303ed0 Merge pull request #209 from creative-commoners/pulls/3.0/new-travis-config
FIX New Travis configuration, linting and updating existing skipped unit tests
2017-09-18 21:10:28 +01:00
Robbie Averill cb94bbe5e0
FIX Update remaining unit tests, separate stubs, add PSR-4 autoloader 2017-09-18 21:06:17 +01:00
Robbie Averill 0ad1fc367e
MINOR Apply PSR-2 linting 2017-09-18 21:06:16 +01:00
Robbie Averill 3ee4ff4cb7
Update Travis configuration, add codecov.io and PHPUnit config 2017-09-18 21:06:16 +01:00
Daniel Hensby 9e26e38d75
Merge branch '2' 2017-09-18 17:39:41 +01:00
Daniel Hensby fbc7278378
Merge branch '2.0' into 2 2017-09-18 14:45:58 +01:00
Daniel Hensby 09f6cdf299
Merge branch '1.5' into 2.0 2017-09-18 14:45:40 +01:00
Robbie Averill 39dddc4c91 Merge pull request #214 from open-sausages/pulls/2.0/better-casting
BUG Fix casting for ‘$Attributes’
2017-09-15 17:35:17 +12:00
Damian Mooyman b7b3678b26
BUG Fix casting for ‘$Attributes’ 2017-09-15 17:03:20 +12:00
Robbie Averill 46b792b27f FIX Import Closure class for type checking, and only set sort if the sort field exists 2017-09-15 16:32:38 +12:00
Robbie Averill 37b35f6a46 Merge pull request #213 from open-sausages/pulls/2.0/fix-inline-editable
BUG Fix inline editing / sortable
2017-09-15 16:30:45 +12:00
Damian Mooyman 9421e85483 BUG Fix inline editing / sortable 2017-09-15 16:08:13 +12:00
Marcus b07079d758 Merge pull request #211 from creative-commoners/pulls/2.0/add-button
FIX Upgrade button to use bootstrap classes
2017-09-12 09:42:15 +10:00
Sacha Judd 99f59c4efe FIX Upgrade button to use bootstrap classes 2017-09-08 15:19:15 +12:00
Robbie Averill 0404b305c7 Merge pull request #206 from open-sausages/pulls/2.0/fix-css-drag
BUG Fix incorrect drag-drop handle
2017-09-07 10:02:53 +12:00
Damian Mooyman 973f524b41
BUG Fix incorrect drag-drop handle
Update .editorconfig to match css / scss coding conventions
2017-09-06 09:13:56 +01:00
Marcus db29e863f7 Merge pull request #210 from adrexia/feature/addnewcss
FIX: Add new multiclass UI
2017-09-06 10:47:27 +10:00
Jake B 04a07d505f FIX Backport and sanitiseClassName for the "Save" action URL 2017-09-04 14:59:38 +10:00
Naomi Guyer da2e75f3c4 FIX: Add new multiclass UI
Visual fixes for add new multiclass button/selector
2017-09-01 23:05:02 +12:00
Robbie Averill 9ccd300629 Merge branch '2.0' into 2 2017-08-31 14:53:12 +12:00
Robbie Averill 730f7a13ea Merge branch '1.5' into 2.0 2017-08-31 14:52:32 +12:00
Robbie Averill 58200f847f FIX When setting the page sizes, reset items per page to the first value 2017-08-31 14:47:19 +12:00
Robbie Averill a34d6242a5 Update branch alias 2017-08-31 14:45:42 +12:00
Robbie Averill a36c96e692 Remove obsolete branch alias 2017-08-31 14:44:40 +12:00
Franco Springveldt bb4bb62be3 Merge pull request #207 from creative-commoners/pulls/1.5/reset-page-size
FIX When setting the page sizes, reset items per page to the first value
2017-08-31 14:43:19 +12:00
Robbie Averill 320a6198e5 FIX When setting the page sizes, reset items per page to the first value 2017-08-31 14:35:52 +12:00
Daniel Hensby 1e84c9af06 Merge pull request #203 from creative-commoners/pulls/3.0/remove-object
FIX Remove references to Object, replace with Injector calls
2017-08-25 10:45:40 +01:00
Robbie Averill 58604a6234 FIX Remove references to Object, replace with Injector calls 2017-08-25 13:46:30 +12:00
Marcus aa9bbbafdd Merge pull request #199 from wilr/master
Inline fixes for better SS4 behaviour
2017-08-24 16:30:16 +10:00
Will Rossiter aaf1130569 Inline fixes 2017-07-28 07:19:28 +12:00
Damian Mooyman 57acb83546
Sanitise GridFieldAddNewMultiClassHandler::Link() 2017-07-06 17:27:07 +12:00
Daniel Hensby 08ccb15bdb
Merge branch '1.4' 2017-07-05 17:45:20 +01:00
Nathan Glasl 315cb0b1c1 FIX, #195, the template folder required a vendor change. 2017-07-05 10:12:12 +10:00
Marcus 87cd8ffc6d Merge pull request #194 from dnadesign/fix-multi-class-injector
fix to multi-class for ss4 support
2017-07-03 10:28:57 +10:00
Nathan Glasl 997a5614fc VENDOR UPDATE. 2017-06-29 16:26:01 +10:00
John Milmine 7d3f5acbd7 fix to multi-class for ss4 support 2017-06-26 07:29:43 +12:00
Nathan Glasl 5cb337df00 VENDOR UPDATE. 2017-06-23 11:31:32 +10:00
Nathan Glasl 2fc4490af4 VENDOR UPDATE. 2017-06-22 14:31:39 +10:00
Nathan Glasl 3ebf09a572 VENDOR UPDATE. 2017-06-19 14:48:48 +10:00
Nathan Glasl ef48ea9cdd NAMESPACE, updating vendor. 2017-06-16 14:46:31 +10:00
Marcus 1b0abe0ca6 Merge pull request #190 from sachithra-gayan/gridFieldEditableColumns_SS4
Fixes error when inline editing of records SS4
2017-06-06 13:08:30 +10:00
Marcus ddec10c141 Merge pull request #187 from edlinklater/fix-table-namespace-separator
Support table namespace separator introduced in SilverStripe 4 alpha 6
2017-06-02 09:59:59 +10:00
sac 658874419a deny inline editing of records SS4 2017-06-01 13:02:43 +05:30
Ed Linklater 90b1a466c1 FIX support table namespace separator introduced in ss4 alpha 6 2017-05-14 16:17:14 +12:00
Marcus 3f6036b1ae Merge pull request #185 from wilr/patch-1
Fix SearchForm throwing a 404 on search
2017-05-04 11:45:51 +10:00
Marcus cc8276482f Merge pull request #186 from silverstripe-australia/nyeholt-patch-1
fix(GridFieldAddNewMultiClass) removed obsolete config param
2017-05-04 11:45:12 +10:00
Marcus 0795113cf4 fix(GridFieldAddNewMultiClass) removed obsolete config param
FIRST_SET has been removed from SS4
2017-05-04 11:41:58 +10:00
Will Rossiter 2c73b802d6 Fix SearchForm throwing a 404 on search
The GridFieldAddExistingSearchHandler search has been converted to namespaces however this causes the FormAction to be incorrect and when performing a search, currently throws an error. The form name does not need to be namespaced.
2017-05-04 05:29:44 +12:00
Jack O'Connor 0907b2f493 Initialising button before attempting to enable or disable it (#177)
* Initialising button before attempting to enable or disable it

* Updating class reference

* Adding exception use case

* Removing unused use case

* Including Exception class for classes where exception is used.
2017-02-17 13:14:50 +11:00
Marcus 24057fe79a Merge pull request #179 from oilee80/master
Fix for Exception namespace & CSS image move
2017-02-17 10:00:46 +11:00
Lee Bradley cbcb6e57e0 Applies changes from silverstripe-australia/silverstripe-gridfieldextensions#177
JS fix for "Initialising button before attempting to enable or disable it"
Thanks @JackOconnor21
2017-02-14 17:21:29 +00:00
Lee Bradley 1b5e83b7f6 Fix for Exception namespace & CSS image move
Missing '\\' before Exception
Icon has moved in Framework
2017-02-14 17:07:29 +00:00
Marcus 03b83e538a Merge pull request #174 from robbieaverill/master
SilverStripe 4 compatibility
2016-12-21 22:10:43 +11:00
Robbie Averill d81d701bab Update Travis configuration for SS4 builds and PHP 7 2016-12-21 15:40:46 +13:00
Robbie Averill 2acf6dee47 PSR-2 code style guidelines 2016-12-21 15:35:19 +13:00
Robbie Averill 4ee047b591 Add upgrade mapping 2016-12-21 15:27:51 +13:00
Robbie Averill b1c83255c0 Skip some broken tests under 4.x - need to be re-implemented 2016-12-21 15:27:51 +13:00
Robbie Averill 3cacb43934 Update composer constraint, branch alias, namespace and add editorconfig 2016-12-21 15:27:49 +13:00
hirenpatel 59f44984b8 Fix for namespaced classes 2016-12-21 15:26:45 +13:00
hirenpatel 43c1b8352c Updated SS_HTTPResponse_Exception -> HTTPResponse_Exception 2016-12-21 15:26:45 +13:00
hirenpatel 7ee247e6df Bug fixes 2016-12-21 15:26:44 +13:00
hirenpatel 1722e9b47f Template changes 2016-12-21 15:26:44 +13:00
hirenpatel bf158022fa Added ReflectionClass dependency 2016-12-21 15:26:44 +13:00
hirenpatel 0ae237cfee Fixed namespacing + added PSR-4 info 2016-12-21 15:26:42 +13:00
hirenpatel d5d43a7f6e Testing version workaround 2016-12-21 15:25:44 +13:00
Jayden Seric 5254b1865c Fixed name-spacing for SilverStripe v4 compatibility
Used [silverstripe-upgrader](https://github.com/silverstripe/silverstripe-upgrader) to fix name-spacing for SilverStripe v4. I then alphabetically sorted the use declarations for readability.

Fixes: https://github.com/silverstripe-australia/silverstripe-gridfieldextensions/issues/166.
2016-12-21 15:23:43 +13:00
Marcus Nyeholt 7674daf7f2 COMPOSER Updated readme and composer for v4 compat 2016-12-21 10:49:04 +11:00
helpfulrobot 135b248bee Added standard code of conduct file 2016-02-16 12:18:40 +13:00
121 changed files with 4825 additions and 2296 deletions

20
.editorconfig Normal file
View 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
View 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
View 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

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,5 @@
vendor/
resources/
composer.lock
assets/

View File

@ -1,38 +0,0 @@
# See https://github.com/silverstripe-labs/silverstripe-travis-support for setup details
sudo: false
language: php
dist: precise
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
View 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
View 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

View File

@ -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.

View File

@ -1,11 +1,7 @@
SilverStripe Grid Field Extensions Module
=========================================
# 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)
[![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:
@ -23,4 +19,13 @@ This module provides a number of useful grid field components:
* `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.

View File

@ -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
View 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
View 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
View 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
View 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
View 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
View 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
View 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"
}

View 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
View 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
View 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
View 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).

View File

@ -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);
}
}

View File

@ -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());
}
}

View File

@ -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');
if(!singleton($class)->canCreate()) {
return;
}
foreach($value[__CLASS__] as $fields) {
$item = $class::create();
$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();
$list->add($item, $extra);
}
}
}

View File

@ -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);
}
}

View File

@ -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)
);
}
}
}

View File

@ -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');
foreach($value[__CLASS__] as $id => $fields) {
if(!is_numeric($id) || !is_array($fields)) {
continue;
}
$item = $list->byID($id);
if(!$item || !$item->canEdit()) {
continue;
}
$form = $this->getForm($grid, $item);
$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
);
}
}

View File

@ -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__));
}
}

View File

@ -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');
}
}

View File

@ -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";
}
}

View File

@ -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;
}
}

View File

@ -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
View File

@ -0,0 +1 @@
comment: false

View File

@ -1,38 +1,54 @@
{
"name": "symbiote/silverstripe-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/symbiote/silverstripe-gridfieldextensions/issues"
},
"require": {
"silverstripe/framework": "~3.1"
},
"extra": {
"installer-name": "gridfieldextensions",
"branch-alias": {
"2.x-dev": "2.0.x-dev"
},
"screenshots": [
"docs/en/_images/editable-rows.png",
"docs/en/_images/add-existing-search.png"
]
},
"replace": {
"silverstripe-australia/gridfieldextensions": "self.version",
"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"
}

View File

@ -3,58 +3,31 @@
*/
.add-existing-search-dialog {
min-width: inherit !important;
min-width: inherit !important;
}
.add-existing-search-dialog .add-existing-search-form .field {
border: none;
box-shadow: none;
margin-bottom: 0;
padding-bottom: 0;
border: none;
box-shadow: none;
margin-bottom: 0;
padding-bottom: 0;
}
.add-existing-search-dialog .add-existing-search-form .field label {
padding-bottom: 4px;
padding: 4px 0;
}
.add-existing-search-dialog .add-existing-search-form .Actions {
margin-top: 10px;
padding: 0;
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 .list-group-item {
min-height: 32px;
}
.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;
.add-existing-search-dialog .btn-toolbar {
margin-top: 12px;
}
/**
@ -62,11 +35,16 @@
*/
.ss-gridfield-inline-new {
background: #EFE;
background: #EFE;
}
.ss-gridfield-inline-new:nth-child(2n) {
background: #DFD;
background: #DFD;
}
.grid-field__table .form-check-input.editable-column-field {
margin-top: .9rem;
position: relative;
}
/**
@ -74,53 +52,83 @@
*/
.ss-gridfield-add-new-multi-class {
margin-bottom: 8px !important;
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;
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;
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;
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;
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;
margin-bottom: 12px;
}
.ss-gridfield-add-new-inline span.readonly {
color: #FFF !important;
color: #FFF !important;
}
.ss-gridfield-add-new-inline .col-buttons {
text-align: right;
text-align: right;
}
/**
@ -128,79 +136,90 @@
*/
.ss-gridfield-orderable thead tr th.col-Reorder span {
padding: 0 !important;
margin-left: 8px;
padding: 0 !important;
margin-left: 8px;
}
.ss-gridfield-orderable .col-reorder {
position: relative;
padding: 0 !important;
width: 16px !important;
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;
cursor: move;padding: 16px 0 11px;
}
.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;
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);
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;
padding-left: 12px;
padding-right: 12px;
}
.ss-gridfield-orderable tfoot .ui-droppable-active {
background-color: #D4CF90 !important;
background-color: #D4CF90 !important;
}
.ss-gridfield-orderable tfoot .ss-gridfield-previouspage {
background-position: -16px 9px !important;
margin-left: 0;
background-position: -16px 9px !important;
margin-left: 0;
}
.ss-gridfield-orderable tfoot .ss-gridfield-nextpage {
background-position: -40px 9px !important;
margin-right: 0;
background-position: -40px 9px !important;
margin-right: 0;
}
/**
* GridFieldConfigurablePaginator
*/
.ss-gridfield-configurable-paginator .pagination-page-size {
color: #fff;
float: left;
padding: 6px 0;
align-items: center;
display: flex;
float: left;
}
.ss-gridfield-configurable-paginator .pagination-page-size-select {
margin-left: 0;
width: auto;
margin-left: .5rem;
}
.ss-gridfield-configurable-paginator .ss-gridfield-pagesize-submit {
display: none;
display: none;
}
.ss-gridfield-configurable-paginator .pagination-page-number input {
text-align: center;
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;
}

View File

@ -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,6 +106,17 @@ 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
@ -116,7 +129,7 @@ To use this component you should remove the original paginator component first:
```php
$gridField->getConfig()
->removeComponentsByType('GridFieldPaginator')
->addComponent(new GridFieldConfigurablePaginator());
->addComponent(GridFieldConfigurablePaginator::create());
```
You can configure the page sizes with the configuration system. Note that merging is the default strategy, so to replace
@ -131,7 +144,7 @@ Config::inst()->update('GridFieldConfigurablePaginator', 'default_page_sizes', a
You can also override these at call time:
```php
$paginator = new GridFieldConfigurablePaginator(100, array(100, 200, 500));
$paginator = GridFieldConfigurablePaginator::create(100, array(100, 200, 500));
$paginator->setPageSizes(array(200, 500, 1000));
$paginator->setItemsPerPage(500);

View File

@ -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")
});
}

85
javascript/tmpl.js Normal file
View 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 = {
"<": "&lt;",
">": "&gt;",
"&": "&amp;",
"\"": "&quot;",
"\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));

View File

@ -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
View 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
View 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

View File

@ -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)'

View File

@ -1,7 +1,7 @@
et_EE:
GridFieldAddExistingSearchHandler.ss:
NOITEMS: 'Kirjed puuduvad.'
RESULTS: 'Tulemused'
RESULTS: Tulemused
GridFieldExtensions:
ADD: Lisa
ADDEXISTING: 'Lisa olemasolev'

View File

@ -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'

View File

@ -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)'

View File

@ -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
View File

@ -0,0 +1,9 @@
ru_RU:
GridFieldAddExistingSearchHandler.ss:
NOITEMS: 'Нет элементов.'
RESULTS: Результаты
GridFieldExtensions:
ADD: Добавить
ADDEXISTING: 'Добавить существующий'
SEARCH: Поиск
SELECTTYPETOCREATE: '(Выберите тип для создания)'

View File

@ -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
View 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
View 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
View 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
View 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>

View 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');
}
}

View 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);
}
}

View 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());
}
}

View 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
View 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 ?? '');
}
}

View 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 ?? '');
}
}

View File

@ -1,10 +1,27 @@
<?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
*
@ -13,13 +30,6 @@ class GridFieldConfigurablePaginator extends GridFieldPaginator
*/
private static $default_page_sizes = array(15, 30, 60);
/**
* Which template to use for rendering
*
* @var string
*/
protected $itemClass = 'GridFieldConfigurablePaginator';
/**
* @var GridField
*/
@ -41,7 +51,7 @@ class GridFieldConfigurablePaginator extends GridFieldPaginator
*/
public function __construct($itemsPerPage = null, $pageSizes = null)
{
$this->setPageSizes($pageSizes ?: Config::inst()->get('GridFieldConfigurablePaginator', 'default_page_sizes'));
$this->setPageSizes($pageSizes ?: $this->config()->get('default_page_sizes'));
if (!$itemsPerPage) {
$itemsPerPage = $this->pageSizes[0];
@ -159,6 +169,10 @@ class GridFieldConfigurablePaginator extends GridFieldPaginator
public function setPageSizes(array $pageSizes)
{
$this->pageSizes = $pageSizes;
// Reset items per page
$this->setItemsPerPage(current($pageSizes ?? []));
return $this;
}
@ -242,7 +256,7 @@ class GridFieldConfigurablePaginator extends GridFieldPaginator
$this->setItemsPerPage($state->pageSize);
}
if (!($dataList instanceof SS_Limitable) || ($dataList instanceof UnsavedRelationList)) {
if (!($dataList instanceof Limitable) || ($dataList instanceof UnsavedRelationList)) {
return $dataList;
}
@ -255,7 +269,7 @@ class GridFieldConfigurablePaginator extends GridFieldPaginator
* {@inheritDoc}
*
* @param GridField $gridField
* @return ArrayList|null
* @return ArrayData|null
*/
public function getTemplateParameters(GridField $gridField)
{
@ -267,7 +281,7 @@ class GridFieldConfigurablePaginator extends GridFieldPaginator
// Figure out which page and record range we're on
if (!$arguments['total-rows']) {
return;
return null;
}
// Define a list of the FormActions that should be generated for pager controls (see getPagerActions())
@ -275,31 +289,35 @@ class GridFieldConfigurablePaginator extends GridFieldPaginator
'first' => array(
'title' => 'First',
'args' => array('first-shown' => 1),
'extra-class' => 'ss-gridfield-firstpage',
'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' => 'ss-gridfield-previouspage',
'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' => 'ss-gridfield-nextpage',
'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' => 'ss-gridfield-lastpage',
'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-pagesize-submit'
'extra-class' => 'ss-gridfield-pagination-action ss-gridfield-pagesize-submit'
),
);
@ -337,8 +355,8 @@ class GridFieldConfigurablePaginator extends GridFieldPaginator
if ($forTemplate) {
return array(
'footer' => $forTemplate->renderWith(
$this->itemClass,
array('Colspan' => count($gridField->getColumns()))
__CLASS__,
array('Colspan' => count($gridField->getColumns() ?? []))
)
);
}

View 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;
}
}

View 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');
}
}

View 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
View 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();
}
}

View 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;
}
}

View 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__)
);
}
}

View File

@ -1,3 +0,0 @@
<a href="$Link" class="action ss-ui-button ui-button add-existing-search" data-icon="magnifier">
$Title
</a>

View File

@ -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">&laquo;</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>&hellip;</li>
<% end_if %>
<% end_loop %>
<% if $Items.NotLastPage %>
<li><a href="$Items.NextLink">&raquo;</a></li>
<%end_if %>
</ul>
<% end_if %>
</div>

View File

@ -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>

View File

@ -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>

View File

@ -1,30 +0,0 @@
<tr>
<td class="bottom-all" colspan="$Colspan">
<span class="pagination-page-size">
<%t GridFieldConfigurablePaginator.SHOW 'Show' %>
<select name="$PageSizesName" class="pagination-page-size-select" 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="datagrid-pagination">
$FirstPage $PreviousPage
<span class="pagination-page-number">
<%t Pagination.Page 'Page' %>
<input class="text" value="$CurrentPageNum" data-skip-autofocus="true" />
<%t TableListField_PageControls_ss.OF 'of' is 'Example: View 1 of 2' %>
$NumPages
</span>
$NextPage $LastPage
</div>
<% end_if %>
<span class="pagination-records-number">
{$FirstShownRecord}&ndash;{$LastShownRecord}
<%t TableListField_PageControls_ss.OF 'of' is 'Example: View 1 of 2' %>
$NumRecords
</span>
</td>
</tr>

View File

@ -1,2 +0,0 @@
<span class="handle"><span class="icon"></span></span>
$SortField

View File

@ -0,0 +1,3 @@
<a href="$Link" class="$Classes">
<span class="btn__title">$Title</span>
</a>

View File

@ -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">&laquo;</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="#">&hellip;</a>
</li>
<% end_if %>
<% end_loop %>
<% if $Items.NotLastPage %>
<a class="page-link" href="$Items.NextLink">
<span aria-hidden="true">&raquo;</span>
<span class="sr-only"><%t GridFieldExtensions.Next "Next" %></span>
</a>
<%end_if %>
</ul>
<% end_if %>
</div>

View File

@ -0,0 +1,3 @@
<button href="$Link" class="ss-gridfield-add-new-inline btn btn-primary font-icon-plus-circled">
$Title
</button>

View File

@ -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>

View File

@ -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>

View File

@ -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}&ndash;{$LastShownRecord}
<%t SilverStripe\\Forms\\GridField\\GridFieldPaginator.OF 'of' is 'Example: View 1 of 2' %>
$NumRecords
</span>
</td>
</tr>

View File

@ -0,0 +1,2 @@
<div class="handle"><i class="icon font-icon-drag-handle"></i></div>
$SortField

View File

@ -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 {}
/**#@-*/

View File

@ -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;
}
}
/**#@-*/

View File

@ -1,5 +1,14 @@
<?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
{
/**
@ -7,7 +16,7 @@ class GridFieldConfigurablePaginatorTest extends SapphireTest
*/
protected $gridField;
public function setUp()
protected function setUp(): void
{
parent::setUp();
@ -80,6 +89,20 @@ class GridFieldConfigurablePaginatorTest extends SapphireTest
$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));
@ -135,26 +158,24 @@ class GridFieldConfigurablePaginatorTest extends SapphireTest
// Via default configuration
$paginator = new GridFieldConfigurablePaginator;
$default = Config::inst()->get('GridFieldConfigurablePaginator', 'default_page_sizes');
$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->assertDOSEquals(array(
$this->assertListEquals(array(
array('Size' => '10', 'Selected' => true),
array('Size' => '20', 'Selected' => false),
array('Size' => '30', 'Selected' => false),
), $paginator->getPageSizesAsList());
}
/**
* @expectedException Exception
* @expectedExceptionMessage No GridField available yet for this request!
*/
public function testGetGridFieldThrowsExceptionWhenNotSet()
{
$this->expectException(\Exception::class);
$this->expectExceptionMessage('No GridField available yet for this request!');
$paginator = new GridFieldConfigurablePaginator;
$paginator->getGridField();
}
@ -182,13 +203,13 @@ class GridFieldConfigurablePaginatorTest extends SapphireTest
)
);
$gridField = $this->getMockBuilder('GridField')->disableOriginalConstructor()->getMock();
$paginator = new GridFieldConfigurablePaginator;
$result = $paginator->getPagerActions($controls, $gridField);
$paginator->setGridField($this->gridField);
$result = $paginator->getPagerActions($controls, $this->gridField);
$this->assertCount(2, $result);
$this->assertArrayHasKey('next', $result);
$this->assertContainsOnlyInstancesOf('GridField_FormAction', $result);
$this->assertContainsOnlyInstancesOf(GridField_FormAction::class, $result);
$this->assertFalse($result['prev']->isDisabled());

View 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()
);
}
}

View File

@ -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 {
}
/**#@-*/

View File

@ -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

View 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

View 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

View 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);
}
}

View 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;
}
}

View 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
];
}

View 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';
}

View 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
View 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
View 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
View File

@ -0,0 +1,9 @@
<?php
namespace Symbiote\GridFieldExtensions\Tests\Stub;
use SilverStripe\Dev\TestOnly;
class StubC extends StubA implements TestOnly
{
}

View 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';
}

View 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';
}

View 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
View 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';
}

View 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';
}

View 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,
];
}

View 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;
}
}

View File

@ -0,0 +1,10 @@
<?php
namespace Symbiote\GridFieldExtensions\Tests\Stub;
use SilverStripe\Control\Controller;
class TestController extends Controller
{
private static $url_segment = 'test';
}

View 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,
];
}

View 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,
];
}

View 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'
];
}

View 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,
];
}

View 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,
];
}

View 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,
];
}

View 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';
}

View 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';
}

View 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';
}