@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.)
hq.recaptime.dev/wiki/Phorge
phorge
phabricator
1<?php
2
3final class PhabricatorProjectBoardViewController
4 extends PhabricatorProjectBoardController {
5
6 public function shouldAllowPublic() {
7 return true;
8 }
9
10 /**
11 * @return AphrontResponse|PhabricatorStandardPageView
12 */
13 public function handleRequest(AphrontRequest $request) {
14 $viewer = $request->getUser();
15
16 $response = $this->loadProject();
17 if ($response) {
18 return $response;
19 }
20
21 $project = $this->getProject();
22 $state = $this->getViewState();
23 $board_uri = $project->getWorkboardURI();
24
25 $search_engine = $state->getSearchEngine();
26 $query_key = $state->getQueryKey();
27 $saved = $state->getSavedQuery();
28 if (!$saved) {
29 return new Aphront404Response();
30 }
31
32 if ($saved->getID()) {
33 $custom_query = $saved;
34 } else {
35 $custom_query = null;
36 }
37
38 $columns = array();
39 $board_phid = $project->getPHID();
40
41 if ($project->getHasWorkboard()) {
42 $layout_engine = $state->getLayoutEngine();
43 $columns = $layout_engine->getColumns($board_phid);
44 } else {
45 // Avoid setting up from scratch if board existed but got disabled
46 $columns = id(new PhabricatorProjectColumnQuery())
47 ->setViewer($viewer)
48 ->withProjectPHIDs(array($project->getPHID()))
49 ->withIsProxyColumn(false)
50 ->execute();
51 }
52
53 if (!$columns || !$project->getHasWorkboard()) {
54 $has_normal_columns = false;
55
56 foreach ($columns as $column) {
57 if (!$column->getProxyPHID()) {
58 $has_normal_columns = true;
59 break;
60 }
61 }
62
63 $can_edit = PhabricatorPolicyFilter::hasCapability(
64 $viewer,
65 $project,
66 PhabricatorPolicyCapability::CAN_EDIT);
67
68 if (!$has_normal_columns) {
69 if (!$can_edit) {
70 $content = $this->buildNoAccessContent($project);
71 } else {
72 $content = $this->buildInitializeContent($project);
73 }
74 } else {
75 if (!$can_edit) {
76 $content = $this->buildDisabledContent($project);
77 } else {
78 $content = $this->buildEnableContent($project);
79 }
80 }
81
82 if ($content instanceof AphrontResponse) {
83 return $content;
84 }
85
86 $nav = $this->newNavigation(
87 $project,
88 PhabricatorProject::ITEM_WORKBOARD);
89
90 $crumbs = $this->buildApplicationCrumbs();
91 $crumbs->addTextCrumb(pht('Workboard'));
92
93 return $this->newPage()
94 ->setTitle(
95 array(
96 $project->getDisplayName(),
97 pht('Workboard'),
98 ))
99 ->setNavigation($nav)
100 ->setCrumbs($crumbs)
101 ->appendChild($content);
102 }
103
104 $tasks = $state->getObjects();
105
106 $task_can_edit_map = id(new PhabricatorPolicyFilter())
107 ->setViewer($viewer)
108 ->requireCapabilities(array(PhabricatorPolicyCapability::CAN_EDIT))
109 ->apply($tasks);
110
111 $board_id = celerity_generate_unique_node_id();
112
113 $board = id(new PHUIWorkboardView())
114 ->setViewer($viewer)
115 ->setID($board_id)
116 ->addSigil('jx-workboard')
117 ->setMetadata(
118 array(
119 'boardPHID' => $project->getPHID(),
120 ));
121
122 $visible_columns = array();
123 $column_phids = array();
124 $visible_phids = array();
125 foreach ($columns as $column) {
126 if (!$state->getShowHidden()) {
127 if ($column->isHidden()) {
128 continue;
129 }
130 }
131
132 $proxy = $column->getProxy();
133 if ($proxy && !$proxy->isMilestone()) {
134 // TODO: For now, don't show subproject columns because we can't
135 // handle tasks with multiple positions yet.
136 continue;
137 }
138
139 $task_phids = $layout_engine->getColumnObjectPHIDs(
140 $board_phid,
141 $column->getPHID());
142
143 $column_tasks = array_select_keys($tasks, $task_phids);
144 $column_phid = $column->getPHID();
145
146 $visible_columns[$column_phid] = $column;
147 $column_phids[$column_phid] = $column_tasks;
148
149 foreach ($column_tasks as $phid => $task) {
150 $visible_phids[$phid] = $phid;
151 }
152 }
153
154 $container_phids = $state->getBoardContainerPHIDs();
155
156 $rendering_engine = id(new PhabricatorBoardRenderingEngine())
157 ->setViewer($viewer)
158 ->setObjects(array_select_keys($tasks, $visible_phids))
159 ->setEditMap($task_can_edit_map)
160 ->setExcludedProjectPHIDs($container_phids);
161
162 $templates = array();
163 $all_tasks = array();
164 $column_templates = array();
165 $sounds = array();
166 foreach ($visible_columns as $column_phid => $column) {
167 $column_tasks = $column_phids[$column_phid];
168
169 $panel = id(new PHUIWorkpanelView())
170 ->setHeader($column->getDisplayName())
171 ->setSubHeader($column->getDisplayType())
172 ->addSigil('workpanel');
173
174 $proxy = $column->getProxy();
175 if ($proxy) {
176 $proxy_id = $proxy->getID();
177 $href = $this->getApplicationURI("view/{$proxy_id}/");
178 $panel->setHref($href);
179 }
180
181 $header_icon = $column->getHeaderIcon();
182 if ($header_icon) {
183 $panel->setHeaderIcon($header_icon);
184 }
185
186 $display_class = $column->getDisplayClass();
187 if ($display_class) {
188 $panel->addClass($display_class);
189 }
190
191 if ($column->isHidden()) {
192 $panel->addClass('project-panel-hidden');
193 }
194
195 $column_menu = $this->buildColumnMenu($project, $column);
196 $panel->addHeaderAction($column_menu);
197
198 if ($column->canHaveTrigger()) {
199 $trigger = $column->getTrigger();
200 if ($trigger) {
201 $trigger->setViewer($viewer);
202 }
203
204 $trigger_menu = $this->buildTriggerMenu($column);
205 $panel->addHeaderAction($trigger_menu);
206 }
207
208 $column_points_tip = pht('Tasks');
209 $point_limit = $column->getPointLimit();
210 if (ManiphestTaskPoints::getIsEnabled()) {
211 $column_points_tip .= ' | '.pht('Points');
212 if ($point_limit !== null) {
213 $column_points_tip .= ' / '.pht('Point Limit');
214 }
215 } else if ($point_limit !== null) {
216 $column_points_tip .= ' / '.pht('Count Limit');
217 }
218
219 $count_tag = id(new PHUITagView())
220 ->setType(PHUITagView::TYPE_SHADE)
221 ->setColor(PHUITagView::COLOR_BLUE)
222 ->addSigil('column-points')
223 ->addSigil('has-tooltip')
224 ->setName(
225 javelin_tag(
226 'span',
227 array(
228 'sigil' => 'column-points-content',
229 ),
230 pht('-')))
231 ->setMetadata(
232 array(
233 'tip' => $column_points_tip,
234 'align' => 'S',
235 ))
236 ->setStyle('display: none');
237
238 $panel->setHeaderTag($count_tag);
239
240 $cards = id(new PHUIObjectItemListView())
241 ->setViewer($viewer)
242 ->setFlush(true)
243 ->setAllowEmptyList(true)
244 ->addSigil('project-column')
245 ->setItemClass('phui-workcard')
246 ->setMetadata(
247 array(
248 'columnPHID' => $column->getPHID(),
249 'pointLimit' => $column->getPointLimit(),
250 ));
251
252 $card_phids = array();
253 foreach ($column_tasks as $task) {
254 $object_phid = $task->getPHID();
255
256 $card = $rendering_engine->renderCard($object_phid);
257 $templates[$object_phid] = hsprintf('%s', $card->getItem());
258 $card_phids[] = $object_phid;
259
260 $all_tasks[$object_phid] = $task;
261 }
262
263 $panel->setCards($cards);
264 $board->addPanel($panel);
265
266 $drop_effects = $column->getDropEffects();
267 $drop_effects = mpull($drop_effects, 'toDictionary');
268
269 $preview_effect = null;
270 if ($column->canHaveTrigger()) {
271 $trigger = $column->getTrigger();
272 if ($trigger) {
273 $preview_effect = $trigger->getPreviewEffect()
274 ->toDictionary();
275
276 foreach ($trigger->getSoundEffects() as $sound) {
277 $sounds[] = $sound;
278 }
279 }
280 }
281
282 $column_templates[] = array(
283 'columnPHID' => $column_phid,
284 'effects' => $drop_effects,
285 'cardPHIDs' => $card_phids,
286 'triggerPreviewEffect' => $preview_effect,
287 );
288 }
289
290 $order_key = $state->getOrder();
291
292 $ordering_map = PhabricatorProjectColumnOrder::getEnabledOrders();
293 $ordering = id(clone $ordering_map[$order_key])
294 ->setViewer($viewer);
295
296 $headers = $ordering->getHeadersForObjects($all_tasks);
297 $headers = mpull($headers, 'toDictionary');
298
299 $vectors = $ordering->getSortVectorsForObjects($all_tasks);
300 $vector_map = array();
301 foreach ($vectors as $task_phid => $vector) {
302 $vector_map[$task_phid][$order_key] = $vector;
303 }
304
305 $header_keys = $ordering->getHeaderKeysForObjects($all_tasks);
306
307 $order_maps = array();
308 $order_maps[] = $ordering->toDictionary();
309
310 $properties = array();
311 foreach ($all_tasks as $task) {
312 $properties[$task->getPHID()] =
313 PhabricatorBoardResponseEngine::newTaskProperties($task);
314 }
315
316 $behavior_config = array(
317 'moveURI' => $this->getApplicationURI('move/'.$project->getID().'/'),
318 'uploadURI' => '/file/dropupload/',
319 'coverURI' => $this->getApplicationURI('cover/'),
320 'reloadURI' => phutil_string_cast($state->newWorkboardURI('reload/')),
321 'chunkThreshold' => PhabricatorFileStorageEngine::getChunkThreshold(),
322 'pointsEnabled' => ManiphestTaskPoints::getIsEnabled(),
323
324 'boardPHID' => $project->getPHID(),
325 'order' => $state->getOrder(),
326 'orders' => $order_maps,
327 'headers' => $headers,
328 'headerKeys' => $header_keys,
329 'templateMap' => $templates,
330 'orderMaps' => $vector_map,
331 'propertyMaps' => $properties,
332 'columnTemplates' => $column_templates,
333
334 'boardID' => $board_id,
335 'projectPHID' => $project->getPHID(),
336 'preloadSounds' => $sounds,
337 );
338 $this->initBehavior('project-boards', $behavior_config);
339
340 $sort_menu = $this->buildSortMenu(
341 $viewer,
342 $project,
343 $state->getOrder(),
344 $ordering_map);
345
346 $filter_menu = $this->buildFilterMenu(
347 $viewer,
348 $project,
349 $custom_query,
350 $search_engine,
351 $query_key);
352
353 $manage_menu = $this->buildManageMenu($project, $state->getShowHidden());
354
355 $header_link = phutil_tag(
356 'a',
357 array(
358 'href' => $this->getApplicationURI('profile/'.$project->getID().'/'),
359 ),
360 $project->getName());
361
362 $board_box = id(new PHUIBoxView())
363 ->appendChild($board)
364 ->addClass('project-board-wrapper');
365
366 $nav = $this->newNavigation(
367 $project,
368 PhabricatorProject::ITEM_WORKBOARD);
369
370 $divider = id(new PHUIListItemView())
371 ->setType(PHUIListItemView::TYPE_DIVIDER);
372 $fullscreen = $this->buildFullscreenMenu();
373
374 $crumbs = $this->newWorkboardCrumbs();
375 $crumbs->addTextCrumb(pht('Workboard'));
376 $crumbs->setBorder(true);
377
378 $crumbs->addAction($sort_menu);
379 $crumbs->addAction($filter_menu);
380 $crumbs->addAction($divider);
381 $crumbs->addAction($manage_menu);
382 $crumbs->addAction($fullscreen);
383
384 $page = $this->newPage()
385 ->setTitle(
386 array(
387 $project->getDisplayName(),
388 pht('Workboard'),
389 ))
390 ->setPageObjectPHIDs(array($project->getPHID()))
391 ->setShowFooter(false)
392 ->setNavigation($nav)
393 ->setCrumbs($crumbs)
394 ->addQuicksandConfig(
395 array(
396 'boardConfig' => $behavior_config,
397 ))
398 ->appendChild(
399 array(
400 $board_box,
401 ));
402
403 $background = $project->getDisplayWorkboardBackgroundColor();
404 require_celerity_resource('phui-workboard-color-css');
405 if ($background !== null) {
406 $background_color_class = "phui-workboard-{$background}";
407
408 $page->addClass('phui-workboard-color');
409 $page->addClass($background_color_class);
410 } else {
411 $page->addClass('phui-workboard-no-color');
412 }
413
414 return $page;
415 }
416
417 /**
418 * @return PHUIListItemView
419 */
420 private function buildSortMenu(
421 PhabricatorUser $viewer,
422 PhabricatorProject $project,
423 $sort_key,
424 array $ordering_map) {
425
426 $state = $this->getViewState();
427 $base_uri = $state->newWorkboardURI();
428
429 $items = array();
430 foreach ($ordering_map as $key => $ordering) {
431 // TODO: It would be desirable to build a real "PHUIIconView" here, but
432 // the pathway for threading that through all the view classes ends up
433 // being fairly complex, since some callers read the icon out of other
434 // views. For now, just stick with a string.
435 $ordering_icon = $ordering->getMenuIconIcon();
436 $ordering_name = $ordering->getDisplayName();
437
438 $is_selected = ($key === $sort_key);
439 if ($is_selected) {
440 $active_name = $ordering_name;
441 $active_icon = $ordering_icon;
442 }
443
444 $item = id(new PhabricatorActionView())
445 ->setIcon($ordering_icon)
446 ->setSelected($is_selected)
447 ->setName($ordering_name);
448
449 $uri = $base_uri->alter('order', $key);
450 $item->setHref($uri);
451
452 $items[] = $item;
453 }
454
455 $id = $project->getID();
456
457 $save_uri = $state->newWorkboardURI('default/sort/');
458
459 $can_edit = PhabricatorPolicyFilter::hasCapability(
460 $viewer,
461 $project,
462 PhabricatorPolicyCapability::CAN_EDIT);
463
464 $items[] = id(new PhabricatorActionView())
465 ->setType(PhabricatorActionView::TYPE_DIVIDER);
466
467 $items[] = id(new PhabricatorActionView())
468 ->setIcon('fa-floppy-o')
469 ->setName(pht('Save as Default'))
470 ->setHref($save_uri)
471 ->setWorkflow(true)
472 ->setDisabled(!$can_edit);
473
474 $sort_menu = id(new PhabricatorActionListView())
475 ->setViewer($viewer);
476 foreach ($items as $item) {
477 $sort_menu->addAction($item);
478 }
479
480 $sort_button = id(new PHUIListItemView())
481 ->setName($active_name)
482 ->setIcon($active_icon)
483 ->setHref('#')
484 ->addSigil('boards-dropdown-menu')
485 ->setMetadata(
486 array(
487 'items' => hsprintf('%s', $sort_menu),
488 ));
489
490 return $sort_button;
491 }
492
493 /**
494 * @return PHUIListItemView
495 */
496 private function buildFilterMenu(
497 PhabricatorUser $viewer,
498 PhabricatorProject $project,
499 $custom_query,
500 PhabricatorApplicationSearchEngine $engine,
501 $query_key) {
502
503 $state = $this->getViewState();
504
505 $named = array(
506 'open' => pht('Open Tasks'),
507 'all' => pht('All Tasks'),
508 );
509
510 if ($viewer->isLoggedIn()) {
511 $named['assigned'] = pht('Assigned to Me');
512 }
513
514 if ($custom_query) {
515 $named[$custom_query->getQueryKey()] = pht('Custom Filter');
516 }
517
518 $items = array();
519 foreach ($named as $key => $name) {
520 $is_selected = ($key == $query_key);
521 if ($is_selected) {
522 $active_filter = $name;
523 }
524
525 $is_custom = false;
526 if ($custom_query) {
527 $is_custom = ($key == $custom_query->getQueryKey());
528 }
529
530 $item = id(new PhabricatorActionView())
531 ->setIcon('fa-search')
532 ->setSelected($is_selected)
533 ->setName($name);
534
535 if ($is_custom) {
536 // When you're using a custom filter already and you select "Custom
537 // Filter", you get a dialog back to let you edit the filter. This is
538 // equivalent to selecting "Advanced Filter..." to configure a new
539 // filter.
540 $filter_uri = $state->newWorkboardURI('filter/');
541 $item->setWorkflow(true);
542 } else {
543 $filter_uri = urisprintf('query/%s/', $key);
544 $filter_uri = $state->newWorkboardURI($filter_uri);
545 $filter_uri->removeQueryParam('filter');
546 }
547
548 $item->setHref($filter_uri);
549
550 $items[] = $item;
551 }
552
553 $id = $project->getID();
554
555 $filter_uri = $state->newWorkboardURI('filter/');
556
557 $items[] = id(new PhabricatorActionView())
558 ->setIcon('fa-cog')
559 ->setHref($filter_uri)
560 ->setWorkflow(true)
561 ->setName(pht('Advanced Filter...'));
562
563 $save_uri = $state->newWorkboardURI('default/filter/');
564
565 $can_edit = PhabricatorPolicyFilter::hasCapability(
566 $viewer,
567 $project,
568 PhabricatorPolicyCapability::CAN_EDIT);
569
570 $items[] = id(new PhabricatorActionView())
571 ->setType(PhabricatorActionView::TYPE_DIVIDER);
572
573 $items[] = id(new PhabricatorActionView())
574 ->setIcon('fa-floppy-o')
575 ->setName(pht('Save as Default'))
576 ->setHref($save_uri)
577 ->setWorkflow(true)
578 ->setDisabled(!$can_edit);
579
580 $filter_menu = id(new PhabricatorActionListView())
581 ->setViewer($viewer);
582 foreach ($items as $item) {
583 $filter_menu->addAction($item);
584 }
585
586 $filter_button = id(new PHUIListItemView())
587 ->setName($active_filter)
588 ->setIcon('fa-search')
589 ->setHref('#')
590 ->addSigil('boards-dropdown-menu')
591 ->setMetadata(
592 array(
593 'items' => hsprintf('%s', $filter_menu),
594 ));
595
596 return $filter_button;
597 }
598
599 private function buildManageMenu(
600 PhabricatorProject $project,
601 $show_hidden) {
602
603 $request = $this->getRequest();
604 $viewer = $request->getUser();
605 $state = $this->getViewState();
606
607 $id = $project->getID();
608
609 $manage_uri = $this->getApplicationURI("board/{$id}/manage/");
610 $add_uri = $this->getApplicationURI("board/{$id}/edit/");
611
612 $can_edit = PhabricatorPolicyFilter::hasCapability(
613 $viewer,
614 $project,
615 PhabricatorPolicyCapability::CAN_EDIT);
616
617 $manage_items = array();
618
619 $manage_items[] = id(new PhabricatorActionView())
620 ->setIcon('fa-plus')
621 ->setName(pht('Add Column'))
622 ->setHref($add_uri)
623 ->setDisabled(!$can_edit)
624 ->setWorkflow(true);
625
626 $reorder_uri = $this->getApplicationURI("board/{$id}/reorder/");
627 $manage_items[] = id(new PhabricatorActionView())
628 ->setIcon('fa-exchange')
629 ->setName(pht('Reorder Columns'))
630 ->setHref($reorder_uri)
631 ->setDisabled(!$can_edit)
632 ->setWorkflow(true);
633
634 if ($show_hidden) {
635 $hidden_uri = $state->newWorkboardURI()
636 ->removeQueryParam('hidden');
637 $hidden_icon = 'fa-eye-slash';
638 $hidden_text = pht('Hide Hidden Columns');
639 } else {
640 $hidden_uri = $state->newWorkboardURI()
641 ->replaceQueryParam('hidden', 'true');
642 $hidden_icon = 'fa-eye';
643 $hidden_text = pht('Show Hidden Columns');
644 }
645
646 $manage_items[] = id(new PhabricatorActionView())
647 ->setIcon($hidden_icon)
648 ->setName($hidden_text)
649 ->setHref($hidden_uri);
650
651 $manage_items[] = id(new PhabricatorActionView())
652 ->setType(PhabricatorActionView::TYPE_DIVIDER);
653
654 $background_uri = $this->getApplicationURI("board/{$id}/background/");
655 $manage_items[] = id(new PhabricatorActionView())
656 ->setIcon('fa-paint-brush')
657 ->setName(pht('Change Background Color'))
658 ->setHref($background_uri)
659 ->setDisabled(!$can_edit)
660 ->setWorkflow(false);
661
662 $manage_uri = $this->getApplicationURI("board/{$id}/manage/");
663 $manage_items[] = id(new PhabricatorActionView())
664 ->setIcon('fa-gear')
665 ->setName(pht('Manage Workboard'))
666 ->setHref($manage_uri);
667
668 $manage_menu = id(new PhabricatorActionListView())
669 ->setViewer($viewer);
670 foreach ($manage_items as $item) {
671 $manage_menu->addAction($item);
672 }
673
674 $manage_button = id(new PHUIListItemView())
675 ->setIcon('fa-cog')
676 ->setHref('#')
677 ->addSigil('boards-dropdown-menu')
678 ->addSigil('has-tooltip')
679 ->setMetadata(
680 array(
681 'tip' => pht('Manage'),
682 'align' => 'S',
683 'items' => hsprintf('%s', $manage_menu),
684 ));
685
686 return $manage_button;
687 }
688
689 private function buildFullscreenMenu() {
690
691 $up = id(new PHUIListItemView())
692 ->setIcon('fa-arrows-alt')
693 ->setHref('#')
694 ->addClass('phui-workboard-expand-icon')
695 ->addSigil('jx-toggle-class')
696 ->addSigil('has-tooltip')
697 ->setMetaData(array(
698 'tip' => pht('Fullscreen'),
699 'map' => array(
700 'phabricator-standard-page' => 'phui-workboard-fullscreen',
701 ),
702 ));
703
704 return $up;
705 }
706
707 private function buildColumnMenu(
708 PhabricatorProject $project,
709 PhabricatorProjectColumn $column) {
710
711 $request = $this->getRequest();
712 $viewer = $request->getUser();
713 $state = $this->getViewState();
714
715 $can_edit = PhabricatorPolicyFilter::hasCapability(
716 $viewer,
717 $project,
718 PhabricatorPolicyCapability::CAN_EDIT);
719
720 $column_items = array();
721
722 if ($column->getProxyPHID()) {
723 $default_phid = $column->getProxyPHID();
724 } else {
725 $default_phid = $column->getProjectPHID();
726 }
727
728 $specs = id(new ManiphestEditEngine())
729 ->setViewer($viewer)
730 ->newCreateActionSpecifications(array());
731
732 foreach ($specs as $spec) {
733
734 // Prefill tags= when you open the Column menu
735 // https://we.phorge.it/T15147
736 $spec_href = new PhutilURI($spec['uri']);
737 $spec_slug = $project->getPrimarySlug();
738 if ($spec_slug !== null) {
739 $spec_href->replaceQueryParam('tags', $spec_slug);
740 }
741
742 $column_items[] = id(new PhabricatorActionView())
743 ->setIcon($spec['icon'])
744 ->setName($spec['name'])
745 ->setHref($spec_href)
746 ->setDisabled($spec['disabled'])
747 ->addSigil('column-add-task')
748 ->setMetadata(
749 array(
750 'createURI' => $spec['uri'],
751 'columnPHID' => $column->getPHID(),
752 'boardPHID' => $project->getPHID(),
753 'projectPHID' => $default_phid,
754 ));
755 }
756
757 $column_items[] = id(new PhabricatorActionView())
758 ->setType(PhabricatorActionView::TYPE_DIVIDER);
759
760 $query_uri = urisprintf('viewquery/%d/', $column->getID());
761 $query_uri = $state->newWorkboardURI($query_uri);
762
763 $column_items[] = id(new PhabricatorActionView())
764 ->setName(pht('View Tasks as Query'))
765 ->setIcon('fa-search')
766 ->setHref($query_uri);
767
768 $can_bulk_edit = PhabricatorPolicyFilter::hasCapability(
769 $viewer,
770 PhabricatorApplication::getByClass(
771 PhabricatorManiphestApplication::class),
772 ManiphestBulkEditCapability::CAPABILITY);
773
774 $column_move_uri = urisprintf('bulkmove/%d/column/', $column->getID());
775 $column_move_uri = $state->newWorkboardURI($column_move_uri);
776
777 $column_items[] = id(new PhabricatorActionView())
778 ->setIcon('fa-arrows-h')
779 ->setName(pht('Move Tasks to Column...'))
780 ->setHref($column_move_uri)
781 ->setDisabled(!$can_bulk_edit)
782 ->setWorkflow(true);
783
784 $project_move_uri = urisprintf('bulkmove/%d/project/', $column->getID());
785 $project_move_uri = $state->newWorkboardURI($project_move_uri);
786
787 $column_items[] = id(new PhabricatorActionView())
788 ->setIcon('fa-arrows')
789 ->setName(pht('Move Tasks to Project...'))
790 ->setHref($project_move_uri)
791 ->setDisabled(!$can_bulk_edit)
792 ->setWorkflow(true);
793
794 $bulk_edit_uri = urisprintf('bulk/%d/', $column->getID());
795 $bulk_edit_uri = $state->newWorkboardURI($bulk_edit_uri);
796
797 $column_items[] = id(new PhabricatorActionView())
798 ->setIcon('fa-pencil-square-o')
799 ->setName(pht('Bulk Edit Tasks...'))
800 ->setHref($bulk_edit_uri)
801 ->setDisabled(!$can_bulk_edit);
802
803 $column_items[] = id(new PhabricatorActionView())
804 ->setType(PhabricatorActionView::TYPE_DIVIDER);
805
806
807 $edit_uri = 'board/'.$project->getID().'/edit/'.$column->getID().'/';
808 $column_items[] = id(new PhabricatorActionView())
809 ->setName(pht('Edit Column'))
810 ->setIcon('fa-pencil')
811 ->setHref($this->getApplicationURI($edit_uri))
812 ->setDisabled(!$can_edit)
813 ->setWorkflow(true);
814
815 $can_hide = ($can_edit && !$column->isDefaultColumn());
816
817 $hide_uri = urisprintf('hide/%d/', $column->getID());
818 $hide_uri = $state->newWorkboardURI($hide_uri);
819
820 if (!$column->isHidden()) {
821 $column_items[] = id(new PhabricatorActionView())
822 ->setName(pht('Hide Column'))
823 ->setIcon('fa-eye-slash')
824 ->setHref($hide_uri)
825 ->setDisabled(!$can_hide)
826 ->setWorkflow(true);
827 } else {
828 $column_items[] = id(new PhabricatorActionView())
829 ->setName(pht('Show Column'))
830 ->setIcon('fa-eye')
831 ->setHref($hide_uri)
832 ->setDisabled(!$can_hide)
833 ->setWorkflow(true);
834 }
835
836 $column_menu = id(new PhabricatorActionListView())
837 ->setViewer($viewer);
838 foreach ($column_items as $item) {
839 $column_menu->addAction($item);
840 }
841
842 $column_button = id(new PHUIIconView())
843 ->setIcon('fa-pencil')
844 ->setHref('#')
845 ->addSigil('boards-dropdown-menu')
846 ->setMetadata(
847 array(
848 'items' => hsprintf('%s', $column_menu),
849 ));
850
851 return $column_button;
852 }
853
854 private function buildTriggerMenu(PhabricatorProjectColumn $column) {
855 $viewer = $this->getViewer();
856 $trigger = $column->getTrigger();
857
858 $can_edit = PhabricatorPolicyFilter::hasCapability(
859 $viewer,
860 $column,
861 PhabricatorPolicyCapability::CAN_EDIT);
862
863 $trigger_items = array();
864 if (!$trigger) {
865 $set_uri = $this->getApplicationURI(
866 new PhutilURI(
867 'trigger/edit/',
868 array(
869 'columnPHID' => $column->getPHID(),
870 )));
871
872 $trigger_items[] = id(new PhabricatorActionView())
873 ->setIcon('fa-cogs')
874 ->setName(pht('New Trigger...'))
875 ->setHref($set_uri)
876 ->setDisabled(!$can_edit);
877 } else {
878 $trigger_items[] = id(new PhabricatorActionView())
879 ->setIcon('fa-cogs')
880 ->setName(pht('View Trigger'))
881 ->setHref($trigger->getURI())
882 ->setDisabled(!$can_edit);
883 }
884
885 $remove_uri = $this->getApplicationURI(
886 new PhutilURI(
887 urisprintf(
888 'column/remove/%d/',
889 $column->getID())));
890
891 $trigger_items[] = id(new PhabricatorActionView())
892 ->setIcon('fa-times')
893 ->setName(pht('Remove Trigger'))
894 ->setHref($remove_uri)
895 ->setWorkflow(true)
896 ->setDisabled(!$can_edit || !$trigger);
897
898 $trigger_menu = id(new PhabricatorActionListView())
899 ->setViewer($viewer);
900 foreach ($trigger_items as $item) {
901 $trigger_menu->addAction($item);
902 }
903
904 if ($trigger) {
905 $trigger_icon = 'fa-cogs';
906 } else {
907 $trigger_icon = 'fa-cogs grey';
908 }
909
910 $trigger_button = id(new PHUIIconView())
911 ->setIcon($trigger_icon)
912 ->setHref('#')
913 ->addSigil('boards-dropdown-menu')
914 ->addSigil('trigger-preview')
915 ->setMetadata(
916 array(
917 'items' => hsprintf('%s', $trigger_menu),
918 'columnPHID' => $column->getPHID(),
919 ));
920
921 return $trigger_button;
922 }
923
924 private function buildInitializeContent(PhabricatorProject $project) {
925 $request = $this->getRequest();
926 $viewer = $this->getViewer();
927
928 $type = $request->getStr('initialize-type');
929
930 $id = $project->getID();
931
932 $profile_uri = $this->getApplicationURI("profile/{$id}/");
933 $board_uri = $this->getApplicationURI("board/{$id}/");
934 $import_uri = $this->getApplicationURI("board/{$id}/import/");
935
936 $set_default = $request->getBool('default');
937 if ($set_default) {
938 $this
939 ->getProfileMenuEngine()
940 ->adjustDefault(PhabricatorProject::ITEM_WORKBOARD);
941 }
942
943 if ($request->isFormPost()) {
944 if ($type == 'backlog-only') {
945 $column = PhabricatorProjectColumn::initializeNewColumn($viewer)
946 ->setSequence(0)
947 ->setProperty('isDefault', true)
948 ->setProjectPHID($project->getPHID())
949 ->save();
950
951 $xactions = array();
952 $xactions[] = id(new PhabricatorProjectTransaction())
953 ->setTransactionType(
954 PhabricatorProjectWorkboardTransaction::TRANSACTIONTYPE)
955 ->setNewValue(1);
956
957 id(new PhabricatorProjectTransactionEditor())
958 ->setActor($viewer)
959 ->setContentSourceFromRequest($request)
960 ->setContinueOnNoEffect(true)
961 ->setContinueOnMissingFields(true)
962 ->applyTransactions($project, $xactions);
963
964 return id(new AphrontRedirectResponse())
965 ->setURI($board_uri);
966 } else {
967 return id(new AphrontRedirectResponse())
968 ->setURI($import_uri);
969 }
970 }
971
972 // TODO: Tailor this UI if the project is already a parent project. We
973 // should not offer options for creating a parent project workboard, since
974 // they can't have their own columns.
975
976 $new_selector = id(new AphrontFormRadioButtonControl())
977 ->setLabel(pht('Columns'))
978 ->setName('initialize-type')
979 ->setValue('backlog-only')
980 ->addButton(
981 'backlog-only',
982 pht('New Empty Board'),
983 pht('Create a new board with just a backlog column.'))
984 ->addButton(
985 'import',
986 pht('Import Columns'),
987 pht('Import board columns from another project.'));
988
989 $default_checkbox = id(new AphrontFormCheckboxControl())
990 ->setLabel(pht('Make Default'))
991 ->addCheckbox(
992 'default',
993 1,
994 pht('Make the workboard the default view for this project.'),
995 true);
996
997 $form = id(new AphrontFormView())
998 ->setUser($viewer)
999 ->addHiddenInput('initialize', 1)
1000 ->appendRemarkupInstructions(
1001 pht('The workboard for this project has not been created yet.'))
1002 ->appendControl($new_selector)
1003 ->appendControl($default_checkbox)
1004 ->appendControl(
1005 id(new AphrontFormSubmitControl())
1006 ->addCancelButton($profile_uri)
1007 ->setValue(pht('Create Workboard')));
1008
1009 $box = id(new PHUIObjectBoxView())
1010 ->setHeaderText(pht('Create Workboard'))
1011 ->setForm($form);
1012
1013 return $box;
1014 }
1015
1016 private function buildNoAccessContent(PhabricatorProject $project) {
1017 $viewer = $this->getViewer();
1018
1019 $id = $project->getID();
1020
1021 $profile_uri = $this->getApplicationURI("profile/{$id}/");
1022
1023 return $this->newDialog()
1024 ->setTitle(pht('Unable to Create Workboard'))
1025 ->appendParagraph(
1026 pht(
1027 'The workboard for this project has not been created yet, '.
1028 'but you do not have permission to create it. Only users '.
1029 'who can edit this project can create a workboard for it.'))
1030 ->addCancelButton($profile_uri);
1031 }
1032
1033
1034 private function buildEnableContent(PhabricatorProject $project) {
1035 $request = $this->getRequest();
1036 $viewer = $this->getViewer();
1037
1038 $id = $project->getID();
1039 $profile_uri = $this->getApplicationURI("profile/{$id}/");
1040 $board_uri = $this->getApplicationURI("board/{$id}/");
1041
1042 if ($request->isFormPost()) {
1043 $xactions = array();
1044
1045 $xactions[] = id(new PhabricatorProjectTransaction())
1046 ->setTransactionType(
1047 PhabricatorProjectWorkboardTransaction::TRANSACTIONTYPE)
1048 ->setNewValue(1);
1049
1050 id(new PhabricatorProjectTransactionEditor())
1051 ->setActor($viewer)
1052 ->setContentSourceFromRequest($request)
1053 ->setContinueOnNoEffect(true)
1054 ->setContinueOnMissingFields(true)
1055 ->applyTransactions($project, $xactions);
1056
1057 return id(new AphrontRedirectResponse())
1058 ->setURI($board_uri);
1059 }
1060
1061 return $this->newDialog()
1062 ->setTitle(pht('Workboard Disabled'))
1063 ->addHiddenInput('initialize', 1)
1064 ->appendParagraph(
1065 pht(
1066 'This workboard has been disabled, but can be restored to its '.
1067 'former glory.'))
1068 ->addCancelButton($profile_uri)
1069 ->addSubmitButton(pht('Enable Workboard'));
1070 }
1071
1072 private function buildDisabledContent(PhabricatorProject $project) {
1073 $viewer = $this->getViewer();
1074
1075 $id = $project->getID();
1076
1077 $profile_uri = $this->getApplicationURI("profile/{$id}/");
1078
1079 return $this->newDialog()
1080 ->setTitle(pht('Workboard Disabled'))
1081 ->appendParagraph(
1082 pht(
1083 'This workboard has been disabled, and you do not have permission '.
1084 'to enable it. Only users who can edit this project can restore '.
1085 'the workboard.'))
1086 ->addCancelButton($profile_uri);
1087 }
1088
1089}