Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F1068299
PhabricatorProfileMenuEngine.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
41 KB
Referenced Files
None
Subscribers
None
PhabricatorProfileMenuEngine.php
View Options
<?php
abstract
class
PhabricatorProfileMenuEngine
extends
Phobject
{
private
$viewer
;
private
$profileObject
;
private
$customPHID
;
private
$items
;
private
$controller
;
private
$navigation
;
private
$showNavigation
=
true
;
private
$editMode
;
private
$pageClasses
=
array
();
private
$showContentCrumbs
=
true
;
const
ITEM_CUSTOM_DIVIDER
=
'engine.divider'
;
const
ITEM_MANAGE
=
'item.configure'
;
const
MODE_COMBINED
=
'combined'
;
const
MODE_GLOBAL
=
'global'
;
const
MODE_CUSTOM
=
'custom'
;
public
function
setViewer
(
PhabricatorUser
$viewer
)
{
$this
->
viewer
=
$viewer
;
return
$this
;
}
public
function
getViewer
()
{
return
$this
->
viewer
;
}
public
function
setProfileObject
(
$profile_object
)
{
$this
->
profileObject
=
$profile_object
;
return
$this
;
}
public
function
getProfileObject
()
{
return
$this
->
profileObject
;
}
public
function
setCustomPHID
(
$custom_phid
)
{
$this
->
customPHID
=
$custom_phid
;
return
$this
;
}
public
function
getCustomPHID
()
{
return
$this
->
customPHID
;
}
private
function
getEditModeCustomPHID
()
{
$mode
=
$this
->
getEditMode
();
switch
(
$mode
)
{
case
self
::
MODE_CUSTOM
:
$custom_phid
=
$this
->
getCustomPHID
();
break
;
case
self
::
MODE_GLOBAL
:
$custom_phid
=
null
;
break
;
}
return
$custom_phid
;
}
public
function
setController
(
PhabricatorController
$controller
)
{
$this
->
controller
=
$controller
;
return
$this
;
}
public
function
getController
()
{
return
$this
->
controller
;
}
private
function
setDefaultItem
(
PhabricatorProfileMenuItemConfiguration
$default_item
)
{
$this
->
defaultItem
=
$default_item
;
return
$this
;
}
public
function
getDefaultItem
()
{
return
$this
->
pickDefaultItem
(
$this
->
getItems
());
}
public
function
setShowNavigation
(
$show
)
{
$this
->
showNavigation
=
$show
;
return
$this
;
}
public
function
getShowNavigation
()
{
return
$this
->
showNavigation
;
}
public
function
addContentPageClass
(
$class
)
{
$this
->
pageClasses
[]
=
$class
;
return
$this
;
}
public
function
setShowContentCrumbs
(
$show_content_crumbs
)
{
$this
->
showContentCrumbs
=
$show_content_crumbs
;
return
$this
;
}
public
function
getShowContentCrumbs
()
{
return
$this
->
showContentCrumbs
;
}
abstract
public
function
getItemURI
(
$path
);
abstract
protected
function
isMenuEngineConfigurable
();
abstract
protected
function
getBuiltinProfileItems
(
$object
);
protected
function
getBuiltinCustomProfileItems
(
$object
,
$custom_phid
)
{
return
array
();
}
protected
function
getEditMode
()
{
return
$this
->
editMode
;
}
public
function
buildResponse
()
{
$controller
=
$this
->
getController
();
$viewer
=
$controller
->
getViewer
();
$this
->
setViewer
(
$viewer
);
$request
=
$controller
->
getRequest
();
$item_action
=
$request
->
getURIData
(
'itemAction'
);
if
(!
$item_action
)
{
$item_action
=
'view'
;
}
$is_view
=
(
$item_action
==
'view'
);
// If the engine is not configurable, don't respond to any of the editing
// or configuration routes.
if
(!
$this
->
isMenuEngineConfigurable
())
{
if
(!
$is_view
)
{
return
new
Aphront404Response
();
}
}
$item_id
=
$request
->
getURIData
(
'itemID'
);
// If we miss on the MenuEngine route, try the EditEngine route. This will
// be populated while editing items.
if
(!
$item_id
)
{
$item_id
=
$request
->
getURIData
(
'id'
);
}
$item_list
=
$this
->
getItems
();
$selected_item
=
$this
->
pickSelectedItem
(
$item_list
,
$item_id
,
$is_view
);
switch
(
$item_action
)
{
case
'view'
:
// If we were not able to select an item, we're still going to render
// a page state. For example, this happens when you create a new
// portal for the first time.
break
;
case
'info'
:
case
'hide'
:
case
'default'
:
case
'builtin'
:
if
(!
$selected_item
)
{
return
new
Aphront404Response
();
}
break
;
case
'edit'
:
if
(!
$request
->
getURIData
(
'id'
))
{
// If we continue along the "edit" pathway without an ID, we hit an
// unrelated exception because we can not build a new menu item out
// of thin air. For menus, new items are created via the "new"
// action. Just catch this case and 404 early since there's currently
// no clean way to make EditEngine aware of this.
return
new
Aphront404Response
();
}
break
;
}
$navigation
=
$this
->
buildNavigation
(
$selected_item
);
$crumbs
=
$controller
->
buildApplicationCrumbsForEditEngine
();
if
(!
$is_view
)
{
$navigation
->
selectFilter
(
self
::
ITEM_MANAGE
);
if
(
$selected_item
)
{
if
(
$selected_item
->
getCustomPHID
())
{
$edit_mode
=
'custom'
;
}
else
{
$edit_mode
=
'global'
;
}
}
else
{
$edit_mode
=
$request
->
getURIData
(
'itemEditMode'
);
}
$available_modes
=
$this
->
getViewerEditModes
();
if
(
$available_modes
)
{
$available_modes
=
array_fuse
(
$available_modes
);
if
(
isset
(
$available_modes
[
$edit_mode
]))
{
$this
->
editMode
=
$edit_mode
;
}
else
{
if
(
$item_action
!=
'configure'
)
{
return
new
Aphront404Response
();
}
}
}
$page_title
=
pht
(
'Configure Menu'
);
}
else
{
if
(
$selected_item
)
{
$page_title
=
$selected_item
->
getDisplayName
();
}
else
{
$page_title
=
pht
(
'Empty'
);
}
}
switch
(
$item_action
)
{
case
'view'
:
if
(
$selected_item
)
{
try
{
$content
=
$this
->
buildItemViewContent
(
$selected_item
);
}
catch
(
Exception
$ex
)
{
$content
=
id
(
new
PHUIInfoView
())
->
setTitle
(
pht
(
'Unable to Render Dashboard'
))
->
setErrors
(
array
(
$ex
->
getMessage
()));
}
$crumbs
->
addTextCrumb
(
$selected_item
->
getDisplayName
());
}
else
{
$content
=
$this
->
newNoMenuItemsView
();
}
if
(!
$content
)
{
$content
=
$this
->
newEmptyView
(
pht
(
'Empty'
),
pht
(
'There is nothing here.'
));
}
break
;
case
'configure'
:
$mode
=
$this
->
getEditMode
();
if
(!
$mode
)
{
$crumbs
->
addTextCrumb
(
pht
(
'Configure Menu'
));
$content
=
$this
->
buildMenuEditModeContent
();
}
else
{
if
(
count
(
$available_modes
)
>
1
)
{
$crumbs
->
addTextCrumb
(
pht
(
'Configure Menu'
),
$this
->
getItemURI
(
'configure/'
));
switch
(
$mode
)
{
case
self
::
MODE_CUSTOM
:
$crumbs
->
addTextCrumb
(
pht
(
'Personal'
));
break
;
case
self
::
MODE_GLOBAL
:
$crumbs
->
addTextCrumb
(
pht
(
'Global'
));
break
;
}
}
else
{
$crumbs
->
addTextCrumb
(
pht
(
'Configure Menu'
));
}
$edit_list
=
$this
->
loadItems
(
$mode
);
$content
=
$this
->
buildItemConfigureContent
(
$edit_list
);
}
break
;
case
'reorder'
:
$mode
=
$this
->
getEditMode
();
$edit_list
=
$this
->
loadItems
(
$mode
);
$content
=
$this
->
buildItemReorderContent
(
$edit_list
);
break
;
case
'new'
:
$item_key
=
$request
->
getURIData
(
'itemKey'
);
$mode
=
$this
->
getEditMode
();
$content
=
$this
->
buildItemNewContent
(
$item_key
,
$mode
);
break
;
case
'builtin'
:
$content
=
$this
->
buildItemBuiltinContent
(
$selected_item
);
break
;
case
'hide'
:
$content
=
$this
->
buildItemHideContent
(
$selected_item
);
break
;
case
'default'
:
if
(!
$this
->
isMenuEnginePinnable
())
{
return
new
Aphront404Response
();
}
$content
=
$this
->
buildItemDefaultContent
(
$selected_item
,
$item_list
);
break
;
case
'edit'
:
$content
=
$this
->
buildItemEditContent
();
break
;
default
:
throw
new
Exception
(
pht
(
'Unsupported item action "%s".'
,
$item_action
));
}
if
(
$content
instanceof
AphrontResponse
)
{
return
$content
;
}
if
(
$content
instanceof
AphrontResponseProducerInterface
)
{
return
$content
;
}
$crumbs
->
setBorder
(
true
);
$page
=
$controller
->
newPage
()
->
setTitle
(
$page_title
)
->
appendChild
(
$content
);
if
(!
$is_view
||
$this
->
getShowContentCrumbs
())
{
$page
->
setCrumbs
(
$crumbs
);
}
if
(
$this
->
getShowNavigation
())
{
$page
->
setNavigation
(
$navigation
);
}
if
(
$is_view
)
{
foreach
(
$this
->
pageClasses
as
$class
)
{
$page
->
addClass
(
$class
);
}
}
return
$page
;
}
public
function
buildNavigation
(
PhabricatorProfileMenuItemConfiguration
$selected_item
=
null
)
{
if
(
$this
->
navigation
)
{
return
$this
->
navigation
;
}
$nav
=
id
(
new
AphrontSideNavFilterView
())
->
setIsProfileMenu
(
true
)
->
setBaseURI
(
new
PhutilURI
(
$this
->
getItemURI
(
''
)));
$menu_items
=
$this
->
getItems
();
$filtered_items
=
array
();
foreach
(
$menu_items
as
$menu_item
)
{
if
(
$menu_item
->
isDisabled
())
{
continue
;
}
$filtered_items
[]
=
$menu_item
;
}
$filtered_groups
=
mgroup
(
$filtered_items
,
'getMenuItemKey'
);
foreach
(
$filtered_groups
as
$group
)
{
$first_item
=
head
(
$group
);
$first_item
->
willBuildNavigationItems
(
$group
);
}
$has_items
=
false
;
foreach
(
$menu_items
as
$menu_item
)
{
if
(
$menu_item
->
isDisabled
())
{
continue
;
}
$items
=
$menu_item
->
buildNavigationMenuItems
();
foreach
(
$items
as
$item
)
{
$this
->
validateNavigationMenuItem
(
$item
);
}
// If the item produced only a single item which does not otherwise
// have a key, try to automatically assign it a reasonable key. This
// makes selecting the correct item simpler.
if
(
count
(
$items
)
==
1
)
{
$item
=
head
(
$items
);
if
(
$item
->
getKey
()
===
null
)
{
$default_key
=
$menu_item
->
getDefaultMenuItemKey
();
$item
->
setKey
(
$default_key
);
}
}
foreach
(
$items
as
$item
)
{
$nav
->
addMenuItem
(
$item
);
$has_items
=
true
;
}
}
if
(!
$has_items
)
{
// If the navigation menu has no items, add an empty label item to
// force it to render something.
$empty_item
=
id
(
new
PHUIListItemView
())
->
setType
(
PHUIListItemView
::
TYPE_LABEL
);
$nav
->
addMenuItem
(
$empty_item
);
}
$nav
->
selectFilter
(
null
);
$navigation_items
=
$nav
->
getMenu
()->
getItems
();
$select_key
=
$this
->
pickHighlightedMenuItem
(
$navigation_items
,
$selected_item
);
$nav
->
selectFilter
(
$select_key
);
$this
->
navigation
=
$nav
;
return
$this
->
navigation
;
}
private
function
getItems
()
{
if
(
$this
->
items
===
null
)
{
$this
->
items
=
$this
->
loadItems
(
self
::
MODE_COMBINED
);
}
return
$this
->
items
;
}
private
function
loadItems
(
$mode
)
{
$viewer
=
$this
->
getViewer
();
$object
=
$this
->
getProfileObject
();
$items
=
$this
->
loadBuiltinProfileItems
(
$mode
);
$query
=
id
(
new
PhabricatorProfileMenuItemConfigurationQuery
())
->
setViewer
(
$viewer
)
->
withProfilePHIDs
(
array
(
$object
->
getPHID
()));
switch
(
$mode
)
{
case
self
::
MODE_GLOBAL
:
$query
->
withCustomPHIDs
(
array
(),
true
);
break
;
case
self
::
MODE_CUSTOM
:
$query
->
withCustomPHIDs
(
array
(
$this
->
getCustomPHID
()),
false
);
break
;
case
self
::
MODE_COMBINED
:
$query
->
withCustomPHIDs
(
array
(
$this
->
getCustomPHID
()),
true
);
break
;
}
$stored_items
=
$query
->
execute
();
foreach
(
$stored_items
as
$stored_item
)
{
$impl
=
$stored_item
->
getMenuItem
();
$impl
->
setViewer
(
$viewer
);
$impl
->
setEngine
(
$this
);
}
// Merge the stored items into the builtin items. If a builtin item has
// a stored version, replace the defaults with the stored changes.
foreach
(
$stored_items
as
$stored_item
)
{
if
(!
$stored_item
->
shouldEnableForObject
(
$object
))
{
continue
;
}
$builtin_key
=
$stored_item
->
getBuiltinKey
();
if
(
$builtin_key
!==
null
)
{
// If this builtin actually exists, replace the builtin with the
// stored configuration. Otherwise, we're just going to drop the
// stored config: it corresponds to an out-of-date or uninstalled
// item.
if
(
isset
(
$items
[
$builtin_key
]))
{
$builtin_item
=
$items
[
$builtin_key
];
// Copy runtime properties from the builtin item to the stored item.
$stored_item
->
setIsHeadItem
(
$builtin_item
->
getIsHeadItem
());
$stored_item
->
setIsTailItem
(
$builtin_item
->
getIsTailItem
());
$items
[
$builtin_key
]
=
$stored_item
;
}
else
{
continue
;
}
}
else
{
$items
[]
=
$stored_item
;
}
}
return
$this
->
arrangeItems
(
$items
,
$mode
);
}
private
function
loadBuiltinProfileItems
(
$mode
)
{
$object
=
$this
->
getProfileObject
();
switch
(
$mode
)
{
case
self
::
MODE_GLOBAL
:
$builtins
=
$this
->
getBuiltinProfileItems
(
$object
);
break
;
case
self
::
MODE_CUSTOM
:
$builtins
=
$this
->
getBuiltinCustomProfileItems
(
$object
,
$this
->
getCustomPHID
());
break
;
case
self
::
MODE_COMBINED
:
$builtins
=
array
();
$builtins
[]
=
$this
->
getBuiltinCustomProfileItems
(
$object
,
$this
->
getCustomPHID
());
$builtins
[]
=
$this
->
getBuiltinProfileItems
(
$object
);
$builtins
=
array_mergev
(
$builtins
);
break
;
}
$items
=
PhabricatorProfileMenuItem
::
getAllMenuItems
();
$viewer
=
$this
->
getViewer
();
$order
=
1
;
$map
=
array
();
foreach
(
$builtins
as
$builtin
)
{
$builtin_key
=
$builtin
->
getBuiltinKey
();
if
(!
$builtin_key
)
{
throw
new
Exception
(
pht
(
'Object produced a builtin item with no builtin item key! '
.
'Builtin items must have a unique key.'
));
}
if
(
isset
(
$map
[
$builtin_key
]))
{
throw
new
Exception
(
pht
(
'Object produced two items with the same builtin key ("%s"). '
.
'Each item must have a unique builtin key.'
,
$builtin_key
));
}
$item_key
=
$builtin
->
getMenuItemKey
();
$item
=
idx
(
$items
,
$item_key
);
if
(!
$item
)
{
throw
new
Exception
(
pht
(
'Builtin item ("%s") specifies a bad item key ("%s"); there '
.
'is no corresponding item implementation available.'
,
$builtin_key
,
$item_key
));
}
$item
=
clone
$item
;
$item
->
setViewer
(
$viewer
);
$item
->
setEngine
(
$this
);
$builtin
->
setProfilePHID
(
$object
->
getPHID
())
->
attachMenuItem
(
$item
)
->
attachProfileObject
(
$object
)
->
setMenuItemOrder
(
$order
);
if
(!
$builtin
->
shouldEnableForObject
(
$object
))
{
continue
;
}
$map
[
$builtin_key
]
=
$builtin
;
$order
++;
}
return
$map
;
}
private
function
validateNavigationMenuItem
(
$item
)
{
if
(!(
$item
instanceof
PHUIListItemView
))
{
throw
new
Exception
(
pht
(
'Expected buildNavigationMenuItems() to return a list of '
.
'PHUIListItemView objects, but got a surprise.'
));
}
}
public
function
getConfigureURI
()
{
$mode
=
$this
->
getEditMode
();
switch
(
$mode
)
{
case
self
::
MODE_CUSTOM
:
return
$this
->
getItemURI
(
'configure/custom/'
);
case
self
::
MODE_GLOBAL
:
return
$this
->
getItemURI
(
'configure/global/'
);
}
return
$this
->
getItemURI
(
'configure/'
);
}
private
function
buildItemReorderContent
(
array
$items
)
{
$viewer
=
$this
->
getViewer
();
$object
=
$this
->
getProfileObject
();
// If you're reordering global items, you need to be able to edit the
// object the menu appears on. If you're reordering custom items, you only
// need to be able to edit the custom object. Currently, the custom object
// is always the viewing user's own user object.
$custom_phid
=
$this
->
getEditModeCustomPHID
();
if
(!
$custom_phid
)
{
PhabricatorPolicyFilter
::
requireCapability
(
$viewer
,
$object
,
PhabricatorPolicyCapability
::
CAN_EDIT
);
}
else
{
$policy_object
=
id
(
new
PhabricatorObjectQuery
())
->
setViewer
(
$viewer
)
->
withPHIDs
(
array
(
$custom_phid
))
->
executeOne
();
if
(!
$policy_object
)
{
throw
new
Exception
(
pht
(
'Failed to load custom PHID "%s"!'
,
$custom_phid
));
}
PhabricatorPolicyFilter
::
requireCapability
(
$viewer
,
$policy_object
,
PhabricatorPolicyCapability
::
CAN_EDIT
);
}
$controller
=
$this
->
getController
();
$request
=
$controller
->
getRequest
();
$request
->
validateCSRF
();
$order
=
$request
->
getStrList
(
'order'
);
$by_builtin
=
array
();
$by_id
=
array
();
foreach
(
$items
as
$key
=>
$item
)
{
$id
=
$item
->
getID
();
if
(
$id
)
{
$by_id
[
$id
]
=
$key
;
continue
;
}
$builtin_key
=
$item
->
getBuiltinKey
();
if
(
$builtin_key
)
{
$by_builtin
[
$builtin_key
]
=
$key
;
continue
;
}
}
$key_order
=
array
();
foreach
(
$order
as
$order_item
)
{
if
(
isset
(
$by_id
[
$order_item
]))
{
$key_order
[]
=
$by_id
[
$order_item
];
continue
;
}
if
(
isset
(
$by_builtin
[
$order_item
]))
{
$key_order
[]
=
$by_builtin
[
$order_item
];
continue
;
}
}
$items
=
array_select_keys
(
$items
,
$key_order
)
+
$items
;
$type_order
=
PhabricatorProfileMenuItemConfigurationTransaction
::
TYPE_ORDER
;
$order
=
1
;
foreach
(
$items
as
$item
)
{
$xactions
=
array
();
$xactions
[]
=
id
(
new
PhabricatorProfileMenuItemConfigurationTransaction
())
->
setTransactionType
(
$type_order
)
->
setNewValue
(
$order
);
$editor
=
id
(
new
PhabricatorProfileMenuEditor
())
->
setContentSourceFromRequest
(
$request
)
->
setActor
(
$viewer
)
->
setContinueOnMissingFields
(
true
)
->
setContinueOnNoEffect
(
true
)
->
applyTransactions
(
$item
,
$xactions
);
$order
++;
}
return
id
(
new
AphrontRedirectResponse
())
->
setURI
(
$this
->
getConfigureURI
());
}
protected
function
buildItemViewContent
(
PhabricatorProfileMenuItemConfiguration
$item
)
{
return
$item
->
newPageContent
();
}
private
function
getViewerEditModes
()
{
$modes
=
array
();
$viewer
=
$this
->
getViewer
();
if
(
$viewer
->
isLoggedIn
()
&&
$this
->
isMenuEnginePersonalizable
())
{
$modes
[]
=
self
::
MODE_CUSTOM
;
}
$object
=
$this
->
getProfileObject
();
$can_edit
=
PhabricatorPolicyFilter
::
hasCapability
(
$viewer
,
$object
,
PhabricatorPolicyCapability
::
CAN_EDIT
);
if
(
$can_edit
)
{
$modes
[]
=
self
::
MODE_GLOBAL
;
}
return
$modes
;
}
protected
function
isMenuEnginePersonalizable
()
{
return
true
;
}
/**
* Does this engine support pinning items?
*
* Personalizable menus disable pinning by default since it creates a number
* of weird edge cases without providing many benefits for current menus.
*
* @return bool True if items may be pinned as default items.
*/
protected
function
isMenuEnginePinnable
()
{
return
!
$this
->
isMenuEnginePersonalizable
();
}
private
function
buildMenuEditModeContent
()
{
$viewer
=
$this
->
getViewer
();
$modes
=
$this
->
getViewerEditModes
();
if
(!
$modes
)
{
return
new
Aphront404Response
();
}
if
(
count
(
$modes
)
==
1
)
{
$mode
=
head
(
$modes
);
return
id
(
new
AphrontRedirectResponse
())
->
setURI
(
$this
->
getItemURI
(
"configure/{$mode}/"
));
}
$menu
=
id
(
new
PHUIObjectItemListView
())
->
setUser
(
$viewer
);
$modes
=
array_fuse
(
$modes
);
if
(
isset
(
$modes
[
'custom'
]))
{
$menu
->
addItem
(
id
(
new
PHUIObjectItemView
())
->
setHeader
(
pht
(
'Personal Menu Items'
))
->
setHref
(
$this
->
getItemURI
(
'configure/custom/'
))
->
setImageURI
(
$viewer
->
getProfileImageURI
())
->
addAttribute
(
pht
(
'Edit the menu for your personal account.'
)));
}
if
(
isset
(
$modes
[
'global'
]))
{
$icon
=
id
(
new
PHUIIconView
())
->
setIcon
(
'fa-globe'
)
->
setBackground
(
'bg-blue'
);
$menu
->
addItem
(
id
(
new
PHUIObjectItemView
())
->
setHeader
(
pht
(
'Global Menu Items'
))
->
setHref
(
$this
->
getItemURI
(
'configure/global/'
))
->
setImageIcon
(
$icon
)
->
addAttribute
(
pht
(
'Edit the global default menu for all users.'
)));
}
$box
=
id
(
new
PHUIObjectBoxView
())
->
setObjectList
(
$menu
);
$header
=
id
(
new
PHUIHeaderView
())
->
setHeader
(
pht
(
'Manage Menu'
))
->
setHeaderIcon
(
'fa-list'
);
return
id
(
new
PHUITwoColumnView
())
->
setHeader
(
$header
)
->
setFooter
(
$box
);
}
private
function
buildItemConfigureContent
(
array
$items
)
{
$viewer
=
$this
->
getViewer
();
$object
=
$this
->
getProfileObject
();
$filtered_groups
=
mgroup
(
$items
,
'getMenuItemKey'
);
foreach
(
$filtered_groups
as
$group
)
{
$first_item
=
head
(
$group
);
$first_item
->
willBuildNavigationItems
(
$group
);
}
// Users only need to be able to edit the object which this menu appears
// on if they're editing global menu items. For example, users do not need
// to be able to edit the Favorites application to add new items to the
// Favorites menu.
if
(!
$this
->
getCustomPHID
())
{
PhabricatorPolicyFilter
::
requireCapability
(
$viewer
,
$object
,
PhabricatorPolicyCapability
::
CAN_EDIT
);
}
$list_id
=
celerity_generate_unique_node_id
();
$mode
=
$this
->
getEditMode
();
Javelin
::
initBehavior
(
'reorder-profile-menu-items'
,
array
(
'listID'
=>
$list_id
,
'orderURI'
=>
$this
->
getItemURI
(
"reorder/{$mode}/"
),
));
$list
=
id
(
new
PHUIObjectItemListView
())
->
setID
(
$list_id
)
->
setNoDataString
(
pht
(
'This menu currently has no items.'
));
$any_draggable
=
false
;
foreach
(
$items
as
$item
)
{
$id
=
$item
->
getID
();
$builtin_key
=
$item
->
getBuiltinKey
();
$can_edit
=
PhabricatorPolicyFilter
::
hasCapability
(
$viewer
,
$item
,
PhabricatorPolicyCapability
::
CAN_EDIT
);
$view
=
id
(
new
PHUIObjectItemView
());
$name
=
$item
->
getDisplayName
();
$type
=
$item
->
getMenuItemTypeName
();
if
(!
strlen
(
trim
(
$name
)))
{
$name
=
pht
(
'Untitled "%s" Item'
,
$type
);
}
$view
->
setHeader
(
$name
);
$view
->
addAttribute
(
$type
);
$icon
=
$item
->
getMenuItem
()->
getMenuItemTypeIcon
();
if
(
$icon
!==
null
)
{
$view
->
setStatusIcon
(
$icon
);
}
if
(
$can_edit
)
{
$can_move
=
(!
$item
->
getIsHeadItem
()
&&
!
$item
->
getIsTailItem
());
if
(
$can_move
)
{
$view
->
setGrippable
(
true
)
->
addSigil
(
'profile-menu-item'
)
->
setMetadata
(
array
(
'key'
=>
nonempty
(
$id
,
$builtin_key
),
));
$any_draggable
=
true
;
}
else
{
$view
->
setGrippable
(
false
);
}
if
(
$id
)
{
$default_uri
=
$this
->
getItemURI
(
"default/{$id}/"
);
}
else
{
$default_uri
=
$this
->
getItemURI
(
"default/{$builtin_key}/"
);
}
$default_text
=
null
;
if
(
$this
->
isMenuEnginePinnable
())
{
if
(
$item
->
isDefault
())
{
$default_icon
=
'fa-thumb-tack green'
;
$default_text
=
pht
(
'Current Default'
);
}
else
if
(
$item
->
canMakeDefault
())
{
$default_icon
=
'fa-thumb-tack'
;
$default_text
=
pht
(
'Make Default'
);
}
}
if
(
$default_text
!==
null
)
{
$view
->
addAction
(
id
(
new
PHUIListItemView
())
->
setHref
(
$default_uri
)
->
setWorkflow
(
true
)
->
setName
(
$default_text
)
->
setIcon
(
$default_icon
));
}
if
(
$id
)
{
$view
->
setHref
(
$this
->
getItemURI
(
"edit/{$id}/"
));
$hide_uri
=
$this
->
getItemURI
(
"hide/{$id}/"
);
}
else
{
$view
->
setHref
(
$this
->
getItemURI
(
"builtin/{$builtin_key}/"
));
$hide_uri
=
$this
->
getItemURI
(
"hide/{$builtin_key}/"
);
}
if
(
$item
->
isDisabled
())
{
$hide_icon
=
'fa-plus'
;
$hide_text
=
pht
(
'Enable'
);
}
else
if
(
$item
->
getBuiltinKey
()
!==
null
)
{
$hide_icon
=
'fa-times'
;
$hide_text
=
pht
(
'Disable'
);
}
else
{
$hide_icon
=
'fa-times'
;
$hide_text
=
pht
(
'Delete'
);
}
$can_disable
=
$item
->
canHideMenuItem
();
$view
->
addAction
(
id
(
new
PHUIListItemView
())
->
setHref
(
$hide_uri
)
->
setWorkflow
(
true
)
->
setDisabled
(!
$can_disable
)
->
setName
(
$hide_text
)
->
setIcon
(
$hide_icon
));
}
if
(
$item
->
isDisabled
())
{
$view
->
setDisabled
(
true
);
}
$list
->
addItem
(
$view
);
}
$item_types
=
PhabricatorProfileMenuItem
::
getAllMenuItems
();
$object
=
$this
->
getProfileObject
();
$action_list
=
id
(
new
PhabricatorActionListView
())
->
setViewer
(
$viewer
);
// See T12167. This makes the "Actions" dropdown button show up in the
// page header.
$action_list
->
setID
(
celerity_generate_unique_node_id
());
$action_list
->
addAction
(
id
(
new
PhabricatorActionView
())
->
setLabel
(
true
)
->
setName
(
pht
(
'Add New Menu Item...'
)));
foreach
(
$item_types
as
$item_type
)
{
if
(!
$item_type
->
canAddToObject
(
$object
))
{
continue
;
}
$item_key
=
$item_type
->
getMenuItemKey
();
$edit_mode
=
$this
->
getEditMode
();
$action_list
->
addAction
(
id
(
new
PhabricatorActionView
())
->
setIcon
(
$item_type
->
getMenuItemTypeIcon
())
->
setName
(
$item_type
->
getMenuItemTypeName
())
->
setHref
(
$this
->
getItemURI
(
"new/{$edit_mode}/{$item_key}/"
))
->
setWorkflow
(
true
));
}
$action_list
->
addAction
(
id
(
new
PhabricatorActionView
())
->
setLabel
(
true
)
->
setName
(
pht
(
'Documentation'
)));
$doc_link
=
PhabricatorEnv
::
getDoclink
(
'Profile Menu User Guide'
);
$doc_name
=
pht
(
'Profile Menu User Guide'
);
$action_list
->
addAction
(
id
(
new
PhabricatorActionView
())
->
setIcon
(
'fa-book'
)
->
setHref
(
$doc_link
)
->
setName
(
$doc_name
));
$header
=
id
(
new
PHUIHeaderView
())
->
setHeader
(
pht
(
'Menu Items'
))
->
setHeaderIcon
(
'fa-list'
);
$list_header
=
id
(
new
PHUIHeaderView
())
->
setHeader
(
pht
(
'Current Menu Items'
));
if
(
$any_draggable
)
{
$list_header
->
setSubheader
(
pht
(
'Drag items in this list to reorder them.'
));
}
$box
=
id
(
new
PHUIObjectBoxView
())
->
setHeader
(
$list_header
)
->
setBackground
(
PHUIObjectBoxView
::
BLUE_PROPERTY
)
->
setObjectList
(
$list
);
$curtain
=
id
(
new
PHUICurtainView
())
->
setViewer
(
$viewer
)
->
setActionList
(
$action_list
);
$view
=
id
(
new
PHUITwoColumnView
())
->
setHeader
(
$header
)
->
setCurtain
(
$curtain
)
->
setMainColumn
(
array
(
$box
,
));
return
$view
;
}
private
function
buildItemNewContent
(
$item_key
,
$mode
)
{
$item_types
=
PhabricatorProfileMenuItem
::
getAllMenuItems
();
$item_type
=
idx
(
$item_types
,
$item_key
);
if
(!
$item_type
)
{
return
new
Aphront404Response
();
}
$object
=
$this
->
getProfileObject
();
if
(!
$item_type
->
canAddToObject
(
$object
))
{
return
new
Aphront404Response
();
}
$custom_phid
=
$this
->
getEditModeCustomPHID
();
$configuration
=
PhabricatorProfileMenuItemConfiguration
::
initializeNewItem
(
$object
,
$item_type
,
$custom_phid
);
$viewer
=
$this
->
getViewer
();
PhabricatorPolicyFilter
::
requireCapability
(
$viewer
,
$configuration
,
PhabricatorPolicyCapability
::
CAN_EDIT
);
$controller
=
$this
->
getController
();
return
id
(
new
PhabricatorProfileMenuEditEngine
())
->
setMenuEngine
(
$this
)
->
setProfileObject
(
$object
)
->
setNewMenuItemConfiguration
(
$configuration
)
->
setCustomPHID
(
$custom_phid
)
->
setController
(
$controller
)
->
buildResponse
();
}
private
function
buildItemEditContent
()
{
$viewer
=
$this
->
getViewer
();
$object
=
$this
->
getProfileObject
();
$controller
=
$this
->
getController
();
$custom_phid
=
$this
->
getEditModeCustomPHID
();
return
id
(
new
PhabricatorProfileMenuEditEngine
())
->
setMenuEngine
(
$this
)
->
setProfileObject
(
$object
)
->
setController
(
$controller
)
->
setCustomPHID
(
$custom_phid
)
->
buildResponse
();
}
private
function
buildItemBuiltinContent
(
PhabricatorProfileMenuItemConfiguration
$configuration
)
{
// If this builtin item has already been persisted, redirect to the
// edit page.
$id
=
$configuration
->
getID
();
if
(
$id
)
{
return
id
(
new
AphrontRedirectResponse
())
->
setURI
(
$this
->
getItemURI
(
"edit/{$id}/"
));
}
// Otherwise, act like we're creating a new item, we're just starting
// with the builtin template.
$viewer
=
$this
->
getViewer
();
PhabricatorPolicyFilter
::
requireCapability
(
$viewer
,
$configuration
,
PhabricatorPolicyCapability
::
CAN_EDIT
);
$object
=
$this
->
getProfileObject
();
$controller
=
$this
->
getController
();
$custom_phid
=
$this
->
getEditModeCustomPHID
();
return
id
(
new
PhabricatorProfileMenuEditEngine
())
->
setIsBuiltin
(
true
)
->
setMenuEngine
(
$this
)
->
setProfileObject
(
$object
)
->
setNewMenuItemConfiguration
(
$configuration
)
->
setController
(
$controller
)
->
setCustomPHID
(
$custom_phid
)
->
buildResponse
();
}
private
function
buildItemHideContent
(
PhabricatorProfileMenuItemConfiguration
$configuration
)
{
$controller
=
$this
->
getController
();
$request
=
$controller
->
getRequest
();
$viewer
=
$this
->
getViewer
();
PhabricatorPolicyFilter
::
requireCapability
(
$viewer
,
$configuration
,
PhabricatorPolicyCapability
::
CAN_EDIT
);
if
(!
$configuration
->
canHideMenuItem
())
{
return
$controller
->
newDialog
()
->
setTitle
(
pht
(
'Mandatory Item'
))
->
appendParagraph
(
pht
(
'This menu item is very important, and can not be disabled.'
))
->
addCancelButton
(
$this
->
getConfigureURI
());
}
if
(
$configuration
->
getBuiltinKey
()
===
null
)
{
$new_value
=
null
;
$title
=
pht
(
'Delete Menu Item'
);
$body
=
pht
(
'Delete this menu item?'
);
$button
=
pht
(
'Delete Menu Item'
);
}
else
if
(
$configuration
->
isDisabled
())
{
$new_value
=
PhabricatorProfileMenuItemConfiguration
::
VISIBILITY_VISIBLE
;
$title
=
pht
(
'Enable Menu Item'
);
$body
=
pht
(
'Enable this menu item? It will appear in the menu again.'
);
$button
=
pht
(
'Enable Menu Item'
);
}
else
{
$new_value
=
PhabricatorProfileMenuItemConfiguration
::
VISIBILITY_DISABLED
;
$title
=
pht
(
'Disable Menu Item'
);
$body
=
pht
(
'Disable this menu item? It will no longer appear in the menu, but '
.
'you can re-enable it later.'
);
$button
=
pht
(
'Disable Menu Item'
);
}
$v_visibility
=
$configuration
->
getVisibility
();
if
(
$request
->
isFormPost
())
{
if
(
$new_value
===
null
)
{
$configuration
->
delete
();
}
else
{
$type_visibility
=
PhabricatorProfileMenuItemConfigurationTransaction
::
TYPE_VISIBILITY
;
$xactions
=
array
();
$xactions
[]
=
id
(
new
PhabricatorProfileMenuItemConfigurationTransaction
())
->
setTransactionType
(
$type_visibility
)
->
setNewValue
(
$new_value
);
$editor
=
id
(
new
PhabricatorProfileMenuEditor
())
->
setContentSourceFromRequest
(
$request
)
->
setActor
(
$viewer
)
->
setContinueOnMissingFields
(
true
)
->
setContinueOnNoEffect
(
true
)
->
applyTransactions
(
$configuration
,
$xactions
);
}
return
id
(
new
AphrontRedirectResponse
())
->
setURI
(
$this
->
getConfigureURI
());
}
return
$controller
->
newDialog
()
->
setTitle
(
$title
)
->
appendParagraph
(
$body
)
->
addCancelButton
(
$this
->
getConfigureURI
())
->
addSubmitButton
(
$button
);
}
private
function
buildItemDefaultContent
(
PhabricatorProfileMenuItemConfiguration
$configuration
,
array
$items
)
{
$controller
=
$this
->
getController
();
$request
=
$controller
->
getRequest
();
$viewer
=
$this
->
getViewer
();
PhabricatorPolicyFilter
::
requireCapability
(
$viewer
,
$configuration
,
PhabricatorPolicyCapability
::
CAN_EDIT
);
$done_uri
=
$this
->
getConfigureURI
();
if
(!
$configuration
->
canMakeDefault
())
{
return
$controller
->
newDialog
()
->
setTitle
(
pht
(
'Not Defaultable'
))
->
appendParagraph
(
pht
(
'This item can not be set as the default item. This is usually '
.
'because the item has no page of its own, or links to an '
.
'external page.'
))
->
addCancelButton
(
$done_uri
);
}
if
(
$configuration
->
isDefault
())
{
return
$controller
->
newDialog
()
->
setTitle
(
pht
(
'Already Default'
))
->
appendParagraph
(
pht
(
'This item is already set as the default item for this menu.'
))
->
addCancelButton
(
$done_uri
);
}
if
(
$request
->
isFormPost
())
{
$key
=
$configuration
->
getID
();
if
(!
$key
)
{
$key
=
$configuration
->
getBuiltinKey
();
}
$this
->
adjustDefault
(
$key
);
return
id
(
new
AphrontRedirectResponse
())
->
setURI
(
$done_uri
);
}
return
$controller
->
newDialog
()
->
setTitle
(
pht
(
'Make Default'
))
->
appendParagraph
(
pht
(
'Set this item as the default for this menu? Users arriving on '
.
'this page will be shown the content of this item by default.'
))
->
addCancelButton
(
$done_uri
)
->
addSubmitButton
(
pht
(
'Make Default'
));
}
protected
function
newItem
()
{
return
PhabricatorProfileMenuItemConfiguration
::
initializeNewBuiltin
();
}
protected
function
newManageItem
()
{
return
$this
->
newItem
()
->
setBuiltinKey
(
self
::
ITEM_MANAGE
)
->
setMenuItemKey
(
PhabricatorManageProfileMenuItem
::
MENUITEMKEY
)
->
setIsTailItem
(
true
);
}
public
function
adjustDefault
(
$key
)
{
$controller
=
$this
->
getController
();
$request
=
$controller
->
getRequest
();
$viewer
=
$request
->
getViewer
();
$items
=
$this
->
loadItems
(
self
::
MODE_COMBINED
);
// To adjust the default item, we first change any existing items that
// are marked as defaults to "visible", then make the new default item
// the default.
$default
=
array
();
$visible
=
array
();
foreach
(
$items
as
$item
)
{
$builtin_key
=
$item
->
getBuiltinKey
();
$id
=
$item
->
getID
();
$is_target
=
((
$builtin_key
!==
null
)
&&
(
$builtin_key
===
$key
))
||
((
$id
!==
null
)
&&
((
int
)
$id
===
(
int
)
$key
));
if
(
$is_target
)
{
if
(!
$item
->
isDefault
())
{
$default
[]
=
$item
;
}
}
else
{
if
(
$item
->
isDefault
())
{
$visible
[]
=
$item
;
}
}
}
$type_visibility
=
PhabricatorProfileMenuItemConfigurationTransaction
::
TYPE_VISIBILITY
;
$v_visible
=
PhabricatorProfileMenuItemConfiguration
::
VISIBILITY_VISIBLE
;
$v_default
=
PhabricatorProfileMenuItemConfiguration
::
VISIBILITY_DEFAULT
;
$apply
=
array
(
array
(
$v_visible
,
$visible
),
array
(
$v_default
,
$default
),
);
foreach
(
$apply
as
$group
)
{
list
(
$value
,
$items
)
=
$group
;
foreach
(
$items
as
$item
)
{
$xactions
=
array
();
$xactions
[]
=
id
(
new
PhabricatorProfileMenuItemConfigurationTransaction
())
->
setTransactionType
(
$type_visibility
)
->
setNewValue
(
$value
);
$editor
=
id
(
new
PhabricatorProfileMenuEditor
())
->
setContentSourceFromRequest
(
$request
)
->
setActor
(
$viewer
)
->
setContinueOnMissingFields
(
true
)
->
setContinueOnNoEffect
(
true
)
->
applyTransactions
(
$item
,
$xactions
);
}
}
return
$this
;
}
private
function
arrangeItems
(
array
$items
,
$mode
)
{
// Sort the items.
$items
=
msortv
(
$items
,
'getSortVector'
);
$object
=
$this
->
getProfileObject
();
// If we have some global items and some custom items and are in "combined"
// mode, put a hard-coded divider item between them.
if
(
$mode
==
self
::
MODE_COMBINED
)
{
$list
=
array
();
$seen_custom
=
false
;
$seen_global
=
false
;
foreach
(
$items
as
$item
)
{
if
(
$item
->
getCustomPHID
())
{
$seen_custom
=
true
;
}
else
{
if
(
$seen_custom
&&
!
$seen_global
)
{
$list
[]
=
$this
->
newItem
()
->
setBuiltinKey
(
self
::
ITEM_CUSTOM_DIVIDER
)
->
setMenuItemKey
(
PhabricatorDividerProfileMenuItem
::
MENUITEMKEY
)
->
attachProfileObject
(
$object
)
->
attachMenuItem
(
new
PhabricatorDividerProfileMenuItem
());
}
$seen_global
=
true
;
}
$list
[]
=
$item
;
}
$items
=
$list
;
}
// Normalize keys since callers shouldn't rely on this array being
// partially keyed.
$items
=
array_values
(
$items
);
return
$items
;
}
final
protected
function
newEmptyView
(
$title
,
$message
)
{
return
id
(
new
PHUIInfoView
())
->
setTitle
(
$title
)
->
setSeverity
(
PHUIInfoView
::
SEVERITY_NODATA
)
->
setErrors
(
array
(
$message
,
));
}
protected
function
newNoMenuItemsView
()
{
return
$this
->
newEmptyView
(
pht
(
'No Menu Items'
),
pht
(
'There are no menu items.'
));
}
private
function
pickDefaultItem
(
array
$items
)
{
// Remove all the items which can not be the default item.
foreach
(
$items
as
$key
=>
$item
)
{
if
(!
$item
->
canMakeDefault
())
{
unset
(
$items
[
$key
]);
continue
;
}
if
(
$item
->
isDisabled
())
{
unset
(
$items
[
$key
]);
continue
;
}
}
// If this engine supports pinning items and a valid item is pinned,
// pick that item as the default.
if
(
$this
->
isMenuEnginePinnable
())
{
foreach
(
$items
as
$key
=>
$item
)
{
if
(
$item
->
isDefault
())
{
return
$item
;
}
}
}
// If we have some other valid items, pick the first one as the default.
if
(
$items
)
{
return
head
(
$items
);
}
return
null
;
}
private
function
pickSelectedItem
(
array
$items
,
$item_id
,
$is_view
)
{
if
(
strlen
(
$item_id
))
{
$item_id_int
=
(
int
)
$item_id
;
foreach
(
$items
as
$item
)
{
if
(
$item_id_int
)
{
if
((
int
)
$item
->
getID
()
===
$item_id_int
)
{
return
$item
;
}
}
$builtin_key
=
$item
->
getBuiltinKey
();
if
(
$builtin_key
===
(
string
)
$item_id
)
{
return
$item
;
}
}
// Nothing matches the selected item ID, so we don't have a valid
// selection.
return
null
;
}
if
(
$is_view
)
{
return
$this
->
pickDefaultItem
(
$items
);
}
return
null
;
}
private
function
pickHighlightedMenuItem
(
array
$items
,
PhabricatorProfileMenuItemConfiguration
$selected_item
=
null
)
{
assert_instances_of
(
$items
,
'PHUIListItemView'
);
$default_key
=
null
;
if
(
$selected_item
)
{
$default_key
=
$selected_item
->
getDefaultMenuItemKey
();
}
$controller
=
$this
->
getController
();
// In some rare cases, when like building the "Favorites" menu on a
// 404 page, we may not have a controller. Just accept whatever default
// behavior we'd otherwise end up with.
if
(!
$controller
)
{
return
$default_key
;
}
$request
=
$controller
->
getRequest
();
// See T12949. If one of the menu items is a link to the same URI that
// the page was accessed with, we want to highlight that item. For example,
// this allows you to add links to a menu that apply filters to a
// workboard.
$matches
=
array
();
foreach
(
$items
as
$item
)
{
$href
=
$item
->
getHref
();
if
(
$this
->
isMatchForRequestURI
(
$request
,
$href
))
{
$matches
[]
=
$item
;
}
}
foreach
(
$matches
as
$match
)
{
if
(
$match
->
getKey
()
===
$default_key
)
{
return
$default_key
;
}
}
if
(
$matches
)
{
return
head
(
$matches
)->
getKey
();
}
return
$default_key
;
}
private
function
isMatchForRequestURI
(
AphrontRequest
$request
,
$item_uri
)
{
$request_uri
=
$request
->
getAbsoluteRequestURI
();
$item_uri
=
new
PhutilURI
(
$item_uri
);
// If the request URI and item URI don't have matching paths, they
// do not match.
if
(
$request_uri
->
getPath
()
!==
$item_uri
->
getPath
())
{
return
false
;
}
// If the request URI and item URI don't have matching parameters, they
// also do not match. We're specifically trying to let "?filter=X" work
// on Workboards, among other use cases, so this is important.
$request_params
=
$request_uri
->
getQueryParamsAsPairList
();
$item_params
=
$item_uri
->
getQueryParamsAsPairList
();
if
(
$request_params
!==
$item_params
)
{
return
false
;
}
// If the paths and parameters match, the item domain must be: empty; or
// match the request domain; or match the production domain.
$request_domain
=
$request_uri
->
getDomain
();
$production_uri
=
PhabricatorEnv
::
getProductionURI
(
'/'
);
$production_domain
=
id
(
new
PhutilURI
(
$production_uri
))
->
getDomain
();
$allowed_domains
=
array
(
''
,
$request_domain
,
$production_domain
,
);
$allowed_domains
=
array_fuse
(
$allowed_domains
);
$item_domain
=
$item_uri
->
getDomain
();
$item_domain
=
(
string
)
$item_domain
;
if
(
isset
(
$allowed_domains
[
$item_domain
]))
{
return
true
;
}
return
false
;
}
}
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Sun, Jun 29, 1:55 PM (2 d)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
260101
Default Alt Text
PhabricatorProfileMenuEngine.php (41 KB)
Attached To
Mode
rP Phorge
Attached
Detach File
Event Timeline
Log In to Comment