Passed
Pull Request — 4.10 (#10211)
by Steve
10:44 queued 02:40
created

GridFieldAddExistingAutocompleter::setSearchFields()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\Forms\GridField;
4
5
use SilverStripe\Control\HTTPRequest;
6
use SilverStripe\Control\HTTPResponse;
7
use SilverStripe\Core\Config\Config;
8
use SilverStripe\Core\Convert;
9
use SilverStripe\Control\Controller;
10
use SilverStripe\Forms\FieldList;
11
use SilverStripe\Forms\TextField;
12
use SilverStripe\ORM\SS_List;
13
use SilverStripe\ORM\DataObject;
14
use SilverStripe\ORM\DataList;
15
use SilverStripe\ORM\Filters\SearchFilter;
16
use SilverStripe\View\ArrayData;
17
use SilverStripe\View\SSViewer;
18
use LogicException;
19
use SilverStripe\ORM\Filters\SearchFilter;
0 ignored issues
show
Bug introduced by
A parse error occurred: Cannot use SilverStripe\ORM\Filters\SearchFilter as SearchFilter because the name is already in use
Loading history...
20
21
/**
22
 * This class is is responsible for adding objects to another object's has_many
23
 * and many_many relation, as defined by the {@link RelationList} passed to the
24
 * {@link GridField} constructor.
25
 *
26
 * Objects can be searched through an input field (partially matching one or
27
 * more fields).
28
 *
29
 * Selecting from the results will add the object to the relation.
30
 *
31
 * Often used alongside {@link GridFieldDeleteAction} for detaching existing
32
 * records from a relationship.
33
 *
34
 * For easier setup, have a look at a sample configuration in
35
 * {@link GridFieldConfig_RelationEditor}.
36
 */
37
class GridFieldAddExistingAutocompleter implements GridField_HTMLProvider, GridField_ActionProvider, GridField_DataManipulator, GridField_URLHandler
38
{
39
40
    /**
41
     * The HTML fragment to write this component into
42
     */
43
    protected $targetFragment;
44
45
    /**
46
     * @var SS_List
47
     */
48
    protected $searchList;
49
50
    /**
51
     * Define column names which should be included in the search.
52
     * By default, they're searched with a {@link StartsWithFilter}.
53
     * To define custom filters, use the same notation as {@link DataList->filter()},
54
     * e.g. "Name:EndsWith".
55
     *
56
     * If multiple fields are provided, the filtering is performed non-exclusive.
57
     * If no fields are provided, tries to auto-detect fields from
58
     * {@link DataObject->searchableFields()}.
59
     *
60
     * The fields support "dot-notation" for relationships, e.g.
61
     * a entry called "Team.Name" will search through the names of
62
     * a "Team" relationship.
63
     *
64
     * @example
65
     *  array(
66
     *      'Name',
67
     *      'Email:StartsWith',
68
     *      'Team.Name'
69
     *  )
70
     *
71
     * @var array
72
     */
73
    protected $searchFields = [];
74
75
    /**
76
     * @var string SSViewer template to render the results presentation
77
     */
78
    protected $resultsFormat = '$Title';
79
80
    /**
81
     * @var string Text shown on the search field, instructing what to search for.
82
     */
83
    protected $placeholderText;
84
85
    /**
86
     * @var int
87
     */
88
    protected $resultsLimit = 20;
89
90
    /**
91
     *
92
     * @param string $targetFragment
93
     * @param array $searchFields Which fields on the object in the list should be searched
94
     */
95
    public function __construct($targetFragment = 'before', $searchFields = null)
96
    {
97
        $this->targetFragment = $targetFragment;
98
        $this->searchFields = (array)$searchFields;
99
    }
100
101
    /**
102
     *
103
     * @param GridField $gridField
104
     * @return string[] - HTML
105
     */
106
    public function getHTMLFragments($gridField)
107
    {
108
        $dataClass = $gridField->getModelClass();
109
110
        $forTemplate = new ArrayData([]);
111
        $forTemplate->Fields = new FieldList();
112
113
        $searchField = new TextField('gridfield_relationsearch', _t('SilverStripe\\Forms\\GridField\\GridField.RelationSearch', "Relation search"));
114
115
        $searchField->setAttribute('data-search-url', Controller::join_links($gridField->Link('search')));
116
        $searchField->setAttribute('placeholder', $this->getPlaceholderText($dataClass));
117
        $searchField->addExtraClass('relation-search no-change-track action_gridfield_relationsearch');
118
119
        $findAction = new GridField_FormAction(
120
            $gridField,
121
            'gridfield_relationfind',
122
            _t('SilverStripe\\Forms\\GridField\\GridField.Find', "Find"),
123
            'find',
124
            'find'
125
        );
126
        $findAction->setAttribute('data-icon', 'relationfind');
127
        $findAction->addExtraClass('action_gridfield_relationfind');
128
129
        $addAction = new GridField_FormAction(
130
            $gridField,
131
            'gridfield_relationadd',
132
            _t('SilverStripe\\Forms\\GridField\\GridField.LinkExisting', "Link Existing"),
133
            'addto',
134
            'addto'
135
        );
136
        $addAction->setAttribute('data-icon', 'chain--plus');
137
        $addAction->addExtraClass('btn btn-outline-secondary font-icon-link action_gridfield_relationadd');
138
139
        // If an object is not found, disable the action
140
        if (!is_int($gridField->State->GridFieldAddRelation(null))) {
141
            $addAction->setDisabled(true);
142
        }
143
144
        $forTemplate->Fields->push($searchField);
145
        $forTemplate->Fields->push($findAction);
146
        $forTemplate->Fields->push($addAction);
147
        if ($form = $gridField->getForm()) {
148
            $forTemplate->Fields->setForm($form);
149
        }
150
151
        $template = SSViewer::get_templates_by_class($this, '', __CLASS__);
152
        return [
153
            $this->targetFragment => $forTemplate->renderWith($template)
154
        ];
155
    }
156
157
    /**
158
     *
159
     * @param GridField $gridField
160
     * @return array
161
     */
162
    public function getActions($gridField)
163
    {
164
        return ['addto', 'find'];
165
    }
166
167
    /**
168
     * Manipulate the state to add a new relation
169
     *
170
     * @param GridField $gridField
171
     * @param string $actionName Action identifier, see {@link getActions()}.
172
     * @param array $arguments Arguments relevant for this
173
     * @param array $data All form data
174
     */
175
    public function handleAction(GridField $gridField, $actionName, $arguments, $data)
176
    {
177
        switch ($actionName) {
178
            case 'addto':
179
                if (isset($data['relationID']) && $data['relationID']) {
180
                    $gridField->State->GridFieldAddRelation = $data['relationID'];
181
                }
182
                break;
183
        }
184
    }
185
186
    /**
187
     * If an object ID is set, add the object to the list
188
     *
189
     * @param GridField $gridField
190
     * @param SS_List $dataList
191
     * @return SS_List
192
     */
193
    public function getManipulatedData(GridField $gridField, SS_List $dataList)
194
    {
195
        $objectID = $gridField->State->GridFieldAddRelation(null);
196
        if (empty($objectID)) {
197
            return $dataList;
198
        }
199
        $object = DataObject::get_by_id($gridField->getModelClass(), $objectID);
200
        if ($object) {
201
            $dataList->add($object);
202
        }
203
        $gridField->State->GridFieldAddRelation = null;
204
        return $dataList;
205
    }
206
207
    /**
208
     *
209
     * @param GridField $gridField
210
     * @return array
211
     */
212
    public function getURLHandlers($gridField)
213
    {
214
        return [
215
            'search' => 'doSearch',
216
        ];
217
    }
218
219
    /**
220
     * Returns a json array of a search results that can be used by for example Jquery.ui.autosuggestion
221
     *
222
     * @param GridField $gridField
223
     * @param HTTPRequest $request
224
     * @return string
225
     */
226
    public function doSearch($gridField, $request)
227
    {
228
        $searchStr = $request->getVar('gridfield_relationsearch');
229
        $dataClass = $gridField->getModelClass();
230
231
        $searchFields = ($this->getSearchFields())
232
            ? $this->getSearchFields()
233
            : $this->scaffoldSearchFields($dataClass);
234
        if (!$searchFields) {
235
            throw new LogicException(
236
                sprintf(
237
                    'GridFieldAddExistingAutocompleter: No searchable fields could be found for class "%s"',
238
                    $dataClass
239
                )
240
            );
241
        }
242
243
        $params = [];
244
        foreach ($searchFields as $searchField) {
245
            $name = (strpos($searchField, ':') !== false) ? $searchField : "$searchField:StartsWith";
246
            $params[$name] = $searchStr;
247
        }
248
249
        $results = null;
250
        if ($this->searchList) {
251
            // Assume custom sorting, don't apply default sorting
252
            $results = $this->searchList;
253
        } else {
254
            $results = DataList::create($dataClass)
255
                ->sort(strtok($searchFields[0], ':'), 'ASC');
256
        }
257
258
        // Apply baseline filtering and limits which should hold regardless of any customisations
259
        $results = $results
260
            ->subtract($gridField->getList())
261
            ->filterAny($params)
262
            ->limit($this->getResultsLimit());
263
264
        $json = [];
265
        Config::nest();
266
        SSViewer::config()->update('source_file_comments', false);
267
        $viewer = SSViewer::fromString($this->resultsFormat);
268
        foreach ($results as $result) {
269
            $title = Convert::html2raw($viewer->process($result));
270
            $json[] = [
271
                'label' => $title,
272
                'value' => $title,
273
                'id' => $result->ID,
274
            ];
275
        }
276
        Config::unnest();
277
        $response = new HTTPResponse(json_encode($json));
278
        $response->addHeader('Content-Type', 'application/json');
279
        return $response;
280
    }
281
282
    /**
283
     * @param string $format
284
     *
285
     * @return $this
286
     */
287
    public function setResultsFormat($format)
288
    {
289
        $this->resultsFormat = $format;
290
        return $this;
291
    }
292
293
    /**
294
     * @return string
295
     */
296
    public function getResultsFormat()
297
    {
298
        return $this->resultsFormat;
299
    }
300
301
    /**
302
     * Sets the base list instance which will be used for the autocomplete
303
     * search.
304
     *
305
     * @param SS_List $list
306
     */
307
    public function setSearchList(SS_List $list)
308
    {
309
        $this->searchList = $list;
310
        return $this;
311
    }
312
313
    /**
314
     * @param array $fields
315
     * @return $this
316
     */
317
    public function setSearchFields($fields)
318
    {
319
        $this->searchFields = $fields;
320
        return $this;
321
    }
322
323
    /**
324
     * @return array
325
     */
326
    public function getSearchFields()
327
    {
328
        return $this->searchFields;
329
    }
330
331
    /**
332
     * Detect searchable fields and searchable relations.
333
     * Falls back to {@link DataObject->summaryFields()} if
334
     * no custom search fields are defined.
335
     *
336
     * @param string $dataClass The class name
337
     * @return array|null names of the searchable fields
338
     */
339
    public function scaffoldSearchFields($dataClass)
340
    {
341
        $obj = DataObject::singleton($dataClass);
342
        $fields = null;
343
        if ($fieldSpecs = $obj->searchableFields()) {
344
            $customSearchableFields = $obj->config()->get('searchable_fields');
345
            foreach ($fieldSpecs as $name => $spec) {
346
                if (is_array($spec) && array_key_exists('filter', $spec)) {
347
                    // The searchableFields() spec defaults to PartialMatch,
348
                    // so we need to check the original setting.
349
                    // If the field is defined $searchable_fields = array('MyField'),
350
                    // then default to StartsWith filter, which makes more sense in this context.
351
                    if (!$customSearchableFields || array_search($name, $customSearchableFields) !== false) {
352
                        $filter = 'StartsWith';
353
                    } else {
354
                        $filterName = $spec['filter'];
355
                        // It can be an instance
356
                        if ($filterName instanceof SearchFilter) {
357
                            $filterName = get_class($filterName);
358
                        }
359
                        // It can be a fully qualified class name
360
                        if (strpos($filterName, '\\') !== false) {
361
                            $filterNameParts = explode("\\", $filterName);
362
                            // We expect an alias matching the class name without namespace, see #coresearchaliases
363
                            $filterName = array_pop($filterNameParts);
364
                        }
365
                        $filter = preg_replace('/Filter$/', '', $filterName);
366
                    }
367
                    $fields[] = "{$name}:{$filter}";
368
                } else {
369
                    $fields[] = $name;
370
                }
371
            }
372
        }
373
        if (is_null($fields)) {
374
            if ($obj->hasDatabaseField('Title')) {
375
                $fields = ['Title'];
376
            } elseif ($obj->hasDatabaseField('Name')) {
377
                $fields = ['Name'];
378
            }
379
        }
380
381
        return $fields;
382
    }
383
384
    /**
385
     * @param string $dataClass The class of the object being searched for
386
     *
387
     * @return string
388
     */
389
    public function getPlaceholderText($dataClass)
390
    {
391
        $searchFields = ($this->getSearchFields())
392
            ? $this->getSearchFields()
393
            : $this->scaffoldSearchFields($dataClass);
394
395
        if ($this->placeholderText) {
396
            return $this->placeholderText;
397
        } else {
398
            $labels = [];
399
            if ($searchFields) {
400
                foreach ($searchFields as $searchField) {
401
                    $searchField = explode(':', $searchField);
402
                    $label = singleton($dataClass)->fieldLabel($searchField[0]);
403
                    if ($label) {
404
                        $labels[] = $label;
405
                    }
406
                }
407
            }
408
            if ($labels) {
409
                return _t(
410
                    'SilverStripe\\Forms\\GridField\\GridField.PlaceHolderWithLabels',
411
                    'Find {type} by {name}',
412
                    ['type' => singleton($dataClass)->i18n_plural_name(), 'name' => implode(', ', $labels)]
413
                );
414
            } else {
415
                return _t(
416
                    'SilverStripe\\Forms\\GridField\\GridField.PlaceHolder',
417
                    'Find {type}',
418
                    ['type' => singleton($dataClass)->i18n_plural_name()]
419
                );
420
            }
421
        }
422
    }
423
424
    /**
425
     * @param string $text
426
     *
427
     * @return $this
428
     */
429
    public function setPlaceholderText($text)
430
    {
431
        $this->placeholderText = $text;
432
        return $this;
433
    }
434
435
    /**
436
     * Gets the maximum number of autocomplete results to display.
437
     *
438
     * @return int
439
     */
440
    public function getResultsLimit()
441
    {
442
        return $this->resultsLimit;
443
    }
444
445
    /**
446
     * @param int $limit
447
     *
448
     * @return $this
449
     */
450
    public function setResultsLimit($limit)
451
    {
452
        $this->resultsLimit = $limit;
453
        return $this;
454
    }
455
}
456