-
Notifications
You must be signed in to change notification settings - Fork 0
/
TrelloWire.module
378 lines (352 loc) · 15.9 KB
/
TrelloWire.module
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
<?php
namespace ProcessWire;
use ProcessWire\TrelloWire\TrelloWireApi;
class TrelloWire extends Wire implements Module
{
/** Parameters for the Trello API token request. */
public const TRELLO_API_PERMISSIONS = ['read', 'write'];
public const TRELLO_API_APP_NAME = 'TrelloWire';
public const TRELLO_API_TOKEN_EXPIRATION = 'never';
/** Setting values for card creation options. */
public const CREATE_NEVER = 'never';
public const CREATE_ON_ADDED = 'added';
public const CREATE_ON_PUBLISHED = 'published';
/** Setting values for status change handling options. */
public const STATUS_CHANGE_NO_ACTION = 'nothing';
public const STATUS_CHANGE_MOVE = 'move';
public const STATUS_CHANGE_ARCHIVE = 'archive';
public const STATUS_CHANGE_DELETE = 'delete';
/** @var string The key used to store the reference to the Trello card inside a page's meta data. */
public const CARD_REFERENCE_KEY = 'TrelloWireCardId';
/** @var string The name of the system log used by this module. */
public const LOG_NAME = 'trello-wire';
/** @var TrelloWireApi Stored TrelloWireApi instance. */
protected $api;
public static function getModuleInfo()
{
return [
'title' => __('Trello Wire'),
'summary' => __('A ProcessWire + Trello workflow module allowing you to create Trello cards based on pages.'),
'author' => "Moritz L'Hoest",
'href' => 'https://github.com/MoritzLost/TrelloWire',
'version' => '2.0.0',
'icon' => 'trello',
'autoload' => true,
'requires' => [
'ProcessWire>=3.0.167',
'PHP>=7.2',
],
'installs' => [
'TrelloWireCard',
],
];
}
/**
* The constructor registers the TrelloWire namespace used by this module.
*/
public function __construct()
{
$namespace = 'ProcessWire\\TrelloWire';
$classLoader = $this->wire('classLoader');
if (!$classLoader->hasNamespace($namespace)) {
$srcPath = $this->wire('config')->paths->get($this) . 'src/';
$classLoader->addNamespace($namespace, $srcPath);
}
}
/**
* Called during initialization, registers hooks to react to page changes.
*/
public function init()
{
// only add the hooks if the module is toggled active and both API key & token are set
if ($this->TrelloWireActive && $this->ApiKey && $this->ApiToken) {
// hook after a page is saved, this will handle all card & status updates
$this->addHookAfter('Pages::saved', $this, 'pageSavedHook');
// special case: create cards as soon as a page is added, this can't
// be determined inside the Pages::saved hook
if ($this->CardCreationTrigger === self::CREATE_ON_ADDED) {
$this->addHookAfter('Pages::added', $this, 'pageAddedHook');
}
// special case: handle page deletion
if ($this->StatusChangeDeleted !== self::STATUS_CHANGE_NO_ACTION) {
$this->addHookAfter('Pages::deleteReady', $this, 'pageDeleteReadyHook');
}
}
}
/**
* Get an instance of the TrelloWireApi class using the API key and token
* set through the module configuration. This will return null if the API
* key or token are missing.
*
* @return TrelloWireApi|null
*/
public function api(): ?TrelloWireApi
{
if (!$this->api) {
if (!$this->ApiKey || !$this->ApiToken) {
return null;
}
$this->api = new TrelloWireApi($this->ApiKey, $this->ApiToken);
}
return $this->api;
}
public function pageSavedHook(HookEvent $e): void
{
$page = $e->arguments(0);
if (!in_array($page->template->name, $this->TrelloWireTemplates)) return;
$card = $this->buildCardData($page);
$isExistingCard = !empty($card->id);
// find all status changes
$prevStatus = $page->statusPrevious;
$statusChanged = null !== $prevStatus;
$wasPublished = $statusChanged && !$page->isUnpublished() && (($prevStatus & Page::statusUnpublished) === Page::statusUnpublished);
$wasUnpublished = $statusChanged && $page->isUnpublished() && !($prevStatus & Page::statusUnpublished);
$wasHidden = $statusChanged && $page->isHidden() && !($prevStatus & Page::statusHidden);
$wasUnhidden = $statusChanged && !$page->isHidden() && (($prevStatus & Page::statusHidden) === Page::statusHidden);
$wasTrashed = $statusChanged && $page->isTrash() && !($prevStatus & Page::statusTrash);
$wasRestored = $statusChanged && !$page->isTrash() && (($prevStatus & Page::statusTrash) === Page::statusTrash);
if (!$isExistingCard) {
// if the card doesn't already exist on trello, and the module is
// set to create cards upon publishing, do so now
if ($wasPublished && $this->CardCreationTrigger === self::CREATE_ON_PUBLISHED) {
$this->createCardForPage($card, $page);
}
// early return: all other updates only apply only to existing cards
return;
}
// update existing cards
if ($this->CardUpdate && !$wasTrashed && !$wasRestored) {
$response = $this->trelloUpdateCard($card);
if (!$response) {
$page->message($this->_('The card associated with this page could not be updated. It may have been deleted manually.'));
} else {
$page->message($this->_('Successfully updated the Trello card associated with this page.'));
}
}
// perform the selected action after status changes
foreach (['Hidden' => $wasHidden, 'Unpublished' => $wasUnpublished, 'Trashed' => $wasTrashed] as $change => $wasChanged) {
if (!$wasChanged) continue;
$actionSettingKey = "StatusChange{$change}";
switch ($this->{$actionSettingKey}) {
case self::STATUS_CHANGE_ARCHIVE:
$this->api()->archiveCard($card->id);
break;
case self::STATUS_CHANGE_DELETE:
$this->deleteCardForPage($card, $page);
break;
case self::STATUS_CHANGE_MOVE:
$targetSettingKey = "MoveListTarget{$change}";
$targetListId = $this->{$targetSettingKey};
if ($targetListId) {
$this->api()->moveCard($card->id, $targetListId);
}
break;
case self::STATUS_CHANGE_NO_ACTION:
default:
// no action
}
}
// restore archived cards after status changes were reverted, if that setting is active
foreach (['Hidden' => $wasUnhidden, 'Unpublished' => $wasPublished, 'Trashed' => $wasRestored] as $change => $wasChanged) {
if (!$wasChanged) continue;
$restoreActionSettingKey = "RestoreOnReverse{$change}";
$restoreCard = $this->{$restoreActionSettingKey};
if (!$restoreCard) continue;
$this->api()->restoreCard($card->id);
}
}
public function pageAddedHook(HookEvent $e): void
{
$page = $e->arguments(0);
if (!in_array($page->template->name, $this->TrelloWireTemplates)) return;
// this hook is only added during initialization if the module is set
// to create card upon adding pages, so we don't need to check this again
$this->createCardForPage($this->buildCardData($page), $page);
}
public function pageDeleteReadyHook(HookEvent $e): void
{
$page = $e->arguments(0);
if (!in_array($page->template->name, $this->TrelloWireTemplates)) return;
$card = $this->buildCardData($page);
if (!$card->id) return;
switch ($this->StatusChangeDeleted) {
case self::STATUS_CHANGE_ARCHIVE:
$this->api()->archiveCard($card->id);
break;
case self::STATUS_CHANGE_DELETE:
$this->deleteCardForPage($card, $page);
break;
case self::STATUS_CHANGE_MOVE:
$targetListId = $this->MoveListTargetDeleted;
if ($targetListId) {
$this->api()->moveCard($card->id, $targetListId);
}
break;
case self::STATUS_CHANGE_NO_ACTION:
default:
// no action
}
}
/**
* Construct a new TrelloWireCard instance based on the passed page using the
* module settings.
* Hook after this if you want to change how Trello card data is
* generated for pages. The method will automatically set the card's ID if
* the page already has a reference to an existing card, so you can check the
* card's ID to determine whether this card will be used to create or update
* card.
* This method is called in multiple places, whenever a TrelloWire hook needs
* to extract data from a page based on the module configuration.
*
* @param Page $page The page this card will belong to.
* @return TrelloWireCard The card object with values based on the page.
*/
public function ___buildCardData(Page $page): TrelloWireCard
{
$defaultSettings = (new TrelloWireConfig())->getDefaults();
$TrelloWireCard = $this->modules->get('TrelloWireCard');
$TrelloWireCard->setPage($page);
$TrelloWireCard->setList($this->TargetList);
$titleField = !empty($this->CardTitle) ? $this->CardTitle : $defaultSettings['CardTitle'];
$TrelloWireCard->setTitle(wirePopulateStringTags($titleField, $page, ['entityDecode' => true]));
$cardBody = !empty($this->CardBody) ? wirePopulateStringTags($this->CardBody, $page, ['entityDecode' => true]) : '';
$TrelloWireCard->setBody($cardBody);
// for existing cards (card id stored in page settings), restore the id
$existingCardId = $page->meta(self::CARD_REFERENCE_KEY);
if ($existingCardId) {
$TrelloWireCard->setId($existingCardId);
}
if (!empty($this->CardLabels)) {
$TrelloWireCard->setLabels($this->CardLabels);
}
return $TrelloWireCard;
}
/**
* Creates a card on Trello based on the data contained in the passed
* TrelloWireCard. It also creates a new checklist on this card based on the
* module settings (Checklist title & items). Those settings are retrieved
* through the corresponding helper methods so you can modify or remove
* them by hooking those methods.
* After the card is created, the module will also store it's ID in the meta
* data of the passed $page, so it can be updated later.
* This method is called when the module wants to create a Trello card after
* a new page is created or published (depeding on the *Card Creation Trigger*
* setting). You can hook before this to modify the card before it is posted
* to Trello or abort the process entirely. Or hook after this to add further
* content to the card using the API.
*
* @param TrelloWireCard $card The card instance with the data for this card.
* @param Page $page The page this card belongs to.
* @return void
*/
public function ___createCardForPage(TrelloWireCard $card, Page $page)
{
$response = $this->trelloCreateCard($card);
if (!$response) {
return $page->error($this->_('An error occured while attempting to create a new card on Trello.'));
}
$page->message($this->_('Successfully created a new Trello card for this page!'));
// save the card ID to the page meta data so it can be updated later
$page->meta(self::CARD_REFERENCE_KEY, $response->id);
$checklistItems = $this->getDefaultChecklistItems($page);
if (!empty($checklistItems)) {
$checklistTitle = $this->getDefaultChecklistTitle($page);
$checklist = $this->api()->addChecklistToCard($response->id, $checklistTitle);
if ($checklist && $checklist->id) {
foreach ($checklistItems as $item) {
$this->api()->addItemToChecklist($checklist->id, $item);
}
}
}
}
/**
* Delete a card associated wih a page from Trello. Because the card no longer
* exists after this, the reference stored inside $page->meta() is removed as
* well.
*
* @param TrelloWireCard $card The card instance holding the ID for the card to delete.
* @param Page $page The page the card belongs to.
* @return void
*/
public function ___deleteCardForPage(TrelloWireCard $card, Page $page)
{
if (!$card->id) return;
if (!$api = $this->api()) return;
$deleteSuccess = $api->deleteCard($card->id);
if ($deleteSuccess) {
$page->meta()->remove(self::CARD_REFERENCE_KEY);
$page->message($this->_('Successfully deleted the card associated with this page, and removed the reference to it.'));
} else {
$page->message($this->_('The card associated with this page could not be deleted. It may have been deleted manually.'));
}
}
/**
* Get the default title for the checklist added to new Trello cards. It
* receives the page the card belongs to as an argument so you can hook this
* method and modify the return value based on the page.
*
* @param Page $page The page the card being currently created is based on.
* @return string
*/
public function ___getDefaultChecklistTitle(Page $page): string
{
return wirePopulateStringTags(
$this->CardChecklistTitle ?: (new TrelloWireConfig())->getDefaults()['CardChecklistTitle'],
$page,
['entityDecode' => true]
);
}
/**
* Get a list of items to add to the checklist added to new Trello cards. It
* received the page the card belongs to as an argument so you can hook this
* method and modify the return value based on the page.
*
* @param Page $page The page the card being currently created is based on.
* @return array
*/
public function ___getDefaultChecklistItems(Page $page): array
{
if (!$this->CardChecklistItems) return [];
return array_map(
function ($item) use ($page) {
return wirePopulateStringTags($item, $page, ['entityDecode' => true]);
},
array_filter(
preg_split('/[\n\r]+/', $this->CardChecklistItems),
function ($item) {
return !empty($item);
}
)
);
}
/**
* Create a new Trello card through the API based on a TrelloWireCard instance.
*
* @param TrelloWireCard $card The card with the values to post to Trello.
* @return object|bool Returns the card object returned from the API, or false on failure.
*/
public function ___trelloCreateCard(TrelloWireCard $card)
{
if (!$card->list) return false;
if (!$api = $this->api()) return null;
return $api->createCard($card->list, $card->title, $card->body, array_filter([
'idLabels' => $card->labels ? implode(',', $card->labels) : null,
]));
}
/**
* Update an existing Trello card's title and body with values from the
* TrelloWireCard instance.
*
* @param TrelloWireCard $card The card with a values to update on Trello. Must contain an ID property referencing an existing card.
* @return object|bool Returns the card object returned from the API, or false on failure.
*/
public function ___trelloUpdateCard(TrelloWireCard $card)
{
if (!$card->id) return false;
if (!$api = $this->api()) return null;
return $api->updateCard($card->id, [
'name' => $card->title,
'desc' => $card->body
]);
}
}