Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 80 additions & 16 deletions src/Security/Group.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
use SilverStripe\Forms\ListboxField;
use SilverStripe\Forms\LiteralField;
use SilverStripe\Forms\RequiredFields;
use SilverStripe\Forms\SearchableDropdownField;
use SilverStripe\Forms\Tab;
use SilverStripe\Forms\TabSet;
use SilverStripe\Forms\TextareaField;
Expand All @@ -33,6 +34,7 @@
use SilverStripe\ORM\HasManyList;
use SilverStripe\ORM\Hierarchy\Hierarchy;
use SilverStripe\ORM\ManyManyList;
use SilverStripe\ORM\Search\SearchContext;
use SilverStripe\ORM\UnsavedRelationList;

/**
Expand Down Expand Up @@ -91,6 +93,37 @@ class Group extends DataObject
'Sort' => true,
];

/**
* Specify the limit from when groups should be lazyloaded for the SearchableDropdownField
*
* @var int
* @config
*/
private static $dropdown_field_threshold = 100;

/**
* Create a search context which is used for the SearchableDropdownField
* in {@see Group::getCMSFields()} and {@see Member::getCMSFields()}
*
* @return SearchContext
*/
public static function get_search_context_for_dropdown(): SearchContext
{
$group = Group::singleton();

return SearchContext::create(
Group::class,
FieldList::create(
TextField::create('Title', $group->fieldLabel('Title')),
TextField::create('Description', $group->fieldLabel('Description')),
),
[
'Title' => 'PartialFilter',
'Description' => 'PartialFilter',
]
);
}

public function getAllChildren()
{
$doSet = new ArrayList();
Expand Down Expand Up @@ -122,18 +155,34 @@ private function getDecodedBreadcrumbs()
*/
public function getCMSFields()
{
$groups = Group::get();
$threshold = Group::config()->get('dropdown_field_threshold');
$overThreshold = $groups->count() > $threshold;

$fields = new FieldList(
new TabSet(
"Root",
new Tab(
'Members',
_t(__CLASS__ . '.MEMBERS', 'Members'),
new TextField("Title", $this->fieldLabel('Title')),
$parentidfield = DropdownField::create(
$parentidfield = SearchableDropdownField::create(
'ParentID',
$this->fieldLabel('Parent'),
$this->getDecodedBreadcrumbs()
)->setEmptyString(' '),
$groups,
null,
'BreadcrumbTitle'
)
->setIsSearchable(true)
->setUseSearchContext(true)
->setSearchContext(Group::get_search_context_for_dropdown())
->setPlaceholder(_t(
__CLASS__ . '.PARENT_GROUP_PLACEHOLDER',
'Select parent group',
'Placeholder text for a dropdown'
))
->setIsLazyLoaded($overThreshold)
->setLazyLoadLimit($threshold),
new TextareaField('Description', $this->fieldLabel('Description'))
),
$permissionsTab = new Tab(
Expand Down Expand Up @@ -460,6 +509,16 @@ public function stageChildren()
->sort('"Sort"');
}

/**
* Label which can be used inside a SearchableDropdownField for example
*
* @return string
*/
public function getBreadcrumbTitle(): string
{
return $this->getBreadcrumbs(' > ');
}
Comment on lines +517 to +520
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this new method?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the Label for the FormField.


/**
* @return string
*/
Expand Down Expand Up @@ -503,12 +562,12 @@ public function validate()
}
}

$currentGroups = Group::get()
->filter('ID:not', $this->ID)
->map('Code', 'Title')
->toArray();
$hasGroupWithSameTitle = Group::get()
->exclude('ID', $this->ID)
->filter('Title', $this->Title)
->exists();

if (in_array($this->Title, $currentGroups)) {
if ($hasGroupWithSameTitle) {
$result->addError(
_t(
'SilverStripe\\Security\\Group.ValidationIdentifierAlreadyExists',
Expand Down Expand Up @@ -711,16 +770,21 @@ public function requireDefaultRecords()
*/
private function dedupeCode(): void
{
$currentGroups = Group::get()
->exclude('ID', $this->ID)
->map('Code', 'Title')
->toArray();
$code = $this->Code;
$count = 2;
while (isset($currentGroups[$code])) {
$code = $this->Code . '-' . $count;
$count++;

if ($code) {
while ($this->checkIfCodeExists($code)) {
$code = $this->Code . '-' . $count;
$count++;
}

$this->setField('Code', $code);
}
$this->setField('Code', $code);
}

private function checkIfCodeExists(string $code): bool
{
return Group::get()->filter('Code', $code)->exclude('ID', $this->ID)->exists();
}
Comment on lines 771 to 789
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do changes to the deduping code relate to usage of a different form field?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is more or less the same issue as with the large list in the FormField. Without you are not able to save a group if you have many of them.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can check the output of my generater script above with and without the change. Without the script will get slower with each iteration.

}
38 changes: 22 additions & 16 deletions src/Security/Member.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,17 @@
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\HTMLEditor\HTMLEditorConfig;
use SilverStripe\Forms\ListboxField;
use SilverStripe\Forms\SearchableMultiDropdownField;
use SilverStripe\Forms\Tab;
use SilverStripe\Forms\TabSet;
use SilverStripe\Forms\TreeMultiselectField;
use SilverStripe\i18n\i18n;
use SilverStripe\ORM\ArrayList;
use SilverStripe\ORM\DataList;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\DB;
use SilverStripe\ORM\FieldType\DBDatetime;
use SilverStripe\ORM\FieldType\DBForeignKey;
use SilverStripe\ORM\HasManyList;
use SilverStripe\ORM\ManyManyList;
use SilverStripe\ORM\Map;
Expand Down Expand Up @@ -1365,29 +1368,32 @@ public function getCMSFields()
$fields->removeByName('RememberLoginHashes');

if (Permission::check('EDIT_PERMISSIONS')) {
// Filter allowed groups
$groups = Group::get();
$disallowedGroupIDs = $this->disallowedGroups();
if ($disallowedGroupIDs) {

if ($disallowedGroupIDs = $this->disallowedGroups()) {
$groups = $groups->exclude('ID', $disallowedGroupIDs);
}
$groupsMap = [];
foreach ($groups as $group) {
// Listboxfield values are escaped, use ASCII char instead of »
$groupsMap[$group->ID] = $group->getBreadcrumbs(' > ');
}
asort($groupsMap);

$threshold = Group::config()->get('dropdown_field_threshold');
$overThreshold = $groups->count() > $threshold;

$fields->addFieldToTab(
'Root.Main',
ListboxField::create('DirectGroups', Group::singleton()->i18n_plural_name())
->setSource($groupsMap)
->setAttribute(
'data-placeholder',
_t(__CLASS__ . '.ADDGROUP', 'Add group', 'Placeholder text for a dropdown')
)
SearchableMultiDropdownField::create(
'DirectGroups',
Group::singleton()->i18n_plural_name(),
$groups,
null,
'BreadcrumbTitle'
)
->setIsSearchable(true)
->setUseSearchContext(true)
->setSearchContext(Group::get_search_context_for_dropdown())
->setIsLazyLoaded($overThreshold)
->setLazyLoadLimit($threshold)
->setPlaceholder(_t(__CLASS__ . '.ADDGROUP', 'Add group', 'Placeholder text for a dropdown'))
);


// Add permission field (readonly to avoid complicated group assignment logic).
// This should only be available for existing records, as new records start
// with no permissions until they have a group assignment anyway.
Expand Down
Loading