Page MenuHomestyx hydra

No OneTemporary

diff --git a/src/applications/project/editor/PhabricatorProjectTransactionEditor.php b/src/applications/project/editor/PhabricatorProjectTransactionEditor.php
index 867bf7e9fe..20eb2c42bf 100644
--- a/src/applications/project/editor/PhabricatorProjectTransactionEditor.php
+++ b/src/applications/project/editor/PhabricatorProjectTransactionEditor.php
@@ -1,858 +1,864 @@
<?php
final class PhabricatorProjectTransactionEditor
extends PhabricatorApplicationTransactionEditor {
private $isMilestone;
private function setIsMilestone($is_milestone) {
$this->isMilestone = $is_milestone;
return $this;
}
private function getIsMilestone() {
return $this->isMilestone;
}
public function getEditorApplicationClass() {
return 'PhabricatorProjectApplication';
}
public function getEditorObjectsDescription() {
return pht('Projects');
}
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = PhabricatorTransactions::TYPE_EDGE;
$types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
$types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;
$types[] = PhabricatorTransactions::TYPE_JOIN_POLICY;
$types[] = PhabricatorProjectTransaction::TYPE_NAME;
$types[] = PhabricatorProjectTransaction::TYPE_SLUGS;
$types[] = PhabricatorProjectTransaction::TYPE_STATUS;
$types[] = PhabricatorProjectTransaction::TYPE_IMAGE;
$types[] = PhabricatorProjectTransaction::TYPE_ICON;
$types[] = PhabricatorProjectTransaction::TYPE_COLOR;
$types[] = PhabricatorProjectTransaction::TYPE_LOCKED;
$types[] = PhabricatorProjectTransaction::TYPE_PARENT;
$types[] = PhabricatorProjectTransaction::TYPE_MILESTONE;
return $types;
}
protected function getCustomTransactionOldValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorProjectTransaction::TYPE_NAME:
return $object->getName();
case PhabricatorProjectTransaction::TYPE_SLUGS:
$slugs = $object->getSlugs();
$slugs = mpull($slugs, 'getSlug', 'getSlug');
unset($slugs[$object->getPrimarySlug()]);
return array_keys($slugs);
case PhabricatorProjectTransaction::TYPE_STATUS:
return $object->getStatus();
case PhabricatorProjectTransaction::TYPE_IMAGE:
return $object->getProfileImagePHID();
case PhabricatorProjectTransaction::TYPE_ICON:
return $object->getIcon();
case PhabricatorProjectTransaction::TYPE_COLOR:
return $object->getColor();
case PhabricatorProjectTransaction::TYPE_LOCKED:
return (int)$object->getIsMembershipLocked();
case PhabricatorProjectTransaction::TYPE_PARENT:
case PhabricatorProjectTransaction::TYPE_MILESTONE:
return null;
}
return parent::getCustomTransactionOldValue($object, $xaction);
}
protected function getCustomTransactionNewValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorProjectTransaction::TYPE_NAME:
case PhabricatorProjectTransaction::TYPE_STATUS:
case PhabricatorProjectTransaction::TYPE_IMAGE:
case PhabricatorProjectTransaction::TYPE_ICON:
case PhabricatorProjectTransaction::TYPE_COLOR:
case PhabricatorProjectTransaction::TYPE_LOCKED:
case PhabricatorProjectTransaction::TYPE_PARENT:
case PhabricatorProjectTransaction::TYPE_MILESTONE:
return $xaction->getNewValue();
case PhabricatorProjectTransaction::TYPE_SLUGS:
return $this->normalizeSlugs($xaction->getNewValue());
}
return parent::getCustomTransactionNewValue($object, $xaction);
}
protected function applyCustomInternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorProjectTransaction::TYPE_NAME:
$name = $xaction->getNewValue();
$object->setName($name);
if (!$this->getIsMilestone()) {
$object->setPrimarySlug(PhabricatorSlug::normalizeProjectSlug($name));
}
return;
case PhabricatorProjectTransaction::TYPE_SLUGS:
return;
case PhabricatorProjectTransaction::TYPE_STATUS:
$object->setStatus($xaction->getNewValue());
return;
case PhabricatorProjectTransaction::TYPE_IMAGE:
$object->setProfileImagePHID($xaction->getNewValue());
return;
case PhabricatorProjectTransaction::TYPE_ICON:
$object->setIcon($xaction->getNewValue());
return;
case PhabricatorProjectTransaction::TYPE_COLOR:
$object->setColor($xaction->getNewValue());
return;
case PhabricatorProjectTransaction::TYPE_LOCKED:
$object->setIsMembershipLocked($xaction->getNewValue());
return;
case PhabricatorProjectTransaction::TYPE_PARENT:
$object->setParentProjectPHID($xaction->getNewValue());
return;
case PhabricatorProjectTransaction::TYPE_MILESTONE:
$number = $object->getParentProject()->loadNextMilestoneNumber();
$object->setMilestoneNumber($number);
$object->setParentProjectPHID($xaction->getNewValue());
return;
}
return parent::applyCustomInternalTransaction($object, $xaction);
}
protected function applyCustomExternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$old = $xaction->getOldValue();
$new = $xaction->getNewValue();
switch ($xaction->getTransactionType()) {
case PhabricatorProjectTransaction::TYPE_NAME:
// First, add the old name as a secondary slug; this is helpful
// for renames and generally a good thing to do.
if ($old !== null) {
$this->addSlug($object, $old, false);
}
$this->addSlug($object, $new, false);
return;
case PhabricatorProjectTransaction::TYPE_SLUGS:
$old = $xaction->getOldValue();
$new = $xaction->getNewValue();
$add = array_diff($new, $old);
$rem = array_diff($old, $new);
foreach ($add as $slug) {
$this->addSlug($object, $slug, true);
}
$this->removeSlugs($object, $rem);
return;
case PhabricatorProjectTransaction::TYPE_STATUS:
case PhabricatorProjectTransaction::TYPE_IMAGE:
case PhabricatorProjectTransaction::TYPE_ICON:
case PhabricatorProjectTransaction::TYPE_COLOR:
case PhabricatorProjectTransaction::TYPE_LOCKED:
case PhabricatorProjectTransaction::TYPE_PARENT:
case PhabricatorProjectTransaction::TYPE_MILESTONE:
return;
}
return parent::applyCustomExternalTransaction($object, $xaction);
}
protected function validateAllTransactions(
PhabricatorLiskDAO $object,
array $xactions) {
$errors = array();
// Prevent creating projects which are both subprojects and milestones,
// since this does not make sense, won't work, and will break everything.
$parent_xaction = null;
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorProjectTransaction::TYPE_PARENT:
case PhabricatorProjectTransaction::TYPE_MILESTONE:
if ($xaction->getNewValue() === null) {
continue;
}
if (!$parent_xaction) {
$parent_xaction = $xaction;
continue;
}
$errors[] = new PhabricatorApplicationTransactionValidationError(
$xaction->getTransactionType(),
pht('Invalid'),
pht(
'When creating a project, specify a maximum of one parent '.
'project or milestone project. A project can not be both a '.
'subproject and a milestone.'),
$xaction);
break;
break;
}
}
$is_milestone = $this->getIsMilestone();
$is_parent = $object->getHasSubprojects();
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_EDGE:
$type = $xaction->getMetadataValue('edge:type');
if ($type != PhabricatorProjectProjectHasMemberEdgeType::EDGECONST) {
break;
}
if ($is_parent) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$xaction->getTransactionType(),
pht('Invalid'),
pht(
'You can not change members of a project with subprojects '.
'directly. Members of any subproject are automatically '.
'members of the parent project.'),
$xaction);
}
if ($is_milestone) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$xaction->getTransactionType(),
pht('Invalid'),
pht(
'You can not change members of a milestone. Members of the '.
'parent project are automatically members of the milestone.'),
$xaction);
}
break;
}
}
return $errors;
}
protected function validateTransaction(
PhabricatorLiskDAO $object,
$type,
array $xactions) {
$errors = parent::validateTransaction($object, $type, $xactions);
switch ($type) {
case PhabricatorProjectTransaction::TYPE_NAME:
$missing = $this->validateIsEmptyTextField(
$object->getName(),
$xactions);
if ($missing) {
$error = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Required'),
pht('Project name is required.'),
nonempty(last($xactions), null));
$error->setIsMissingFieldError(true);
$errors[] = $error;
}
if (!$xactions) {
break;
}
if ($this->getIsMilestone()) {
break;
}
$name = last($xactions)->getNewValue();
if (!PhabricatorSlug::isValidProjectSlug($name)) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
pht(
'Project names must contain at least one letter or number.'),
last($xactions));
break;
}
$slug = PhabricatorSlug::normalizeProjectSlug($name);
$slug_used_already = id(new PhabricatorProjectSlug())
->loadOneWhere('slug = %s', $slug);
if ($slug_used_already &&
$slug_used_already->getProjectPHID() != $object->getPHID()) {
$error = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Duplicate'),
pht(
'Project name generates the same hashtag ("%s") as another '.
'existing project. Choose a unique name.',
'#'.$slug),
nonempty(last($xactions), null));
$errors[] = $error;
}
break;
case PhabricatorProjectTransaction::TYPE_SLUGS:
if (!$xactions) {
break;
}
$slug_xaction = last($xactions);
$new = $slug_xaction->getNewValue();
$invalid = array();
foreach ($new as $slug) {
if (!PhabricatorSlug::isValidProjectSlug($slug)) {
$invalid[] = $slug;
}
}
if ($invalid) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
pht(
'Hashtags must contain at least one letter or number. %s '.
'project hashtag(s) are invalid: %s.',
phutil_count($invalid),
implode(', ', $invalid)),
$slug_xaction);
break;
}
$new = $this->normalizeSlugs($new);
if ($new) {
$slugs_used_already = id(new PhabricatorProjectSlug())
->loadAllWhere('slug IN (%Ls)', $new);
} else {
// The project doesn't have any extra slugs.
$slugs_used_already = array();
}
$slugs_used_already = mgroup($slugs_used_already, 'getProjectPHID');
foreach ($slugs_used_already as $project_phid => $used_slugs) {
if ($project_phid == $object->getPHID()) {
continue;
}
$used_slug_strs = mpull($used_slugs, 'getSlug');
$error = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
pht(
'%s project hashtag(s) are already used by other projects: %s.',
phutil_count($used_slug_strs),
implode(', ', $used_slug_strs)),
$slug_xaction);
$errors[] = $error;
}
break;
case PhabricatorProjectTransaction::TYPE_PARENT:
case PhabricatorProjectTransaction::TYPE_MILESTONE:
if (!$xactions) {
break;
}
$xaction = last($xactions);
$parent_phid = $xaction->getNewValue();
if (!$parent_phid) {
continue;
}
if (!$this->getIsNewObject()) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
pht(
'You can only set a parent or milestone project when creating a '.
'project for the first time.'),
$xaction);
break;
}
$projects = id(new PhabricatorProjectQuery())
->setViewer($this->requireActor())
->withPHIDs(array($parent_phid))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->execute();
if (!$projects) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
pht(
'Parent or milestone project PHID ("%s") must be the PHID of a '.
'valid, visible project which you have permission to edit.',
$parent_phid),
$xaction);
break;
}
$project = head($projects);
if ($project->isMilestone()) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
pht(
'Parent or milestone project PHID ("%s") must not be a '.
'milestone. Milestones may not have subprojects or milestones.',
$parent_phid),
$xaction);
break;
}
$limit = PhabricatorProject::getProjectDepthLimit();
if ($project->getProjectDepth() >= ($limit - 1)) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
pht(
'You can not create a subproject or mielstone under this parent '.
'because it would nest projects too deeply. The maximum '.
'nesting depth of projects is %s.',
new PhutilNumber($limit)),
$xaction);
break;
}
$object->attachParentProject($project);
break;
}
return $errors;
}
protected function requireCapabilities(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorProjectTransaction::TYPE_NAME:
case PhabricatorProjectTransaction::TYPE_STATUS:
case PhabricatorProjectTransaction::TYPE_IMAGE:
case PhabricatorProjectTransaction::TYPE_ICON:
case PhabricatorProjectTransaction::TYPE_COLOR:
PhabricatorPolicyFilter::requireCapability(
$this->requireActor(),
$object,
PhabricatorPolicyCapability::CAN_EDIT);
return;
case PhabricatorProjectTransaction::TYPE_LOCKED:
PhabricatorPolicyFilter::requireCapability(
$this->requireActor(),
newv($this->getEditorApplicationClass(), array()),
ProjectCanLockProjectsCapability::CAPABILITY);
return;
case PhabricatorTransactions::TYPE_EDGE:
switch ($xaction->getMetadataValue('edge:type')) {
case PhabricatorProjectProjectHasMemberEdgeType::EDGECONST:
$old = $xaction->getOldValue();
$new = $xaction->getNewValue();
$add = array_keys(array_diff_key($new, $old));
$rem = array_keys(array_diff_key($old, $new));
$actor_phid = $this->requireActor()->getPHID();
$is_join = (($add === array($actor_phid)) && !$rem);
$is_leave = (($rem === array($actor_phid)) && !$add);
if ($is_join) {
// You need CAN_JOIN to join a project.
PhabricatorPolicyFilter::requireCapability(
$this->requireActor(),
$object,
PhabricatorPolicyCapability::CAN_JOIN);
} else if ($is_leave) {
// You usually don't need any capabilities to leave a project.
if ($object->getIsMembershipLocked()) {
// you must be able to edit though to leave locked projects
PhabricatorPolicyFilter::requireCapability(
$this->requireActor(),
$object,
PhabricatorPolicyCapability::CAN_EDIT);
}
} else {
// You need CAN_EDIT to change members other than yourself.
PhabricatorPolicyFilter::requireCapability(
$this->requireActor(),
$object,
PhabricatorPolicyCapability::CAN_EDIT);
}
return;
}
break;
}
return parent::requireCapabilities($object, $xaction);
}
protected function willPublish(PhabricatorLiskDAO $object, array $xactions) {
// NOTE: We're using the omnipotent user here because the original actor
// may no longer have permission to view the object.
return id(new PhabricatorProjectQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs(array($object->getPHID()))
->needAncestorMembers(true)
->executeOne();
}
protected function shouldSendMail(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function getMailSubjectPrefix() {
return pht('[Project]');
}
protected function getMailTo(PhabricatorLiskDAO $object) {
return array(
$this->getActingAsPHID(),
);
}
protected function getMailCc(PhabricatorLiskDAO $object) {
return array();
}
public function getMailTagsMap() {
return array(
PhabricatorProjectTransaction::MAILTAG_METADATA =>
pht('Project name, hashtags, icon, image, or color changes.'),
PhabricatorProjectTransaction::MAILTAG_MEMBERS =>
pht('Project membership changes.'),
PhabricatorProjectTransaction::MAILTAG_WATCHERS =>
pht('Project watcher list changes.'),
PhabricatorProjectTransaction::MAILTAG_OTHER =>
pht('Other project activity not listed above occurs.'),
);
}
protected function buildReplyHandler(PhabricatorLiskDAO $object) {
return id(new ProjectReplyHandler())
->setMailReceiver($object);
}
protected function buildMailTemplate(PhabricatorLiskDAO $object) {
$id = $object->getID();
$name = $object->getName();
return id(new PhabricatorMetaMTAMail())
->setSubject("{$name}")
->addHeader('Thread-Topic', "Project {$id}");
}
protected function buildMailBody(
PhabricatorLiskDAO $object,
array $xactions) {
$body = parent::buildMailBody($object, $xactions);
$uri = '/project/profile/'.$object->getID().'/';
$body->addLinkSection(
pht('PROJECT DETAIL'),
PhabricatorEnv::getProductionURI($uri));
return $body;
}
protected function shouldPublishFeedStory(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function supportsSearch() {
return true;
}
protected function extractFilePHIDsFromCustomTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorProjectTransaction::TYPE_IMAGE:
$new = $xaction->getNewValue();
if ($new) {
return array($new);
}
break;
}
return parent::extractFilePHIDsFromCustomTransaction($object, $xaction);
}
protected function applyFinalEffects(
PhabricatorLiskDAO $object,
array $xactions) {
$materialize = false;
$new_parent = null;
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_EDGE:
switch ($xaction->getMetadataValue('edge:type')) {
case PhabricatorProjectProjectHasMemberEdgeType::EDGECONST:
$materialize = true;
break;
}
break;
case PhabricatorProjectTransaction::TYPE_PARENT:
+ case PhabricatorProjectTransaction::TYPE_MILESTONE:
$materialize = true;
$new_parent = $object->getParentProject();
break;
}
}
if ($new_parent) {
// If we just created the first subproject of this parent, we want to
// copy all of the real members to the subproject.
if (!$new_parent->getHasSubprojects()) {
$member_type = PhabricatorProjectProjectHasMemberEdgeType::EDGECONST;
$project_members = PhabricatorEdgeQuery::loadDestinationPHIDs(
$new_parent->getPHID(),
$member_type);
if ($project_members) {
$editor = id(new PhabricatorEdgeEditor());
foreach ($project_members as $phid) {
$editor->addEdge($object->getPHID(), $member_type, $phid);
}
$editor->save();
}
}
}
if ($this->getIsNewObject()) {
$this->setDefaultProfilePicture($object);
}
// TODO: We should dump an informational transaction onto the parent
// project to show that we created the sub-thing.
if ($materialize) {
id(new PhabricatorProjectsMembershipIndexEngineExtension())
->rematerialize($object);
}
+ if ($new_parent) {
+ id(new PhabricatorProjectsMembershipIndexEngineExtension())
+ ->rematerialize($new_parent);
+ }
+
return parent::applyFinalEffects($object, $xactions);
}
private function addSlug(PhabricatorProject $project, $slug, $force) {
$slug = PhabricatorSlug::normalizeProjectSlug($slug);
$table = new PhabricatorProjectSlug();
$project_phid = $project->getPHID();
if ($force) {
// If we have the `$force` flag set, we only want to ignore an existing
// slug if it's for the same project. We'll error on collisions with
// other projects.
$current = $table->loadOneWhere(
'slug = %s AND projectPHID = %s',
$slug,
$project_phid);
} else {
// Without the `$force` flag, we'll just return without doing anything
// if any other project already has the slug.
$current = $table->loadOneWhere(
'slug = %s',
$slug);
}
if ($current) {
return;
}
return id(new PhabricatorProjectSlug())
->setSlug($slug)
->setProjectPHID($project_phid)
->save();
}
private function removeSlugs(PhabricatorProject $project, array $slugs) {
if (!$slugs) {
return;
}
// We're going to try to delete both the literal and normalized versions
// of all slugs. This allows us to destroy old slugs that are no longer
// valid.
foreach ($this->normalizeSlugs($slugs) as $slug) {
$slugs[] = $slug;
}
$objects = id(new PhabricatorProjectSlug())->loadAllWhere(
'projectPHID = %s AND slug IN (%Ls)',
$project->getPHID(),
$slugs);
foreach ($objects as $object) {
$object->delete();
}
}
private function normalizeSlugs(array $slugs) {
foreach ($slugs as $key => $slug) {
$slugs[$key] = PhabricatorSlug::normalizeProjectSlug($slug);
}
$slugs = array_unique($slugs);
$slugs = array_values($slugs);
return $slugs;
}
protected function adjustObjectForPolicyChecks(
PhabricatorLiskDAO $object,
array $xactions) {
$copy = parent::adjustObjectForPolicyChecks($object, $xactions);
$type_edge = PhabricatorTransactions::TYPE_EDGE;
$edgetype_member = PhabricatorProjectProjectHasMemberEdgeType::EDGECONST;
$member_xaction = null;
foreach ($xactions as $xaction) {
if ($xaction->getTransactionType() !== $type_edge) {
continue;
}
$edgetype = $xaction->getMetadataValue('edge:type');
if ($edgetype !== $edgetype_member) {
continue;
}
$member_xaction = $xaction;
}
if ($member_xaction) {
$object_phid = $object->getPHID();
if ($object_phid) {
$project = id(new PhabricatorProjectQuery())
->setViewer($this->getActor())
->withPHIDs(array($object_phid))
->needMembers(true)
->executeOne();
$members = $project->getMemberPHIDs();
} else {
$members = array();
}
$clone_xaction = clone $member_xaction;
$hint = $this->getPHIDTransactionNewValue($clone_xaction, $members);
$rule = new PhabricatorProjectMembersPolicyRule();
$hint = array_fuse($hint);
PhabricatorPolicyRule::passTransactionHintToRule(
$copy,
$rule,
$hint);
}
return $copy;
}
protected function expandTransactions(
PhabricatorLiskDAO $object,
array $xactions) {
$actor = $this->getActor();
$actor_phid = $actor->getPHID();
$results = parent::expandTransactions($object, $xactions);
$is_milestone = $object->isMilestone();
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorProjectTransaction::TYPE_MILESTONE:
if ($xaction->getNewValue() !== null) {
$is_milestone = true;
}
break;
}
}
$this->setIsMilestone($is_milestone);
return $results;
}
private function setDefaultProfilePicture(PhabricatorProject $project) {
if ($project->isMilestone()) {
return;
}
$compose_color = $project->getDisplayIconComposeColor();
$compose_icon = $project->getDisplayIconComposeIcon();
$builtin = id(new PhabricatorFilesComposeIconBuiltinFile())
->setColor($compose_color)
->setIcon($compose_icon);
$data = $builtin->loadBuiltinFileData();
$file = PhabricatorFile::newFromFileData(
$data,
array(
'name' => $builtin->getBuiltinDisplayName(),
'profile' => true,
'canCDN' => true,
));
$project
->setProfileImagePHID($file->getPHID())
->save();
}
protected function shouldApplyHeraldRules(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function buildHeraldAdapter(
PhabricatorLiskDAO $object,
array $xactions) {
return id(new PhabricatorProjectHeraldAdapter())
->setProject($object);
}
}
diff --git a/src/applications/project/engine/PhabricatorBoardLayoutEngine.php b/src/applications/project/engine/PhabricatorBoardLayoutEngine.php
index 4415e2fdf9..cfd752abb3 100644
--- a/src/applications/project/engine/PhabricatorBoardLayoutEngine.php
+++ b/src/applications/project/engine/PhabricatorBoardLayoutEngine.php
@@ -1,582 +1,582 @@
<?php
final class PhabricatorBoardLayoutEngine extends Phobject {
private $viewer;
private $boardPHIDs;
private $objectPHIDs;
private $boards;
private $columnMap = array();
private $objectColumnMap = array();
private $boardLayout = array();
private $remQueue = array();
private $addQueue = array();
public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
public function getViewer() {
return $this->viewer;
}
public function setBoardPHIDs(array $board_phids) {
$this->boardPHIDs = array_fuse($board_phids);
return $this;
}
public function getBoardPHIDs() {
return $this->boardPHIDs;
}
public function setObjectPHIDs(array $object_phids) {
$this->objectPHIDs = array_fuse($object_phids);
return $this;
}
public function getObjectPHIDs() {
return $this->objectPHIDs;
}
public function executeLayout() {
$viewer = $this->getViewer();
$boards = $this->loadBoards();
if (!$boards) {
return $this;
}
$columns = $this->loadColumns($boards);
$positions = $this->loadPositions($boards);
foreach ($boards as $board_phid => $board) {
$board_columns = idx($columns, $board_phid);
// Don't layout boards with no columns. These boards need to be formally
// created first.
if (!$columns) {
continue;
}
$board_positions = idx($positions, $board_phid, array());
$this->layoutBoard($board, $board_columns, $board_positions);
}
return $this;
}
public function getColumns($board_phid) {
$columns = idx($this->boardLayout, $board_phid, array());
return array_select_keys($this->columnMap, array_keys($columns));
}
public function getColumnObjectPHIDs($board_phid, $column_phid) {
$columns = idx($this->boardLayout, $board_phid, array());
$positions = idx($columns, $column_phid, array());
return mpull($positions, 'getObjectPHID');
}
public function getObjectColumns($board_phid, $object_phid) {
$board_map = idx($this->objectColumnMap, $board_phid, array());
$column_phids = idx($board_map, $object_phid);
if (!$column_phids) {
return array();
}
return array_select_keys($this->columnMap, $column_phids);
}
public function queueRemovePosition(
$board_phid,
$column_phid,
$object_phid) {
$board_layout = idx($this->boardLayout, $board_phid, array());
$positions = idx($board_layout, $column_phid, array());
$position = idx($positions, $object_phid);
if ($position) {
$this->remQueue[] = $position;
// If this position hasn't been saved yet, get it out of the add queue.
if (!$position->getID()) {
foreach ($this->addQueue as $key => $add_position) {
if ($add_position === $position) {
unset($this->addQueue[$key]);
}
}
}
}
unset($this->boardLayout[$board_phid][$column_phid][$object_phid]);
return $this;
}
public function queueAddPositionBefore(
$board_phid,
$column_phid,
$object_phid,
$before_phid) {
return $this->queueAddPositionRelative(
$board_phid,
$column_phid,
$object_phid,
$before_phid,
true);
}
public function queueAddPositionAfter(
$board_phid,
$column_phid,
$object_phid,
$after_phid) {
return $this->queueAddPositionRelative(
$board_phid,
$column_phid,
$object_phid,
$after_phid,
false);
}
public function queueAddPosition(
$board_phid,
$column_phid,
$object_phid) {
return $this->queueAddPositionRelative(
$board_phid,
$column_phid,
$object_phid,
null,
true);
}
private function queueAddPositionRelative(
$board_phid,
$column_phid,
$object_phid,
$relative_phid,
$is_before) {
$board_layout = idx($this->boardLayout, $board_phid, array());
$positions = idx($board_layout, $column_phid, array());
// Check if the object is already in the column, and remove it if it is.
$object_position = idx($positions, $object_phid);
unset($positions[$object_phid]);
if (!$object_position) {
$object_position = id(new PhabricatorProjectColumnPosition())
->setBoardPHID($board_phid)
->setColumnPHID($column_phid)
->setObjectPHID($object_phid);
}
$found = false;
if (!$positions) {
$object_position->setSequence(0);
} else {
foreach ($positions as $position) {
if (!$found) {
if ($relative_phid === null) {
$is_match = true;
} else {
$position_phid = $position->getObjectPHID();
$is_match = ($relative_phid == $position_phid);
}
if ($is_match) {
$found = true;
$sequence = $position->getSequence();
if (!$is_before) {
$sequence++;
}
$object_position->setSequence($sequence++);
if (!$is_before) {
// If we're inserting after this position, continue the loop so
// we don't update it.
continue;
}
}
}
if ($found) {
$position->setSequence($sequence++);
$this->addQueue[] = $position;
}
}
}
if ($relative_phid && !$found) {
throw new Exception(
pht(
'Unable to find object "%s" in column "%s" on board "%s".',
$relative_phid,
$column_phid,
$board_phid));
}
$this->addQueue[] = $object_position;
$positions[$object_phid] = $object_position;
$positions = msort($positions, 'getOrderingKey');
$this->boardLayout[$board_phid][$column_phid] = $positions;
return $this;
}
public function applyPositionUpdates() {
foreach ($this->remQueue as $position) {
if ($position->getID()) {
$position->delete();
}
}
$this->remQueue = array();
$adds = array();
$updates = array();
foreach ($this->addQueue as $position) {
$id = $position->getID();
if ($id) {
$updates[$id] = $position;
} else {
$adds[] = $position;
}
}
$this->addQueue = array();
$table = new PhabricatorProjectColumnPosition();
$conn_w = $table->establishConnection('w');
$pairs = array();
foreach ($updates as $id => $position) {
// This is ugly because MySQL gets upset with us if it is configured
// strictly and we attempt inserts which can't work. We'll never actually
// do these inserts since they'll always collide (triggering the ON
// DUPLICATE KEY logic), so we just provide dummy values in order to get
// there.
$pairs[] = qsprintf(
$conn_w,
'(%d, %d, "", "", "")',
$id,
$position->getSequence());
}
if ($pairs) {
queryfx(
$conn_w,
'INSERT INTO %T (id, sequence, boardPHID, columnPHID, objectPHID)
VALUES %Q ON DUPLICATE KEY UPDATE sequence = VALUES(sequence)',
$table->getTableName(),
implode(', ', $pairs));
}
foreach ($adds as $position) {
$position->save();
}
return $this;
}
private function loadBoards() {
$viewer = $this->getViewer();
$board_phids = $this->getBoardPHIDs();
$boards = id(new PhabricatorObjectQuery())
->setViewer($viewer)
->withPHIDs($board_phids)
->execute();
$boards = mpull($boards, null, 'getPHID');
foreach ($boards as $key => $board) {
if (!$board->getHasWorkboard()) {
unset($boards[$key]);
}
}
return $boards;
}
private function loadColumns(array $boards) {
$viewer = $this->getViewer();
$columns = id(new PhabricatorProjectColumnQuery())
->setViewer($viewer)
->withProjectPHIDs(array_keys($boards))
->execute();
$columns = msort($columns, 'getOrderingKey');
$columns = mpull($columns, null, 'getPHID');
$need_children = array();
foreach ($boards as $phid => $board) {
if ($board->getHasMilestones() || $board->getHasSubprojects()) {
$need_children[] = $phid;
}
}
if ($need_children) {
$children = id(new PhabricatorProjectQuery())
->setViewer($viewer)
->withParentProjectPHIDs($need_children)
->execute();
$children = mpull($children, null, 'getPHID');
$children = mgroup($children, 'getParentProjectPHID');
} else {
$children = array();
}
$columns = mgroup($columns, 'getProjectPHID');
foreach ($boards as $board_phid => $board) {
$board_columns = idx($columns, $board_phid, array());
// If the project has milestones, create any missing columns.
if ($board->getHasMilestones() || $board->getHasSubprojects()) {
$child_projects = idx($children, $board_phid, array());
$next_sequence = last($board_columns)->getSequence() + 1;
$proxy_columns = mpull($board_columns, null, 'getProxyPHID');
foreach ($child_projects as $child_phid => $child) {
if (isset($proxy_columns[$child_phid])) {
continue;
}
$new_column = PhabricatorProjectColumn::initializeNewColumn($viewer)
->attachProject($board)
->attachProxy($child)
->setSequence($next_sequence++)
->setProjectPHID($board_phid)
->setProxyPHID($child_phid);
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$new_column->save();
unset($unguarded);
$board_columns[$new_column->getPHID()] = $new_column;
}
}
$board_columns = msort($board_columns, 'getOrderingKey');
$columns[$board_phid] = $board_columns;
}
foreach ($columns as $board_phid => $board_columns) {
foreach ($board_columns as $board_column) {
$column_phid = $board_column->getPHID();
$this->columnMap[$column_phid] = $board_column;
}
}
return $columns;
}
private function loadPositions(array $boards) {
$viewer = $this->getViewer();
$object_phids = $this->getObjectPHIDs();
if (!$object_phids) {
return array();
}
$positions = id(new PhabricatorProjectColumnPositionQuery())
->setViewer($viewer)
->withBoardPHIDs(array_keys($boards))
->withObjectPHIDs($object_phids)
->execute();
$positions = msort($positions, 'getOrderingKey');
$positions = mgroup($positions, 'getBoardPHID');
return $positions;
}
private function layoutBoard(
$board,
array $columns,
array $positions) {
$viewer = $this->getViewer();
$board_phid = $board->getPHID();
$position_groups = mgroup($positions, 'getObjectPHID');
$layout = array();
foreach ($columns as $column) {
$column_phid = $column->getPHID();
$layout[$column_phid] = array();
if ($column->isDefaultColumn()) {
$default_phid = $column_phid;
}
}
// Find all the columns which are proxies for other objects.
$proxy_map = array();
foreach ($columns as $column) {
$proxy_phid = $column->getProxyPHID();
if ($proxy_phid) {
$proxy_map[$proxy_phid] = $column->getPHID();
}
}
$object_phids = $this->getObjectPHIDs();
// If we have proxies, we need to force cards into the correct proxy
// columns.
- if ($proxy_map) {
+ if ($proxy_map && $object_phids) {
$edge_query = id(new PhabricatorEdgeQuery())
->withSourcePHIDs($object_phids)
->withEdgeTypes(
array(
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
));
$edge_query->execute();
$project_phids = $edge_query->getDestinationPHIDs();
$project_phids = array_fuse($project_phids);
} else {
$project_phids = array();
}
if ($project_phids) {
$projects = id(new PhabricatorProjectQuery())
->setViewer($viewer)
->withPHIDs($project_phids)
->execute();
$projects = mpull($projects, null, 'getPHID');
} else {
$projects = array();
}
// Build a map from every project that any task is tagged with to the
// ancestor project which has a column on this board, if one exists.
$ancestor_map = array();
foreach ($projects as $phid => $project) {
if (isset($proxy_map[$phid])) {
$ancestor_map[$phid] = $proxy_map[$phid];
} else {
$seen = array($phid);
foreach ($project->getAncestorProjects() as $ancestor) {
$ancestor_phid = $ancestor->getPHID();
$seen[] = $ancestor_phid;
if (isset($proxy_map[$ancestor_phid])) {
foreach ($seen as $project_phid) {
$ancestor_map[$project_phid] = $proxy_map[$ancestor_phid];
}
}
}
}
}
foreach ($object_phids as $object_phid) {
$positions = idx($position_groups, $object_phid, array());
// First, check for objects that have corresponding proxy columns. We're
// going to overwrite normal column positions if a tag belongs to a proxy
// column, since you can't be in normal columns if you're in proxy
// columns.
$proxy_hits = array();
if ($proxy_map) {
$object_project_phids = $edge_query->getDestinationPHIDs(
array(
$object_phid,
));
foreach ($object_project_phids as $project_phid) {
if (isset($ancestor_map[$project_phid])) {
$proxy_hits[] = $ancestor_map[$project_phid];
}
}
}
if ($proxy_hits) {
// TODO: For now, only one column hit is permissible.
$proxy_hits = array_slice($proxy_hits, 0, 1);
$proxy_hits = array_fuse($proxy_hits);
// Check the object positions: we hope to find a position in each
// column the object should be part of. We're going to drop any
// invalid positions and create new positions where positions are
// missing.
foreach ($positions as $key => $position) {
$column_phid = $position->getColumnPHID();
if (isset($proxy_hits[$column_phid])) {
// Valid column, mark the position as found.
unset($proxy_hits[$column_phid]);
} else {
// Invalid column, ignore the position.
unset($positions[$key]);
}
}
// Create new positions for anything we haven't found.
foreach ($proxy_hits as $proxy_hit) {
$new_position = id(new PhabricatorProjectColumnPosition())
->setBoardPHID($board_phid)
->setColumnPHID($proxy_hit)
->setObjectPHID($object_phid)
->setSequence(0);
$this->addQueue[] = $new_position;
$positions[] = $new_position;
}
} else {
// Ignore any positions in columns which no longer exist. We don't
// actively destory them because the rest of the code ignores them and
// there's no real need to destroy the data.
foreach ($positions as $key => $position) {
$column_phid = $position->getColumnPHID();
if (empty($columns[$column_phid])) {
unset($positions[$key]);
}
}
// If the object has no position, put it on the default column.
if (!$positions) {
$new_position = id(new PhabricatorProjectColumnPosition())
->setBoardPHID($board_phid)
->setColumnPHID($default_phid)
->setObjectPHID($object_phid)
->setSequence(0);
$this->addQueue[] = $new_position;
$positions = array(
$new_position,
);
}
}
foreach ($positions as $position) {
$column_phid = $position->getColumnPHID();
$layout[$column_phid][$object_phid] = $position;
}
}
foreach ($layout as $column_phid => $map) {
$map = msort($map, 'getOrderingKey');
$layout[$column_phid] = $map;
foreach ($map as $object_phid => $position) {
$this->objectColumnMap[$board_phid][$object_phid][] = $column_phid;
}
}
$this->boardLayout[$board_phid] = $layout;
}
}

File Metadata

Mime Type
text/x-diff
Expires
Mon, Jul 28, 9:06 AM (1 w, 23 h ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
184301
Default Alt Text
(44 KB)

Event Timeline