Integrations::getAvailableItemActions()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

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