Completed
Push — develop ( a13565...8d3950 )
by Nate
09:35
created

Integrations::getItemActionHtml()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 16
c 0
b 0
f 0
ccs 0
cts 14
cp 0
rs 9.7333
cc 2
nc 2
nop 1
crap 6
1
<?php
2
3
/**
4
 * @copyright  Copyright (c) Flipbox Digital Limited
5
 * @license    https://github.com/flipboxfactory/craft-integration/blob/master/LICENSE
6
 * @link       https://github.com/flipboxfactory/craft-integration/
7
 */
8
9
namespace flipbox\craft\integration\fields;
10
11
use Craft;
12
use craft\base\Element;
13
use craft\base\ElementInterface;
14
use craft\base\Field;
15
use craft\elements\db\ElementQuery;
16
use craft\elements\db\ElementQueryInterface;
17
use craft\helpers\Component as ComponentHelper;
18
use craft\helpers\Db;
19
use craft\helpers\StringHelper;
20
use flipbox\craft\ember\helpers\ModelHelper;
21
use flipbox\craft\ember\records\ActiveRecord;
22
use flipbox\craft\ember\validators\MinMaxValidator;
23
use flipbox\craft\integration\events\RegisterIntegrationFieldActionsEvent;
24
use flipbox\craft\integration\fields\actions\IntegrationActionInterface;
25
use flipbox\craft\integration\fields\actions\IntegrationItemActionInterface;
26
use flipbox\craft\integration\queries\IntegrationAssociationQuery;
27
use flipbox\craft\integration\records\IntegrationAssociation;
28
use flipbox\craft\integration\web\assets\integrations\Integrations as IntegrationsAsset;
29
use yii\base\Exception;
30
use yii\helpers\ArrayHelper;
31
32
/**
33
 * @author Flipbox Factory <[email protected]>
34
 * @since 1.0.0
35
 */
36
abstract class Integrations extends Field
37
{
38
    use ModifyElementQueryTrait,
39
        NormalizeValueTrait;
40
41
    /**
42
     * The Plugin's translation category
43
     */
44
    const TRANSLATION_CATEGORY = '';
45
46
    /**
47
     * The action path to preform field actions
48
     */
49
    const ACTION_PREFORM_ACTION_PATH = '';
50
51
    /**
52
     * The action path to preform field actions
53
     */
54
    const ACTION_PREFORM_ITEM_ACTION_PATH = '';
55
56
    /**
57
     * The action path to associate an item
58
     */
59
    const ACTION_ASSOCIATION_ITEM_PATH = '';
60
61
    /**
62
     * The action path to dissociate an item
63
     */
64
    const ACTION_DISSOCIATION_ITEM_PATH = '';
65
66
    /**
67
     * The action path to create an integration object
68
     */
69
    const ACTION_CREATE_ITEM_PATH = '';
70
71
    /**
72
     * The action event name
73
     */
74
    const EVENT_REGISTER_ACTIONS = 'registerActions';
75
76
    /**
77
     * The action event name
78
     */
79
    const EVENT_REGISTER_AVAILABLE_ACTIONS = 'registerAvailableActions';
80
81
    /**
82
     * The item action event name
83
     */
84
    const EVENT_REGISTER_ITEM_ACTIONS = 'registerItemActions';
85
86
    /**
87
     * The item action event name
88
     */
89
    const EVENT_REGISTER_AVAILABLE_ITEM_ACTIONS = 'registerAvailableItemActions';
90
91
    /**
92
     * The input template path
93
     */
94
    const INPUT_TEMPLATE_PATH = '';
95
96
    /**
97
     * The input template path
98
     */
99
    const INPUT_ITEM_TEMPLATE_PATH = '_inputItem';
100
101
    /**
102
     * The input template path
103
     */
104
    const SETTINGS_TEMPLATE_PATH = '';
105
106
    /**
107
     * @var string
108
     */
109
    public $object;
110
111
    /**
112
     * @var int|null
113
     */
114
    public $min;
115
116
    /**
117
     * @var int|null
118
     */
119
    public $max;
120
121
    /**
122
     * @var string
123
     */
124
    public $viewUrl = '';
125
126
    /**
127
     * @var string
128
     */
129
    public $listUrl = '';
130
131
    /**
132
     * @var IntegrationActionInterface[]
133
     */
134
    public $selectedActions = [];
135
136
    /**
137
     * @var IntegrationItemActionInterface[]
138
     */
139
    public $selectedItemActions = [];
140
141
    /**
142
     * @var string|null
143
     */
144
    public $selectionLabel;
145
146
    /**
147
     * @inheritdoc
148
     */
149
    protected $defaultAvailableActions = [];
150
151
    /**
152
     * @inheritdoc
153
     */
154
    protected $defaultAvailableItemActions = [];
155
156
    /**
157
     * @return string
158
     */
159
    abstract public static function recordClass(): string;
160
161
    /**
162
     * @inheritdoc
163
     */
164
    public static function hasContentColumn(): bool
165
    {
166
        return false;
167
    }
168
169
    /**
170
     * @return string
171
     */
172
    public static function defaultSelectionLabel(): string
173
    {
174
        return Craft::t(static::TRANSLATION_CATEGORY, 'Add an Object');
175
    }
176
177
    /**
178
     * @return string
179
     */
180
    protected static function tableAlias(): string
181
    {
182
        /** @var ActiveRecord $recordClass */
183
        $recordClass = static::recordClass();
184
        return $recordClass::tableAlias();
185
    }
186
187
    /*******************************************
188
     * OBJECT
189
     *******************************************/
190
191
    /**
192
     * @return string
193
     */
194
    public function getObjectLabel(): string
195
    {
196
        return StringHelper::titleize($this->object);
197
    }
198
199
    /*******************************************
200
     * VALIDATION
201
     *******************************************/
202
203
    /**
204
     * @inheritdoc
205
     */
206
    public function getElementValidationRules(): array
207
    {
208
        return [
209
            [
210
                MinMaxValidator::class,
211
                'min' => $this->min ? (int)$this->min : null,
212
                'max' => $this->max ? (int)$this->max : null,
213
                'tooFew' => Craft::t(
214
                    static::TRANSLATION_CATEGORY,
215
                    '{attribute} should contain at least {min, number} {min, plural, one{domain} other{domains}}.'
216
                ),
217
                'tooMany' => Craft::t(
218
                    static::TRANSLATION_CATEGORY,
219
                    '{attribute} should contain at most {max, number} {max, plural, one{domain} other{domains}}.'
220
                ),
221
                'skipOnEmpty' => false
222
            ]
223
        ];
224
    }
225
226
    /*******************************************
227
     * RULES
228
     *******************************************/
229
230
    /**
231
     * @inheritdoc
232
     */
233
    public function rules()
234
    {
235
        return array_merge(
236
            parent::rules(),
237
            [
238
                [
239
                    'object',
240
                    'required',
241
                    'message' => Craft::t(static::TRANSLATION_CATEGORY, 'Object cannot be empty.')
242
                ],
243
                [
244
                    [
245
                        'object',
246
                        'min',
247
                        'max',
248
                        'viewUrl',
249
                        'listUrl',
250
                        'selectionLabel'
251
                    ],
252
                    'safe',
253
                    'on' => [
254
                        ModelHelper::SCENARIO_DEFAULT
255
                    ]
256
                ]
257
            ]
258
        );
259
    }
260
261
262
    /*******************************************
263
     * SEARCH
264
     *******************************************/
265
266
    /**
267
     * @param IntegrationAssociationQuery $value
268
     * @inheritdoc
269
     */
270
    public function getSearchKeywords($value, ElementInterface $element): string
271
    {
272
        $objects = [];
273
274
        /** @var IntegrationAssociation $association */
275
        foreach ($value->all() as $association) {
276
            array_push($objects, $association->objectId);
277
        }
278
279
        return parent::getSearchKeywords($objects, $element);
280
    }
281
282
283
    /*******************************************
284
     * VIEWS
285
     *******************************************/
286
287
    /**
288
     * @inheritdoc
289
     * @param IntegrationAssociationQuery $value
290
     * @throws \Twig_Error_Loader
291
     * @throws \yii\base\Exception
292
     */
293
    public function getInputHtml($value, ElementInterface $element = null): string
294
    {
295
        $value->limit(null);
296
297
        Craft::$app->getView()->registerAssetBundle(IntegrationsAsset::class);
298
299
        return Craft::$app->getView()->renderTemplate(
300
            static::INPUT_TEMPLATE_PATH,
301
            $this->inputHtmlVariables($value, $element)
302
        );
303
    }
304
305
    /**
306
     * @param IntegrationAssociationQuery $query
307
     * @param ElementInterface|null $element
308
     * @param bool $static
309
     * @return array
310
     * @throws \craft\errors\MissingComponentException
311
     * @throws \yii\base\InvalidConfigException
312
     */
313
    protected function inputHtmlVariables(
314
        IntegrationAssociationQuery $query,
315
        ElementInterface $element = null,
316
        bool $static = false
317
    ): array {
318
    
319
        return [
320
            'field' => $this,
321
            'element' => $element,
322
            'value' => $query,
323
            'objectLabel' => $this->getObjectLabel(),
324
            'static' => $static,
325
            'itemTemplate' => static::INPUT_ITEM_TEMPLATE_PATH,
326
            'settings' => [
327
                'translationCategory' => static::TRANSLATION_CATEGORY,
328
                'limit' => $this->max ? $this->max : null,
329
                'data' => [
330
                    'field' => $this->id,
331
                    'element' => $element ? $element->getId() : null
332
                ],
333
                'actions' => $this->getActionHtml($element),
334
                'actionAction' => static::ACTION_PREFORM_ACTION_PATH,
335
                'createItemAction' => static::ACTION_CREATE_ITEM_PATH,
336
                'itemData' => [
337
                    'field' => $this->id,
338
                    'element' => $element ? $element->getId() : null
339
                ],
340
                'itemSettings' => [
341
                    'translationCategory' => static::TRANSLATION_CATEGORY,
342
                    'actionAction' => static::ACTION_PREFORM_ITEM_ACTION_PATH,
343
                    'associateAction' => static::ACTION_ASSOCIATION_ITEM_PATH,
344
                    'dissociateAction' => static::ACTION_DISSOCIATION_ITEM_PATH,
345
                    'data' => [
346
                        'field' => $this->id,
347
                        'element' => $element ? $element->getId() : null
348
                    ],
349
                    'actions' => $this->getItemActionHtml($element),
350
                ]
351
            ]
352
        ];
353
    }
354
355
356
    /*******************************************
357
     * ACTIONS
358
     *******************************************/
359
360
    /**
361
     * @return IntegrationActionInterface[]
362
     * @throws \craft\errors\MissingComponentException
363
     * @throws \yii\base\InvalidConfigException
364
     */
365
    public function getAvailableActions(): array
366
    {
367
        $event = new RegisterIntegrationFieldActionsEvent([
368
            'actions' => $this->defaultAvailableActions
369
        ]);
370
371
        $this->trigger(
372
            static::EVENT_REGISTER_AVAILABLE_ACTIONS,
373
            $event
374
        );
375
376
        return $this->resolveActions(
377
            array_filter((array)$event->actions),
378
            IntegrationActionInterface::class
379
        );
380
    }
381
382
    /**
383
     * @param ElementInterface|null $element
384
     * @return IntegrationActionInterface[]
385
     * @throws \craft\errors\MissingComponentException
386
     * @throws \yii\base\InvalidConfigException
387
     */
388
    public function getActions(ElementInterface $element = null): array
389
    {
390
        $event = new RegisterIntegrationFieldActionsEvent([
391
            'actions' => $this->selectedActions,
392
            'element' => $element
393
        ]);
394
395
        $this->trigger(
396
            static::EVENT_REGISTER_ACTIONS,
397
            $event
398
        );
399
400
        return $this->resolveActions(
401
            array_filter((array)$event->actions),
402
            IntegrationActionInterface::class
403
        );
404
    }
405
406
    /**
407
     * @return IntegrationActionInterface[]
408
     * @throws \craft\errors\MissingComponentException
409
     * @throws \yii\base\InvalidConfigException
410
     */
411
    public function getAvailableItemActions(): array
412
    {
413
        $event = new RegisterIntegrationFieldActionsEvent([
414
            'actions' => $this->defaultAvailableItemActions
415
        ]);
416
417
        $this->trigger(
418
            static::EVENT_REGISTER_AVAILABLE_ITEM_ACTIONS,
419
            $event
420
        );
421
422
        return $this->resolveActions(
423
            array_filter((array)$event->actions),
424
            IntegrationItemActionInterface::class
425
        );
426
    }
427
428
    /**
429
     * @param ElementInterface|null $element
430
     * @return IntegrationItemActionInterface[]
431
     * @throws \craft\errors\MissingComponentException
432
     * @throws \yii\base\InvalidConfigException
433
     */
434
    public function getItemActions(ElementInterface $element = null): array
435
    {
436
        $event = new RegisterIntegrationFieldActionsEvent([
437
            'actions' => $this->selectedItemActions,
438
            'element' => $element
439
        ]);
440
441
        $this->trigger(
442
            static::EVENT_REGISTER_ITEM_ACTIONS,
443
            $event
444
        );
445
446
        return $this->resolveActions(
447
            array_filter((array)$event->actions),
448
            IntegrationItemActionInterface::class
449
        );
450
    }
451
452
    /**
453
     * @param array $actions
454
     * @param string $instance
455
     * @return array
456
     * @throws \craft\errors\MissingComponentException
457
     * @throws \yii\base\InvalidConfigException
458
     */
459
    protected function resolveActions(array $actions, string $instance)
460
    {
461
        foreach ($actions as $i => $action) {
462
            // $action could be a string or config array
463
            if (!$action instanceof $instance) {
464
                $actions[$i] = $action = ComponentHelper::createComponent($action, $instance);
465
466
                if ($actions[$i] === null) {
467
                    unset($actions[$i]);
468
                }
469
            }
470
        }
471
472
        return array_values($actions);
473
    }
474
475
    /**
476
     * @param ElementInterface|null $element
477
     * @return array
478
     * @throws \craft\errors\MissingComponentException
479
     * @throws \yii\base\InvalidConfigException
480
     */
481
    protected function getActionHtml(ElementInterface $element = null): array
482
    {
483
        $actionData = [];
484
485
        foreach ($this->getActions($element) as $action) {
486
            $actionData[] = [
487
                'type' => get_class($action),
488
                'destructive' => $action->isDestructive(),
489
                'name' => $action->getTriggerLabel(),
490
                'trigger' => $action->getTriggerHtml(),
491
                'confirm' => $action->getConfirmationMessage(),
492
            ];
493
        }
494
495
        return $actionData;
496
    }
497
498
    /**
499
     * @param ElementInterface|null $element
500
     * @return array
501
     * @throws \craft\errors\MissingComponentException
502
     * @throws \yii\base\InvalidConfigException
503
     */
504
    protected function getItemActionHtml(ElementInterface $element = null): array
505
    {
506
        $actionData = [];
507
508
        foreach ($this->getItemActions($element) as $action) {
509
            $actionData[] = [
510
                'type' => get_class($action),
511
                'destructive' => $action->isDestructive(),
512
                'name' => $action->getTriggerLabel(),
513
                'trigger' => $action->getTriggerHtml(),
514
                'confirm' => $action->getConfirmationMessage(),
515
            ];
516
        }
517
518
        return $actionData;
519
    }
520
521
522
523
    /*******************************************
524
     * EVENTS
525
     *******************************************/
526
527
    /**
528
     * @param ElementInterface $element
529
     * @param bool $isNew
530
     * @return bool|void
531
     * @throws \Throwable
532
     * @throws \yii\db\StaleObjectException
533
     */
534
    public function afterElementSave(ElementInterface $element, bool $isNew)
535
    {
536
        /** @var IntegrationAssociationQuery $query */
537
        $query = $element->getFieldValue($this->handle);
538
539
        $currentAssociations = [];
540
541
        if (!$isNew) {
542
            /** @var ActiveRecord $recordClass */
543
            $recordClass = static::recordClass();
544
545
            /** @var IntegrationAssociationQuery $existingQuery */
546
            $existingQuery = $recordClass::find();
547
            $existingQuery->element = $query->element;
548
            $existingQuery->field = $query->field;
549
            $existingQuery->site = $query->site;
550
            $existingQuery->indexBy = 'objectId';
551
552
            $currentAssociations = $existingQuery->all();
553
        }
554
555
        $success = true;
556
557
        if (null === ($records = $query->getCachedResult())) {
558
            foreach ($currentAssociations as $currentAssociation) {
559
                if (!$currentAssociation->delete()) {
560
                    $success = false;
561
                }
562
            }
563
564
            if (!$success) {
565
                $this->addError('types', 'Unable to dissociate object.');
566
                throw new Exception('Unable to dissociate object.');
567
            }
568
569
            parent::afterElementSave($element, $isNew);
570
        } else {
571
            $associations = [];
572
            $order = 1;
573
            foreach ($records as $record) {
574
                if (null === ($association = ArrayHelper::remove($currentAssociations, $record->objectId))) {
575
                    $association = $record;
576
                }
577
                $association->sortOrder = $order++;
578
                $associations[] = $association;
579
            }
580
581
            // DeleteOrganization those removed
582
            foreach ($currentAssociations as $currentAssociation) {
583
                if (!$currentAssociation->delete()) {
584
                    $success = false;
585
                }
586
            }
587
588
            foreach ($associations as $association) {
589
                if (!$association->save()) {
590
                    $success = false;
591
                }
592
            }
593
594
            if (!$success) {
595
                $this->addError('users', 'Unable to associate objects.');
596
                throw new Exception('Unable to associate objects.');
597
            }
598
599
            parent::afterElementSave($element, $isNew);
600
        }
601
    }
602
603
604
    /*******************************************
605
     * SETTINGS
606
     *******************************************/
607
608
    /**
609
     * @inheritdoc
610
     * @throws \Twig_Error_Loader
611
     * @throws \yii\base\Exception
612
     */
613
    public function getSettingsHtml()
614
    {
615
        return Craft::$app->getView()->renderTemplate(
616
            static::SETTINGS_TEMPLATE_PATH,
617
            $this->settingsHtmlVariables()
618
        );
619
    }
620
621
    /**
622
     * @return array
623
     * @throws \craft\errors\MissingComponentException
624
     * @throws \yii\base\InvalidConfigException
625
     */
626
    protected function settingsHtmlVariables(): array
627
    {
628
        return [
629
            'field' => $this,
630
            'availableActions' => $this->getAvailableActions(),
631
            'availableItemActions' => $this->getAvailableItemActions(),
632
            'translationCategory' => static::TRANSLATION_CATEGORY,
633
        ];
634
    }
635
636
    /**
637
     * @inheritdoc
638
     */
639
    public function settingsAttributes(): array
640
    {
641
        return array_merge(
642
            [
643
                'object',
644
                'min',
645
                'max',
646
                'viewUrl',
647
                'listUrl',
648
                'selectedActions',
649
                'selectedItemActions',
650
                'selectionLabel'
651
            ],
652
            parent::settingsAttributes()
653
        );
654
    }
655
}
656