ModelAdmin::getEditForm()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 16
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 12
c 1
b 0
f 0
nc 1
nop 2
dl 0
loc 16
rs 9.8666
1
<?php
2
3
namespace LeKoala\Admini;
4
5
use SilverStripe\Forms\Form;
6
use SilverStripe\ORM\DataList;
7
use SilverStripe\ORM\ArrayList;
8
use SilverStripe\ORM\DataObject;
9
use SilverStripe\View\ArrayData;
10
use SilverStripe\Forms\FieldList;
11
use LeKoala\Tabulator\TabulatorGrid;
12
use SilverStripe\Control\Controller;
13
14
/**
15
 * Generates a three-pane UI for editing model classes, tabular results and edit forms.
16
 *
17
 * Relies on data such as {@link DataObject::$db} and {@link DataObject::getCMSFields()}
18
 * to scaffold interfaces "out of the box", while at the same time providing
19
 * flexibility to customize the default output.
20
 */
21
abstract class ModelAdmin extends LeftAndMain
22
{
23
    /**
24
     * @inheritdoc
25
     */
26
    private static $url_rule = '/$ModelClass/$Action';
27
28
    /**
29
     * List of all managed {@link DataObject}s in this interface.
30
     *
31
     * Simple notation with class names only:
32
     * <code>
33
     * array('MyObjectClass','MyOtherObjectClass')
34
     * </code>
35
     *
36
     * Extended notation with options (e.g. custom titles):
37
     * <code>
38
     * array(
39
     *   'MyObjectClass' => ['title' => "Custom title"]
40
     *   'urlslug' => ['title' => "Another title", 'dataClass' => MyNamespacedClass::class]
41
     * )
42
     * </code>
43
     *
44
     * Available options:
45
     * - 'title': Set custom titles for the tabs or dropdown names
46
     * - 'dataClass': The class name being managed. Defaults to the key. Useful for making shorter URLs or placing the same class in multiple tabs
47
     *
48
     * @config
49
     * @var array|string
50
     */
51
    private static $managed_models = null;
0 ignored issues
show
introduced by
The private property $managed_models is not used, and could be removed.
Loading history...
52
53
    /**
54
     * Override menu_priority so that ModelAdmin CMSMenu objects
55
     * are grouped together directly above the Help menu item.
56
     * @var float
57
     */
58
    private static $menu_priority = -0.5;
0 ignored issues
show
introduced by
The private property $menu_priority is not used, and could be removed.
Loading history...
59
60
    /**
61
     * @var string
62
     */
63
    private static $menu_icon = MaterialIcons::STAR;
0 ignored issues
show
introduced by
The private property $menu_icon is not used, and could be removed.
Loading history...
64
65
    private static $allowed_actions = array();
66
67
    private static $url_handlers = array(
0 ignored issues
show
introduced by
The private property $url_handlers is not used, and could be removed.
Loading history...
68
        '$ModelClass/$Action' => 'handleAction'
69
    );
70
71
    /**
72
     * @var string The {@link \SilverStripe\ORM\DataObject} sub-class being managed during this object's lifetime.
73
     */
74
    protected $modelClass;
75
76
    /**
77
     * @var string The {@link \SilverStripe\ORM\DataObject} the currently active model tab, and key of managed_models.
78
     */
79
    protected $modelTab;
80
81
    /**
82
     * @config
83
     * @var int Amount of results to show per page
84
     */
85
    private static $page_length = 15;
86
87
    /**
88
     * Initialize the model admin interface.
89
     *
90
     * Sets the `modelClass` field which determines which of the {@link DataObject} objects will have visible data. This
91
     * is determined by the URL (with the first slug being the name of the DataObject class to represent. If this class
92
     * is loaded without any URL, we pick the first DataObject from the list of {@link self::$managed_models}.
93
     */
94
    protected function init()
95
    {
96
        parent::init();
97
98
        $models = $this->getManagedModels();
99
        $this->modelTab = $this->getRequest()->param('ModelClass');
100
101
        // if we've hit the "landing" page
102
        if ($this->modelTab === null) {
103
            reset($models);
104
            $this->modelTab = key($models);
105
        }
106
107
        // security check for valid models
108
        if (!array_key_exists($this->modelTab, $models)) {
109
            // if it fails to match the string exactly, try reverse-engineering a classname
110
            $this->modelTab = $this->unsanitiseClassName($this->modelTab);
111
112
            if (!array_key_exists($this->modelTab, $models)) {
113
                throw new \RuntimeException(sprintf('ModelAdmin::init(): Invalid Model class %s', $this->modelTab));
114
            }
115
        }
116
117
        $this->modelClass = isset($models[$this->modelTab]['dataClass'])
118
            ? $models[$this->modelTab]['dataClass']
119
            : $this->modelTab;
120
    }
121
122
    /**
123
     * Overrides {@link \LeKoala\Admini\LeftAndMain} to ensure the active model class (the DataObject we are
124
     * currently viewing) is included in the URL.
125
     *
126
     * @inheritdoc
127
     */
128
    public function Link($action = null)
129
    {
130
        if (!$action) {
131
            $action = $this->sanitiseClassName($this->modelTab);
132
        }
133
        return parent::Link($action);
134
    }
135
136
    /**
137
     * @param int|null $id
138
     * @param \SilverStripe\Forms\FieldList $fields
139
     * @return \SilverStripe\Forms\Form A Form object with one tab per grid
140
     */
141
    public function getEditForm($id = null, $fields = null)
142
    {
143
        $form = Form::create(
144
            $this,
145
            'EditForm',
146
            new FieldList(),
147
            new FieldList()
148
        )->setHTMLID('Form_EditForm');
149
        $form->Fields()->push($this->getTabulatorGrid($form));
150
        $form->setTemplate($this->getTemplatesWithSuffix('_EditForm'));
151
        $editFormAction = Controller::join_links($this->Link($this->sanitiseClassName($this->modelTab)), 'EditForm');
152
        $form->setFormAction($editFormAction);
153
154
        $this->extend('updateEditForm', $form);
155
156
        return $form;
157
    }
158
159
    /**
160
     */
161
    protected function getTabulatorGrid(Form $form): TabulatorGrid
162
    {
163
        $grid = new TabulatorGrid(
164
            $this->sanitiseClassName($this->modelTab),
165
            false, // false = no label, null = default label
166
            $this->getList(),
167
        );
168
        $grid->setForm($form);
169
        $grid->setPageSize(self::config()->page_length);
170
171
        $grid->setGlobalSearch(true);
172
        $grid->wizardResponsiveCollapse(false, "hide");
173
        $cols = array_keys($grid->getColumns());
174
        foreach ($cols as $i => $col) {
175
            if (str_contains($col, 'action_')) {
176
                continue;
177
            }
178
            if (str_contains($col, 'ui_')) {
179
                continue;
180
            }
181
            // fitColumns needs a minWidth
182
            $grid->updateColumn($col, [
183
                'minWidth' => 100
184
            ]);
185
186
            if ($i === 0) {
187
                $grid->updateColumn($col, [
188
                    'responsive' => 0,
189
                    'minWidth' => 200,
190
                ]);
191
            }
192
        }
193
194
        $this->extend('updateTabulatorGrid', $grid);
195
        return $grid;
196
    }
197
198
    /**
199
     * You can override how ModelAdmin returns DataObjects by either overloading this method, or defining an extension
200
     * to ModelAdmin that implements the `updateList` method (and takes a {@link \SilverStripe\ORM\DataList} as the
201
     * first argument).
202
     *
203
     * For example, you might want to do this if this particular ModelAdmin should only ever show objects where an
204
     * Archived flag is set to false. That would be best done as an extension, for example:
205
     *
206
     * <code>
207
     * public function updateList(\SilverStripe\ORM\DataList $list)
208
     * {
209
     *     return $list->filter('Archived', false);
210
     * }
211
     * </code>
212
     *
213
     * @return \SilverStripe\ORM\DataList
214
     */
215
    public function getList()
216
    {
217
        $singl = singleton($this->modelClass);
218
        /** @var DataList $list */
219
        $list = $singl::get();
220
        $this->extend('updateList', $list);
221
222
        $config = $singl->config();
223
224
        // Sort by custom sort order if no filter is set
225
        if ($config->model_admin_sort) {
226
            $list = $list->sort($config->model_admin_sort);
227
        }
228
229
        return $list;
230
    }
231
232
    /**
233
     * The model managed by this instance.
234
     * See $managed_models for potential values.
235
     *
236
     * @return string
237
     */
238
    public function getModelClass()
239
    {
240
        return $this->modelClass;
241
    }
242
243
    /**
244
     * @return \SilverStripe\ORM\ArrayList An ArrayList of all managed models to build the tabs for this ModelAdmin
245
     */
246
    protected function getManagedModelTabs()
247
    {
248
        $models = $this->getManagedModels();
249
        $forms = new ArrayList();
250
251
        foreach ($models as $tab => $options) {
252
            $forms->push(new ArrayData(array(
253
                'Title' => $options['title'],
254
                'Tab' => $tab,
255
                // `getManagedModels` did not always return a `dataClass` attribute
256
                // Legacy behaviour is for `ClassName` to map to `$tab`
257
                'ClassName' => isset($options['dataClass']) ? $options['dataClass'] : $tab,
258
                'Link' => $this->Link($this->sanitiseClassName($tab)),
259
                'LinkOrCurrent' => ($tab == $this->modelTab) ? 'current' : 'link'
260
            )));
261
        }
262
263
        return $forms;
264
    }
265
266
    /**
267
     * Sanitise a model class' name for inclusion in a link
268
     *
269
     * @param string $class
270
     * @return string
271
     */
272
    protected function sanitiseClassName($class)
273
    {
274
        return str_replace('\\', '-', $class ?? '');
275
    }
276
277
    /**
278
     * Unsanitise a model class' name from a URL param
279
     *
280
     * @param string $class
281
     * @return string
282
     */
283
    protected function unsanitiseClassName($class)
284
    {
285
        return str_replace('-', '\\', $class ?? '');
286
    }
287
288
    /**
289
     * @return array Map of class name to an array of 'title' (see {@link $managed_models})
290
     */
291
    public function getManagedModels()
292
    {
293
        $models = $this->config()->get('managed_models');
294
        if (is_string($models)) {
295
            $models = array($models);
296
        }
297
        if (!$models || !count($models)) {
298
            throw new \RuntimeException(
299
                'ModelAdmin::getManagedModels():
300
				You need to specify at least one DataObject subclass in private static $managed_models.
301
				Make sure that this property is defined, and that its visibility is set to "private"'
302
            );
303
        }
304
305
        // Normalize models to have their model class in array key
306
        foreach ($models as $k => $v) {
307
            if (is_numeric($k)) {
308
                $models[$v] = ['dataClass' => $v, 'title' => singleton($v)->i18n_plural_name()];
309
                unset($models[$k]);
310
            } elseif (is_array($v) && !isset($v['dataClass'])) {
311
                $models[$k]['dataClass'] = $k;
312
            }
313
        }
314
315
        return $models;
316
    }
317
318
    /**
319
     * @param bool $unlinked
320
     * @return ArrayList
321
     */
322
    public function Breadcrumbs($unlinked = false)
323
    {
324
        $items = parent::Breadcrumbs($unlinked);
325
326
        // Show the class name rather than ModelAdmin title as root node
327
        $models = $this->getManagedModels();
328
        $params = $this->getRequest()->getVars();
329
        if (isset($params['url'])) {
330
            unset($params['url']);
331
        }
332
333
        $items[0]->Title = $models[$this->modelTab]['title'];
334
        $items[0]->Link = Controller::join_links(
335
            $this->Link($this->sanitiseClassName($this->modelTab)),
336
            '?' . http_build_query($params)
337
        );
338
339
        return $items;
340
    }
341
}
342