Test Setup Failed
Push — master ( 210134...c17796 )
by Damian
03:18
created

src/Forms/TreeDropdownField.php (1 issue)

1
<?php
2
3
namespace SilverStripe\Forms;
4
5
use Exception;
6
use InvalidArgumentException;
7
use SilverStripe\Assets\Folder;
8
use SilverStripe\Control\HTTPRequest;
9
use SilverStripe\Control\HTTPResponse;
10
use SilverStripe\ORM\DataList;
11
use SilverStripe\ORM\DataObject;
12
use SilverStripe\ORM\FieldType\DBDatetime;
13
use SilverStripe\ORM\Hierarchy\Hierarchy;
14
use SilverStripe\ORM\Hierarchy\MarkedSet;
15
16
/**
17
 * Dropdown-like field that allows you to select an item from a hierarchical
18
 * AJAX-expandable tree.
19
 *
20
 * Creates a field which opens a dropdown (actually a div via javascript
21
 * included for you) which contains a tree with the ability to select a singular
22
 * item for the value of the field. This field has the ability to store one-to-one
23
 * joins related to hierarchy or a hierarchy based filter.
24
 *
25
 * **Note:** your source object must use an implementation of hierarchy for this
26
 * field to generate the tree correctly, e.g. {@link Group}, {@link SiteTree} etc.
27
 *
28
 * All operations are carried out through javascript and provides no fallback
29
 * to non JS.
30
 *
31
 * <b>Usage</b>.
32
 *
33
 * <code>
34
 * static $has_one = array(
35
 *   'RightContent' => 'SiteTree'
36
 * );
37
 *
38
 * function getCMSFields() {
39
 * ...
40
 * $treedropdownfield = new TreeDropdownField("RightContentID", "Choose a page to show on the right:", "SiteTree");
41
 * ..
42
 * }
43
 * </code>
44
 *
45
 * This will generate a tree allowing the user to expand and contract subsections
46
 * to find the appropriate page to save to the field.
47
 *
48
 * Caution: The form field does not include any JavaScript or CSS when used outside of the CMS context,
49
 * since the required frontend dependencies are included through CMS bundling.
50
 *
51
 * @see TreeMultiselectField for the same implementation allowing multiple selections
52
 * @see DropdownField for a simple dropdown field.
53
 * @see CheckboxSetField for multiple selections through checkboxes.
54
 * @see OptionsetField for single selections via radiobuttons.
55
 */
56
class TreeDropdownField extends FormField
57
{
58
    protected $schemaDataType = self::SCHEMA_DATA_TYPE_SINGLESELECT;
59
60
    /** @skipUpgrade */
61
    protected $schemaComponent = 'TreeDropdownField';
62
63
    private static $url_handlers = array(
64
        '$Action!/$ID' => '$Action'
65
    );
66
67
    private static $allowed_actions = array(
68
        'tree'
69
    );
70
71
    /**
72
     * @config
73
     * @var int
74
     * @see {@link Hierarchy::$node_threshold_total}.
75
     */
76
    private static $node_threshold_total = 30;
77
78
    /**
79
     * @var string
80
     */
81
    protected $emptyString = null;
82
83
    /**
84
     * @var bool
85
     */
86
    protected $hasEmptyDefault = false;
87
88
    /**
89
     * Class name for underlying object
90
     *
91
     * @var string
92
     */
93
    protected $sourceObject = null;
94
95
    /**
96
     * Name of key field on underlying object
97
     *
98
     * @var string
99
     */
100
    protected $keyField = null;
101
102
    /**
103
     * Name of label field on underlying object
104
     *
105
     * @var string
106
     */
107
    protected $labelField = null;
108
109
    /**
110
     * Similar to labelField but for non-html equivalent of field
111
     *
112
     * @var string
113
     */
114
    protected $titleField = 'Title';
115
116
    /**
117
     * Callback for filtering records
118
     *
119
     * @var callable
120
     */
121
    protected $filterCallback = null;
122
123
    /**
124
     * Callback for marking record as disabled
125
     *
126
     * @var callable
127
     */
128
    protected $disableCallback = null;
129
130
    /**
131
     * Callback for searching records. This callback takes the following arguments:
132
     *  - sourceObject Object class to search
133
     *  - labelField Label field
134
     *  - search Search text
135
     *
136
     * @var callable
137
     */
138
    protected $searchCallback = null;
139
140
    /**
141
     * Filter for base record
142
     *
143
     * @var int
144
     */
145
    protected $baseID = 0;
146
147
    /**
148
     * Default child method in Hierarchy->getChildrenAsUL
149
     *
150
     * @var string
151
     */
152
    protected $childrenMethod = 'AllChildrenIncludingDeleted';
153
154
    /**
155
     * Default child counting method in Hierarchy->getChildrenAsUL
156
     *
157
     * @var string
158
     */
159
    protected $numChildrenMethod = 'numChildren';
160
161
    /**
162
     * Current string value for search text to filter on
163
     *
164
     * @var string
165
     */
166
    protected $search = null;
167
168
    /**
169
     * List of ids in current search result (keys are ids, values are true)
170
     * This includes parents of search result children which may not be an actual result
171
     *
172
     * @var array
173
     */
174
    protected $searchIds = [];
175
176
    /**
177
     * List of ids which matches the search result
178
     * This excludes parents of search result children
179
     *
180
     * @var array
181
     */
182
    protected $realSearchIds = [];
183
184
    /**
185
     * Determine if search should be shown
186
     *
187
     * @var bool
188
     */
189
    protected $showSearch = false;
190
191
    /**
192
     * List of ids which have their search expanded (keys are ids, values are true)
193
     *
194
     * @var array
195
     */
196
    protected $searchExpanded = [];
197
198
    /**
199
     * Show full path for selected options, only applies for single select
200
     * @var bool
201
     */
202
    protected $showSelectedPath = false;
203
204
    /**
205
     * @var array
206
     */
207
    protected static $cacheKeyCache = [];
208
209
    /**
210
     * CAVEAT: for search to work properly $labelField must be a database field,
211
     * or you need to setSearchFunction.
212
     *
213
     * @param string $name the field name
214
     * @param string $title the field label
215
     * @param string $sourceObject A DataObject class name with the {@link Hierarchy} extension.
216
     * @param string $keyField to field on the source class to save as the
217
     *      field value (default ID).
218
     * @param string $labelField the field name to show as the human-readable
219
     *      value on the tree (default Title).
220
     * @param bool $showSearch enable the ability to search the tree by
221
     *      entering the text in the input field.
222
     */
223
    public function __construct(
224
        $name,
225
        $title = null,
226
        $sourceObject = null,
227
        $keyField = 'ID',
228
        $labelField = 'TreeTitle',
229
        $showSearch = true
230
    ) {
231
        if (!is_a($sourceObject, DataObject::class, true)) {
232
            throw new InvalidArgumentException("SourceObject must be a DataObject subclass");
233
        }
234
        if (!DataObject::has_extension($sourceObject, Hierarchy::class)) {
235
            throw new InvalidArgumentException("SourceObject must have Hierarchy extension");
236
        }
237
        $this->setSourceObject($sourceObject);
238
        $this->setKeyField($keyField);
239
        $this->setLabelField($labelField);
240
        $this->setShowSearch($showSearch);
241
242
        // Extra settings for Folders
243
        if (strcasecmp($sourceObject, Folder::class) === 0) {
244
            $this->setChildrenMethod('ChildFolders');
245
            $this->setNumChildrenMethod('numChildFolders');
246
        }
247
248
        $this->addExtraClass('single');
249
250
        parent::__construct($name, $title);
251
    }
252
253
    /**
254
     * Set the ID of the root node of the tree. This defaults to 0 - i.e.
255
     * displays the whole tree.
256
     *
257
     * @return int
258
     */
259
    public function getTreeBaseID()
260
    {
261
        return $this->baseID;
262
    }
263
264
    /**
265
     * Set the ID of the root node of the tree. This defaults to 0 - i.e.
266
     * displays the whole tree.
267
     *
268
     * @param int $ID
269
     * @return $this
270
     */
271
    public function setTreeBaseID($ID)
272
    {
273
        $this->baseID = (int) $ID;
274
        return $this;
275
    }
276
277
    /**
278
     * Get a callback used to filter the values of the tree before
279
     * displaying to the user.
280
     *
281
     * @return callable
282
     */
283
    public function getFilterFunction()
284
    {
285
        return $this->filterCallback;
286
    }
287
288
    /**
289
     * Set a callback used to filter the values of the tree before
290
     * displaying to the user.
291
     *
292
     * @param callable $callback
293
     * @return $this
294
     */
295
    public function setFilterFunction($callback)
296
    {
297
        if (!is_callable($callback, true)) {
298
            throw new InvalidArgumentException('TreeDropdownField->setFilterCallback(): not passed a valid callback');
299
        }
300
301
        $this->filterCallback = $callback;
302
        return $this;
303
    }
304
305
    /**
306
     * Get the callback used to disable checkboxes for some items in the tree
307
     *
308
     * @return callable
309
     */
310
    public function getDisableFunction()
311
    {
312
        return $this->disableCallback;
313
    }
314
315
    /**
316
     * Set a callback used to disable checkboxes for some items in the tree
317
     *
318
     * @param callable $callback
319
     * @return $this
320
     */
321
    public function setDisableFunction($callback)
322
    {
323
        if (!is_callable($callback, true)) {
324
            throw new InvalidArgumentException('TreeDropdownField->setDisableFunction(): not passed a valid callback');
325
        }
326
327
        $this->disableCallback = $callback;
328
        return $this;
329
    }
330
331
    /**
332
     * Set a callback used to search the hierarchy globally, even before
333
     * applying the filter.
334
     *
335
     * @return callable
336
     */
337
    public function getSearchFunction()
338
    {
339
        return $this->searchCallback;
340
    }
341
342
    /**
343
     * Set a callback used to search the hierarchy globally, even before
344
     * applying the filter.
345
     *
346
     * @param callable $callback
347
     * @return $this
348
     */
349
    public function setSearchFunction($callback)
350
    {
351
        if (!is_callable($callback, true)) {
352
            throw new InvalidArgumentException('TreeDropdownField->setSearchFunction(): not passed a valid callback');
353
        }
354
355
        $this->searchCallback = $callback;
356
        return $this;
357
    }
358
359
    /**
360
     * Check if search is shown
361
     *
362
     * @return bool
363
     */
364
    public function getShowSearch()
365
    {
366
        return $this->showSearch;
367
    }
368
369
    /**
370
     * @param bool $bool
371
     * @return $this
372
     */
373
    public function setShowSearch($bool)
374
    {
375
        $this->showSearch = $bool;
376
        return $this;
377
    }
378
379
    /**
380
     * Get method to invoke on each node to get the child collection
381
     *
382
     * @return string
383
     */
384
    public function getChildrenMethod()
385
    {
386
        return $this->childrenMethod;
387
    }
388
389
    /**
390
     * @param string $method The parameter to ChildrenMethod to use when calling Hierarchy->getChildrenAsUL in
391
     * {@link Hierarchy}. The method specified determines the structure of the returned list. Use "ChildFolders"
392
     * in place of the default to get a drop-down listing with only folders, i.e. not including the child elements in
393
     * the currently selected folder. setNumChildrenMethod() should be used as well for proper functioning.
394
     *
395
     * See {@link Hierarchy} for a complete list of possible methods.
396
     * @return $this
397
     */
398
    public function setChildrenMethod($method)
399
    {
400
        $this->childrenMethod = $method;
401
        return $this;
402
    }
403
404
    /**
405
     * Get method to invoke on nodes to count children
406
     *
407
     * @return string
408
     */
409
    public function getNumChildrenMethod()
410
    {
411
        return $this->numChildrenMethod;
412
    }
413
414
    /**
415
     * @param string $method The parameter to numChildrenMethod to use when calling Hierarchy->getChildrenAsUL in
416
     * {@link Hierarchy}. Should be used in conjunction with setChildrenMethod().
417
     *
418
     * @return $this
419
     */
420
    public function setNumChildrenMethod($method)
421
    {
422
        $this->numChildrenMethod = $method;
423
        return $this;
424
    }
425
426
    public function extraClass()
427
    {
428
        return implode(' ', array(parent::extraClass(), ($this->getShowSearch() ? "searchable" : null)));
429
    }
430
431
    /**
432
     * Get the whole tree of a part of the tree via an AJAX request.
433
     *
434
     * @param HTTPRequest $request
435
     * @return HTTPResponse
436
     * @throws Exception
437
     */
438
    public function tree(HTTPRequest $request)
439
    {
440
        // Regular source specification
441
        $isSubTree = false;
442
443
        $this->search = $request->requestVar('search');
444
        $id = (is_numeric($request->latestParam('ID')))
445
            ? (int)$request->latestParam('ID')
446
            : (int)$request->requestVar('ID');
447
448
        // pre-process the tree - search needs to operate globally, not locally as marking filter does
449
        if ($this->search) {
450
            $this->populateIDs();
451
        }
452
453
        /** @var DataObject|Hierarchy $obj */
454
        $obj = null;
455
        $sourceObject = $this->getSourceObject();
456
        if ($id && !$request->requestVar('forceFullTree')) {
457
            $obj = DataObject::get_by_id($sourceObject, $id);
458
            $isSubTree = true;
459
            if (!$obj) {
460
                throw new Exception(
461
                    "TreeDropdownField->tree(): the object #$id of type $sourceObject could not be found"
462
                );
463
            }
464
        } else {
465
            if ($this->getTreeBaseID()) {
466
                $obj = DataObject::get_by_id($sourceObject, $this->getTreeBaseID());
467
            }
468
469
            if (!$this->getTreeBaseID() || !$obj) {
470
                $obj = DataObject::singleton($sourceObject);
471
            }
472
        }
473
474
        // Create marking set
475
        $markingSet = MarkedSet::create(
476
            $obj,
477
            $this->getChildrenMethod(),
478
            $this->getNumChildrenMethod(),
479
            $this->config()->get('node_threshold_total')
480
        );
481
482
        // Set filter on searched nodes
483
        if ($this->getFilterFunction() || $this->search) {
484
            // Rely on filtering to limit tree
485
            $markingSet->setMarkingFilterFunction(function ($node) {
486
                return $this->filterMarking($node);
487
            });
488
            $markingSet->setLimitingEnabled(false);
489
        }
490
491
        // Begin marking
492
        $markingSet->markPartialTree();
493
494
        // Allow to pass values to be selected within the ajax request
495
        $value = $request->requestVar('forceValue') ?: $this->value;
496
        if ($value && ($values = preg_split('/,\s*/', $value))) {
497
            foreach ($values as $value) {
498
                if (!$value || $value == 'unchanged') {
499
                    continue;
500
                }
501
502
                $object = $this->objectForKey($value);
503
                if (!$object) {
504
                    continue;
505
                }
506
                $markingSet->markToExpose($object);
507
            }
508
        }
509
510
        // Set title formatter
511
        $customised = function (DataObject $child) use ($isSubTree) {
512
            return [
513
                'name' => $this->getName(),
514
                'id' => $child->obj($this->getKeyField()),
515
                'title' => $child->obj($this->getTitleField()),
516
                'treetitle' => $child->obj($this->getLabelField()),
517
                'disabled' => $this->nodeIsDisabled($child),
518
                'isSubTree' => $isSubTree
519
            ];
520
        };
521
522
        // Determine output format
523
        if ($request->requestVar('format') === 'json') {
524
            // Format JSON output
525
            $json = $markingSet
526
                ->getChildrenAsArray($customised);
527
528
            if ($request->requestVar('flatList')) {
529
                // format and filter $json here
530
                $json['children'] = $this->flattenChildrenArray($json['children']);
531
            }
532
            return HTTPResponse::create()
533
                ->addHeader('Content-Type', 'application/json')
534
                ->setBody(json_encode($json));
535
        } else {
536
            // Return basic html
537
            $html = $markingSet->renderChildren(
538
                [self::class . '_HTML', 'type' => 'Includes'],
539
                $customised
540
            );
541
            return HTTPResponse::create()
542
                ->addHeader('Content-Type', 'text/html')
543
                ->setBody($html);
544
        }
545
    }
546
547
    /**
548
     * Marking public function for the tree, which combines different filters sensibly.
549
     * If a filter function has been set, that will be called. And if search text is set,
550
     * filter on that too. Return true if all applicable conditions are true, false otherwise.
551
     *
552
     * @param DataObject $node
553
     * @return bool
554
     */
555
    public function filterMarking($node)
556
    {
557
        $callback = $this->getFilterFunction();
558
        if ($callback && !call_user_func($callback, $node)) {
559
            return false;
560
        }
561
562
        if ($this->search) {
563
            return isset($this->searchIds[$node->ID]) && $this->searchIds[$node->ID] ? true : false;
564
        }
565
566
        return true;
567
    }
568
569
    /**
570
     * Marking a specific node in the tree as disabled
571
     * @param $node
572
     * @return boolean
573
     */
574
    public function nodeIsDisabled($node)
575
    {
576
        $callback = $this->getDisableFunction();
577
        return $callback && call_user_func($callback, $node);
578
    }
579
580
    /**
581
     * Attributes to be given for this field type
582
     * @return array
583
     */
584
    public function getAttributes()
585
    {
586
        $attributes = array(
587
            'class' => $this->extraClass(),
588
            'id' => $this->ID(),
589
            'data-schema' => json_encode($this->getSchemaData()),
590
            'data-state' => json_encode($this->getSchemaState()),
591
        );
592
593
        $attributes = array_merge($attributes, $this->attributes);
594
595
        $this->extend('updateAttributes', $attributes);
596
597
        return $attributes;
598
    }
599
600
    /**
601
     * HTML-encoded label for this node, including css classes and other markup.
602
     *
603
     * @deprecated 4.0...5.0 Use setTitleField()
604
     * @param string $field
605
     * @return $this
606
     */
607
    public function setLabelField($field)
608
    {
609
        $this->labelField = $field;
610
        return $this;
611
    }
612
613
    /**
614
     * HTML-encoded label for this node, including css classes and other markup.
615
     *
616
     * @deprecated 4.0...5.0 Use getTitleField()
617
     * @return string
618
     */
619
    public function getLabelField()
620
    {
621
        return $this->labelField;
622
    }
623
624
    /**
625
     * Field to use for plain text item titles.
626
     *
627
     * @return string
628
     */
629
    public function getTitleField()
630
    {
631
        return $this->titleField;
632
    }
633
634
    /**
635
     * Set field to use for item title
636
     *
637
     * @param string $field
638
     * @return $this
639
     */
640
    public function setTitleField($field)
641
    {
642
        $this->titleField = $field;
643
        return $this;
644
    }
645
646
    /**
647
     * @param string $field
648
     * @return $this
649
     */
650
    public function setKeyField($field)
651
    {
652
        $this->keyField = $field;
653
        return $this;
654
    }
655
656
    /**
657
     * @return String
658
     */
659
    public function getKeyField()
660
    {
661
        return $this->keyField;
662
    }
663
664
    /**
665
     * @param string $class
666
     * @return $this
667
     */
668
    public function setSourceObject($class)
669
    {
670
        $this->sourceObject = $class;
671
        return $this;
672
    }
673
674
    /**
675
     * Get class of source object
676
     *
677
     * @return string
678
     */
679
    public function getSourceObject()
680
    {
681
        return $this->sourceObject;
682
    }
683
684
    /**
685
     * Flattens a given list of children array items, so the data is no longer
686
     * structured in a hierarchy
687
     *
688
     * NOTE: uses {@link TreeDropdownField::$realSearchIds} to filter items by if there is a search
689
     *
690
     * @param array $children - the list of children, which could contain their own children
691
     * @param array $parentTitles - a list of parent titles, which we use to construct the contextString
692
     * @return array - flattened list of children
693
     */
694
    protected function flattenChildrenArray($children, $parentTitles = [])
695
    {
696
        $output = [];
697
698
        foreach ($children as $child) {
699
            $childTitles = array_merge($parentTitles, [$child['title']]);
700
            $grandChildren = $child['children'];
701
            $contextString = implode('/', $parentTitles);
702
703
            $child['contextString'] = ($contextString !== '') ? $contextString .'/' : '';
704
            unset($child['children']);
705
706
            if (!$this->search || in_array($child['id'], $this->realSearchIds)) {
707
                $output[] = $child;
708
            }
709
            $output = array_merge($output, $this->flattenChildrenArray($grandChildren, $childTitles));
710
        }
711
712
        return $output;
713
    }
714
715
    /**
716
     * Populate $this->searchIds with the IDs of the pages matching the searched parameter and their parents.
717
     * Reverse-constructs the tree starting from the leaves. Initially taken from CMSSiteTreeFilter, but modified
718
     * with pluggable search function.
719
     */
720
    protected function populateIDs()
721
    {
722
        // get all the leaves to be displayed
723
        $res = $this->getSearchResults();
724
725
        if (!$res) {
726
            return;
727
        }
728
729
        // iteratively fetch the parents in bulk, until all the leaves can be accessed using the tree control
730
        foreach ($res as $row) {
731
            if ($row->ParentID) {
732
                $parents[$row->ParentID] = true;
733
            }
734
            $this->searchIds[$row->ID] = true;
735
        }
736
        $this->realSearchIds = $res->column();
737
738
        $sourceObject = $this->getSourceObject();
739
740
        while (!empty($parents)) {
741
            $items = DataObject::get($sourceObject)
742
                ->filter("ID", array_keys($parents));
743
            $parents = array();
744
745
            foreach ($items as $item) {
746
                if ($item->ParentID) {
747
                    $parents[$item->ParentID] = true;
748
                }
749
                $this->searchIds[$item->ID] = true;
750
                $this->searchExpanded[$item->ID] = true;
751
            }
752
        }
753
    }
754
755
    /**
756
     * Get the DataObjects that matches the searched parameter.
757
     *
758
     * @return DataList
759
     */
760
    protected function getSearchResults()
761
    {
762
        $callback = $this->getSearchFunction();
763
        if ($callback) {
764
            return call_user_func($callback, $this->getSourceObject(), $this->getLabelField(), $this->search);
765
        }
766
767
        $sourceObject = $this->getSourceObject();
768
        $filters = array();
769
        $sourceObjectInstance = DataObject::singleton($sourceObject);
770
        $candidates = array_unique([
771
            $this->getLabelField(),
772
            $this->getTitleField(),
773
            'Title',
774
            'Name'
775
        ]);
776
        foreach ($candidates as $candidate) {
777
            if ($sourceObjectInstance->hasDatabaseField($candidate)) {
778
                $filters["{$candidate}:PartialMatch"] = $this->search;
779
            }
780
        }
781
782
        if (empty($filters)) {
783
            throw new InvalidArgumentException(sprintf(
784
                'Cannot query by %s.%s, not a valid database column',
785
                $sourceObject,
786
                $this->getTitleField()
787
            ));
788
        }
789
        return DataObject::get($this->getSourceObject())->filterAny($filters);
790
    }
791
792
    /**
793
     * Get the object where the $keyField is equal to a certain value
794
     *
795
     * @param string|int $key
796
     * @return DataObject
797
     */
798
    protected function objectForKey($key)
799
    {
800
        return DataObject::get($this->getSourceObject())
801
            ->filter($this->getKeyField(), $key)
802
            ->first();
803
    }
804
805
    /**
806
     * Changes this field to the readonly field.
807
     */
808
    public function performReadonlyTransformation()
809
    {
810
        /** @var TreeDropdownField_Readonly $copy */
811
        $copy = $this->castedCopy(TreeDropdownField_Readonly::class);
812
        $copy->setKeyField($this->getKeyField());
813
        $copy->setLabelField($this->getLabelField());
814
        $this->setTitleField($this->getTitleField());
815
        $copy->setSourceObject($this->getSourceObject());
816
        return $copy;
817
    }
818
819
    /**
820
     * @param string|FormField $classOrCopy
821
     * @return FormField
822
     */
823
    public function castedCopy($classOrCopy)
824
    {
825
        $field = $classOrCopy;
826
827
        if (!is_object($field)) {
828
            $field = new $classOrCopy($this->name, $this->title, $this->getSourceObject());
829
        }
830
831
        return parent::castedCopy($field);
832
    }
833
834
    public function getSchemaStateDefaults()
835
    {
836
        $data = parent::getSchemaStateDefaults();
837
        /** @var Hierarchy|DataObject $record */
838
        $record = $this->Value() ? $this->objectForKey($this->Value()) : null;
839
840
        $data['data']['cacheKey'] = $this->getCacheKey();
841
        $data['data']['showSelectedPath'] = $this->getShowSelectedPath();
842
        if ($record) {
843
            $titlePath = '';
844
845
            if ($this->getShowSelectedPath()) {
846
                $ancestors = $record->getAncestors(true)->reverse();
847
848
                foreach ($ancestors as $parent) {
849
                    $title = $parent->obj($this->getTitleField())->getValue();
850
                    $titlePath .= $title .'/';
851
                }
852
            }
853
            $data['data']['valueObject'] = [
854
                'id' => $record->obj($this->getKeyField())->getValue(),
855
                'title' => $record->obj($this->getTitleField())->getValue(),
856
                'treetitle' => $record->obj($this->getLabelField())->getSchemaValue(),
857
                'titlePath' => $titlePath,
858
            ];
859
        }
860
861
        return $data;
862
    }
863
864
    /**
865
     * Ensure cache is keyed by last modified datetime of the underlying list.
866
     * Caches the key for the respective underlying list types, since it doesn't need to query again.
867
     *
868
     * @return DBDatetime
869
     */
870
    protected function getCacheKey()
871
    {
872
        $target = $this->getSourceObject();
873
        if (!isset(self::$cacheKeyCache[$target])) {
874
            self::$cacheKeyCache[$target] = DataList::create($target)->max('LastEdited');
0 ignored issues
show
$target of type string is incompatible with the type array expected by parameter $args of SilverStripe\View\ViewableData::create(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

874
            self::$cacheKeyCache[$target] = DataList::create(/** @scrutinizer ignore-type */ $target)->max('LastEdited');
Loading history...
875
        }
876
        return self::$cacheKeyCache[$target];
877
    }
878
879
    public function getSchemaDataDefaults()
880
    {
881
        $data = parent::getSchemaDataDefaults();
882
        $data['data'] = array_merge($data['data'], [
883
            'urlTree' => $this->Link('tree'),
884
            'showSearch' => $this->getShowSearch(),
885
            'emptyString' => $this->getEmptyString(),
886
            'hasEmptyDefault' => $this->getHasEmptyDefault(),
887
            'multiple' => false,
888
        ]);
889
890
        return $data;
891
    }
892
893
    /**
894
     * @param boolean $bool
895
     * @return self Self reference
896
     */
897
    public function setHasEmptyDefault($bool)
898
    {
899
        $this->hasEmptyDefault = $bool;
900
        return $this;
901
    }
902
903
    /**
904
     * @return bool
905
     */
906
    public function getHasEmptyDefault()
907
    {
908
        return $this->hasEmptyDefault;
909
    }
910
911
    /**
912
     * Set the default selection label, e.g. "select...".
913
     * Defaults to an empty string. Automatically sets
914
     * {@link $hasEmptyDefault} to true.
915
     *
916
     * @param string $string
917
     * @return $this
918
     */
919
    public function setEmptyString($string)
920
    {
921
        $this->setHasEmptyDefault(true);
922
        $this->emptyString = $string;
923
        return $this;
924
    }
925
926
    /**
927
     * @return string
928
     */
929
    public function getEmptyString()
930
    {
931
        if ($this->emptyString !== null) {
932
            return $this->emptyString;
933
        }
934
935
        $item = DataObject::singleton($this->getSourceObject());
936
        $emptyString = _t(
937
            'SilverStripe\\Forms\\DropdownField.CHOOSE_MODEL',
938
            '(Choose {name})',
939
            ['name' => $item->i18n_singular_name()]
940
        );
941
        return $emptyString;
942
    }
943
944
    /**
945
     * @return bool
946
     */
947
    public function getShowSelectedPath()
948
    {
949
        return $this->showSelectedPath;
950
    }
951
952
    /**
953
     * @param bool $showSelectedPath
954
     * @return TreeDropdownField
955
     */
956
    public function setShowSelectedPath($showSelectedPath)
957
    {
958
        $this->showSelectedPath = $showSelectedPath;
959
        return $this;
960
    }
961
}
962