Page MenuHomePhorge

No OneTemporary

diff --git a/src/applications/project/editor/PhabricatorProjectTransactionEditor.php b/src/applications/project/editor/PhabricatorProjectTransactionEditor.php
index 0a03f55d7c..867bf7e9fe 100644
--- a/src/applications/project/editor/PhabricatorProjectTransactionEditor.php
+++ b/src/applications/project/editor/PhabricatorProjectTransactionEditor.php
@@ -1,874 +1,858 @@
<?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 PhabricatorProjectTransaction::TYPE_MEMBERS:
+ 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:
$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);
}
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);
- // Automatically add the author as a member when they create a project
- // if they're using the web interface.
-
- $content_source = $this->getContentSource();
- $source_web = PhabricatorContentSource::SOURCE_WEB;
- $is_web = ($content_source->getSource() === $source_web);
-
- if ($this->getIsNewObject() && $is_web) {
- if ($actor_phid) {
- $type_member = PhabricatorProjectProjectHasMemberEdgeType::EDGECONST;
-
- $results[] = id(new PhabricatorProjectTransaction())
- ->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
- ->setMetadataValue('edge:type', $type_member)
- ->setNewValue(
- array(
- '+' => array($actor_phid => $actor_phid),
- ));
- }
- }
-
$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/PhabricatorProjectEditEngine.php b/src/applications/project/engine/PhabricatorProjectEditEngine.php
index 95416989df..54144cbbb5 100644
--- a/src/applications/project/engine/PhabricatorProjectEditEngine.php
+++ b/src/applications/project/engine/PhabricatorProjectEditEngine.php
@@ -1,248 +1,291 @@
<?php
final class PhabricatorProjectEditEngine
extends PhabricatorEditEngine {
const ENGINECONST = 'projects.project';
private $parentProject;
private $milestoneProject;
public function setParentProject(PhabricatorProject $parent_project) {
$this->parentProject = $parent_project;
return $this;
}
public function getParentProject() {
return $this->parentProject;
}
public function setMilestoneProject(PhabricatorProject $milestone_project) {
$this->milestoneProject = $milestone_project;
return $this;
}
public function getMilestoneProject() {
return $this->milestoneProject;
}
public function getEngineName() {
return pht('Projects');
}
public function getSummaryHeader() {
return pht('Configure Project Forms');
}
public function getSummaryText() {
return pht('Configure forms for creating projects.');
}
public function getEngineApplicationClass() {
return 'PhabricatorProjectApplication';
}
protected function newEditableObject() {
$project = PhabricatorProject::initializeNewProject($this->getViewer());
$milestone = $this->getMilestoneProject();
if ($milestone) {
$default_name = pht(
'Milestone %s',
new PhutilNumber($milestone->loadNextMilestoneNumber()));
$project->setName($default_name);
}
return $project;
}
protected function newObjectQuery() {
return id(new PhabricatorProjectQuery())
->needSlugs(true);
}
protected function getObjectCreateTitleText($object) {
return pht('Create New Project');
}
protected function getObjectEditTitleText($object) {
return pht('Edit %s', $object->getName());
}
protected function getObjectEditShortText($object) {
return $object->getName();
}
protected function getObjectCreateShortText() {
return pht('Create Project');
}
protected function getObjectViewURI($object) {
if ($this->getIsCreate()) {
return $object->getURI();
} else {
$id = $object->getID();
return "/project/manage/{$id}/";
}
}
protected function getObjectCreateCancelURI($object) {
$parent = $this->getParentProject();
if ($parent) {
$id = $parent->getID();
return "/project/subprojects/{$id}/";
}
$milestone = $this->getMilestoneProject();
if ($milestone) {
$id = $milestone->getID();
return "/project/milestones/{$id}/";
}
return parent::getObjectCreateCancelURI($object);
}
protected function getCreateNewObjectPolicy() {
return $this->getApplication()->getPolicy(
ProjectCreateProjectsCapability::CAPABILITY);
}
protected function willConfigureFields($object, array $fields) {
$is_milestone = ($this->getMilestoneProject() || $object->isMilestone());
$unavailable = array(
PhabricatorTransactions::TYPE_VIEW_POLICY,
PhabricatorTransactions::TYPE_EDIT_POLICY,
PhabricatorTransactions::TYPE_JOIN_POLICY,
PhabricatorProjectTransaction::TYPE_ICON,
PhabricatorProjectTransaction::TYPE_COLOR,
);
$unavailable = array_fuse($unavailable);
if ($is_milestone) {
foreach ($fields as $key => $field) {
$xaction_type = $field->getTransactionType();
if (isset($unavailable[$xaction_type])) {
unset($fields[$key]);
}
}
}
return $fields;
}
protected function newBuiltinEngineConfigurations() {
$configuration = head(parent::newBuiltinEngineConfigurations());
// TODO: This whole method is clumsy, and the ordering for the custom
// field is especially clumsy. Maybe try to make this more natural to
// express.
$configuration
->setFieldOrder(
array(
'parent',
'milestone',
'name',
'std:project:internal:description',
'icon',
'color',
'slugs',
));
return array(
$configuration,
);
}
protected function buildCustomEditFields($object) {
$slugs = mpull($object->getSlugs(), 'getSlug');
$slugs = array_fuse($slugs);
unset($slugs[$object->getPrimarySlug()]);
$slugs = array_values($slugs);
$milestone = $this->getMilestoneProject();
$parent = $this->getParentProject();
if ($parent) {
$parent_phid = $parent->getPHID();
} else {
$parent_phid = null;
}
if ($milestone) {
$milestone_phid = $milestone->getPHID();
} else {
$milestone_phid = null;
}
- return array(
+ $fields = array(
id(new PhabricatorHandlesEditField())
->setKey('parent')
->setLabel(pht('Parent'))
->setDescription(pht('Create a subproject of an existing project.'))
->setConduitDescription(
pht('Choose a parent project to create a subproject beneath.'))
->setConduitTypeDescription(pht('PHID of the parent project.'))
->setAliases(array('parentPHID'))
->setTransactionType(PhabricatorProjectTransaction::TYPE_PARENT)
->setHandleParameterType(new AphrontPHIDHTTPParameterType())
->setSingleValue($parent_phid)
->setIsReorderable(false)
->setIsDefaultable(false)
->setIsLockable(false)
->setIsLocked(true),
id(new PhabricatorHandlesEditField())
->setKey('milestone')
->setLabel(pht('Milestone Of'))
->setDescription(pht('Parent project to create a milestone for.'))
->setConduitDescription(
pht('Choose a parent project to create a new milestone for.'))
->setConduitTypeDescription(pht('PHID of the parent project.'))
->setAliases(array('milestonePHID'))
->setTransactionType(PhabricatorProjectTransaction::TYPE_MILESTONE)
->setHandleParameterType(new AphrontPHIDHTTPParameterType())
->setSingleValue($milestone_phid)
->setIsReorderable(false)
->setIsDefaultable(false)
->setIsLockable(false)
->setIsLocked(true),
id(new PhabricatorTextEditField())
->setKey('name')
->setLabel(pht('Name'))
->setTransactionType(PhabricatorProjectTransaction::TYPE_NAME)
->setIsRequired(true)
->setDescription(pht('Project name.'))
->setConduitDescription(pht('Rename the project'))
->setConduitTypeDescription(pht('New project name.'))
->setValue($object->getName()),
id(new PhabricatorIconSetEditField())
->setKey('icon')
->setLabel(pht('Icon'))
->setTransactionType(PhabricatorProjectTransaction::TYPE_ICON)
->setIconSet(new PhabricatorProjectIconSet())
->setDescription(pht('Project icon.'))
->setConduitDescription(pht('Change the project icon.'))
->setConduitTypeDescription(pht('New project icon.'))
->setValue($object->getIcon()),
id(new PhabricatorSelectEditField())
->setKey('color')
->setLabel(pht('Color'))
->setTransactionType(PhabricatorProjectTransaction::TYPE_COLOR)
->setOptions(PhabricatorProjectIconSet::getColorMap())
->setDescription(pht('Project tag color.'))
->setConduitDescription(pht('Change the project tag color.'))
->setConduitTypeDescription(pht('New project tag color.'))
->setValue($object->getColor()),
id(new PhabricatorStringListEditField())
->setKey('slugs')
->setLabel(pht('Additional Hashtags'))
->setTransactionType(PhabricatorProjectTransaction::TYPE_SLUGS)
->setDescription(pht('Additional project slugs.'))
->setConduitDescription(pht('Change project slugs.'))
->setConduitTypeDescription(pht('New list of slugs.'))
->setValue($slugs),
);
+
+ $can_edit_members = (!$milestone) &&
+ (!$object->isMilestone()) &&
+ (!$object->getHasSubprojects());
+
+ if ($can_edit_members) {
+
+ // Show this on the web UI when creating a project, but not when editing
+ // one. It is always available via Conduit.
+ $conduit_only = !$this->getIsCreate();
+
+ $members_field = id(new PhabricatorUsersEditField())
+ ->setKey('members')
+ ->setAliases(array('memberPHIDs'))
+ ->setLabel(pht('Initial Members'))
+ ->setIsConduitOnly($conduit_only)
+ ->setUseEdgeTransactions(true)
+ ->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
+ ->setMetadataValue(
+ 'edge:type',
+ PhabricatorProjectProjectHasMemberEdgeType::EDGECONST)
+ ->setDescription(pht('Initial project members.'))
+ ->setConduitDescription(pht('Set project members.'))
+ ->setConduitTypeDescription(pht('New list of members.'))
+ ->setValue(array());
+
+ $members_field->setViewer($this->getViewer());
+
+ $edit_add = $members_field->getConduitEditType('members.add')
+ ->setConduitDescription(pht('Add members.'));
+
+ $edit_set = $members_field->getConduitEditType('members.set')
+ ->setConduitDescription(
+ pht('Set members, overwriting the current value.'));
+
+ $edit_rem = $members_field->getConduitEditType('members.remove')
+ ->setConduitDescription(pht('Remove members.'));
+
+ $fields[] = $members_field;
+ }
+
+ return $fields;
+
}
}

File Metadata

Mime Type
text/x-diff
Expires
Sun, Jan 19, 19:07 (1 w, 3 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1127709
Default Alt Text
(37 KB)

Event Timeline