Page Menu
Home
GnuPG
Search
Configure Global Search
Log In
Files
F34102333
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Size
40 KB
Subscribers
None
View Options
diff --git a/src/applications/project/editor/PhabricatorProjectTransactionEditor.php b/src/applications/project/editor/PhabricatorProjectTransactionEditor.php
index c9594122d..b1aab1e41 100644
--- a/src/applications/project/editor/PhabricatorProjectTransactionEditor.php
+++ b/src/applications/project/editor/PhabricatorProjectTransactionEditor.php
@@ -1,897 +1,898 @@
<?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;
$types[] = PhabricatorProjectTransaction::TYPE_HASWORKBOARD;
$types[] = PhabricatorProjectTransaction::TYPE_DEFAULT_SORT;
$types[] = PhabricatorProjectTransaction::TYPE_DEFAULT_FILTER;
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_HASWORKBOARD:
return (int)$object->getHasWorkboard();
case PhabricatorProjectTransaction::TYPE_PARENT:
case PhabricatorProjectTransaction::TYPE_MILESTONE:
return null;
case PhabricatorProjectTransaction::TYPE_DEFAULT_SORT:
return $object->getDefaultWorkboardSort();
case PhabricatorProjectTransaction::TYPE_DEFAULT_FILTER:
return $object->getDefaultWorkboardFilter();
}
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:
case PhabricatorProjectTransaction::TYPE_DEFAULT_SORT:
case PhabricatorProjectTransaction::TYPE_DEFAULT_FILTER:
return $xaction->getNewValue();
case PhabricatorProjectTransaction::TYPE_HASWORKBOARD:
return (int)$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;
case PhabricatorProjectTransaction::TYPE_HASWORKBOARD:
$object->setHasWorkboard($xaction->getNewValue());
return;
case PhabricatorProjectTransaction::TYPE_DEFAULT_SORT:
$object->setDefaultWorkboardSort($xaction->getNewValue());
return;
case PhabricatorProjectTransaction::TYPE_DEFAULT_FILTER:
$object->setDefaultWorkboardFilter($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);
+ if (!$this->getIsMilestone()) {
+ if ($old !== null) {
+ $this->addSlug($object, $old, false);
+ }
+ $this->addSlug($object, $new, 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:
case PhabricatorProjectTransaction::TYPE_HASWORKBOARD:
case PhabricatorProjectTransaction::TYPE_DEFAULT_SORT:
case PhabricatorProjectTransaction::TYPE_DEFAULT_FILTER:
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) {
// Herald rules may run on behalf of other users and need to execute
// membership checks against ancestors.
$project = id(new PhabricatorProjectQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs(array($object->getPHID()))
->needAncestorMembers(true)
->executeOne();
return id(new PhabricatorProjectHeraldAdapter())
->setProject($project);
}
}
diff --git a/src/applications/project/typeahead/PhabricatorProjectDatasource.php b/src/applications/project/typeahead/PhabricatorProjectDatasource.php
index 33fb8df0e..40d8dbb0e 100644
--- a/src/applications/project/typeahead/PhabricatorProjectDatasource.php
+++ b/src/applications/project/typeahead/PhabricatorProjectDatasource.php
@@ -1,94 +1,112 @@
<?php
final class PhabricatorProjectDatasource
extends PhabricatorTypeaheadDatasource {
public function getBrowseTitle() {
return pht('Browse Projects');
}
public function getPlaceholderText() {
return pht('Type a project name...');
}
public function getDatasourceApplicationClass() {
return 'PhabricatorProjectApplication';
}
public function loadResults() {
$viewer = $this->getViewer();
$raw_query = $this->getRawQuery();
// Allow users to type "#qa" or "qa" to find "Quality Assurance".
$raw_query = ltrim($raw_query, '#');
$tokens = self::tokenizeString($raw_query);
$query = id(new PhabricatorProjectQuery())
->needImages(true)
->needSlugs(true);
if ($tokens) {
$query->withNameTokens($tokens);
}
// If this is for policy selection, prevent users from using milestones.
$for_policy = $this->getParameter('policy');
if ($for_policy) {
$query->withIsMilestone(false);
}
+ $for_autocomplete = $this->getParameter('autocomplete');
+
$projs = $this->executeQuery($query);
$projs = mpull($projs, null, 'getPHID');
$must_have_cols = $this->getParameter('mustHaveColumns', false);
if ($must_have_cols) {
$columns = id(new PhabricatorProjectColumnQuery())
->setViewer($viewer)
->withProjectPHIDs(array_keys($projs))
->execute();
$has_cols = mgroup($columns, 'getProjectPHID');
} else {
$has_cols = array_fill_keys(array_keys($projs), true);
}
$results = array();
foreach ($projs as $proj) {
if (!isset($has_cols[$proj->getPHID()])) {
continue;
}
+
+ $slug = $proj->getPrimarySlug();
+ if (!strlen($slug)) {
+ foreach ($proj->getSlugs() as $slug_object) {
+ $slug = $slug_object->getSlug();
+ if (strlen($slug)) {
+ break;
+ }
+ }
+ }
+
+ // If we're building results for the autocompleter and this project
+ // doesn't have any usable slugs, don't return it as a result.
+ if ($for_autocomplete && !strlen($slug)) {
+ continue;
+ }
+
$closed = null;
if ($proj->isArchived()) {
$closed = pht('Archived');
}
$all_strings = mpull($proj->getSlugs(), 'getSlug');
$all_strings[] = $proj->getDisplayName();
$all_strings = implode(' ', $all_strings);
$proj_result = id(new PhabricatorTypeaheadResult())
->setName($all_strings)
->setDisplayName($proj->getDisplayName())
->setDisplayType($proj->getDisplayIconName())
->setURI($proj->getURI())
->setPHID($proj->getPHID())
->setIcon($proj->getDisplayIconIcon())
->setColor($proj->getColor())
->setPriorityType('proj')
->setClosed($closed);
- $slug = $proj->getPrimarySlug();
if (strlen($slug)) {
$proj_result->setAutocomplete('#'.$slug);
}
$proj_result->setImageURI($proj->getProfileImageURI());
$results[] = $proj_result;
}
return $results;
}
}
diff --git a/src/view/form/control/PhabricatorRemarkupControl.php b/src/view/form/control/PhabricatorRemarkupControl.php
index 11388d257..eeec4971d 100644
--- a/src/view/form/control/PhabricatorRemarkupControl.php
+++ b/src/view/form/control/PhabricatorRemarkupControl.php
@@ -1,286 +1,290 @@
<?php
final class PhabricatorRemarkupControl extends AphrontFormTextAreaControl {
private $disableMacro = false;
private $disableFullScreen = false;
public function setDisableMacros($disable) {
$this->disableMacro = $disable;
return $this;
}
public function setDisableFullScreen($disable) {
$this->disableFullScreen = $disable;
return $this;
}
protected function renderInput() {
$id = $this->getID();
if (!$id) {
$id = celerity_generate_unique_node_id();
$this->setID($id);
}
$viewer = $this->getUser();
if (!$viewer) {
throw new PhutilInvalidStateException('setUser');
}
// We need to have this if previews render images, since Ajax can not
// currently ship JS or CSS.
require_celerity_resource('lightbox-attachment-css');
if (!$this->getDisabled()) {
Javelin::initBehavior(
'aphront-drag-and-drop-textarea',
array(
'target' => $id,
'activatedClass' => 'aphront-textarea-drag-and-drop',
'uri' => '/file/dropupload/',
'chunkThreshold' => PhabricatorFileStorageEngine::getChunkThreshold(),
));
}
$root_id = celerity_generate_unique_node_id();
$user_datasource = new PhabricatorPeopleDatasource();
- $proj_datasource = new PhabricatorProjectDatasource();
+ $proj_datasource = id(new PhabricatorProjectDatasource())
+ ->setParameters(
+ array(
+ 'autocomplete' => 1,
+ ));
Javelin::initBehavior(
'phabricator-remarkup-assist',
array(
'pht' => array(
'bold text' => pht('bold text'),
'italic text' => pht('italic text'),
'monospaced text' => pht('monospaced text'),
'List Item' => pht('List Item'),
'Quoted Text' => pht('Quoted Text'),
'data' => pht('data'),
'name' => pht('name'),
'URL' => pht('URL'),
),
'disabled' => $this->getDisabled(),
'rootID' => $root_id,
'autocompleteMap' => (object)array(
64 => array( // "@"
'datasourceURI' => $user_datasource->getDatasourceURI(),
'headerIcon' => 'fa-user',
'headerText' => pht('Find User:'),
'hintText' => $user_datasource->getPlaceholderText(),
),
35 => array( // "#"
'datasourceURI' => $proj_datasource->getDatasourceURI(),
'headerIcon' => 'fa-briefcase',
'headerText' => pht('Find Project:'),
'hintText' => $proj_datasource->getPlaceholderText(),
),
),
));
Javelin::initBehavior('phabricator-tooltips', array());
$actions = array(
'fa-bold' => array(
'tip' => pht('Bold'),
'nodevice' => true,
),
'fa-italic' => array(
'tip' => pht('Italics'),
'nodevice' => true,
),
'fa-text-width' => array(
'tip' => pht('Monospaced'),
'nodevice' => true,
),
'fa-link' => array(
'tip' => pht('Link'),
'nodevice' => true,
),
array(
'spacer' => true,
'nodevice' => true,
),
'fa-list-ul' => array(
'tip' => pht('Bulleted List'),
'nodevice' => true,
),
'fa-list-ol' => array(
'tip' => pht('Numbered List'),
'nodevice' => true,
),
'fa-code' => array(
'tip' => pht('Code Block'),
'nodevice' => true,
),
'fa-quote-right' => array(
'tip' => pht('Quote'),
'nodevice' => true,
),
'fa-table' => array(
'tip' => pht('Table'),
'nodevice' => true,
),
'fa-cloud-upload' => array(
'tip' => pht('Upload File'),
),
);
$can_use_macros =
(!$this->disableMacro) &&
(function_exists('imagettftext'));
if ($can_use_macros) {
$can_use_macros = PhabricatorApplication::isClassInstalledForViewer(
'PhabricatorMacroApplication',
$viewer);
}
if ($can_use_macros) {
$actions[] = array(
'spacer' => true,
);
$actions['fa-meh-o'] = array(
'tip' => pht('Meme'),
);
}
$actions['fa-eye'] = array(
'tip' => pht('Preview'),
'align' => 'right',
);
$actions[] = array(
'spacer' => true,
'align' => 'right',
);
$actions['fa-life-bouy'] = array(
'tip' => pht('Help'),
'align' => 'right',
'href' => PhabricatorEnv::getDoclink('Remarkup Reference'),
);
if (!$this->disableFullScreen) {
$actions[] = array(
'spacer' => true,
'align' => 'right',
);
$actions['fa-arrows-alt'] = array(
'tip' => pht('Fullscreen Mode'),
'align' => 'right',
);
}
$buttons = array();
foreach ($actions as $action => $spec) {
$classes = array();
if (idx($spec, 'align') == 'right') {
$classes[] = 'remarkup-assist-right';
}
if (idx($spec, 'nodevice')) {
$classes[] = 'remarkup-assist-nodevice';
}
if (idx($spec, 'spacer')) {
$classes[] = 'remarkup-assist-separator';
$buttons[] = phutil_tag(
'span',
array(
'class' => implode(' ', $classes),
),
'');
continue;
} else {
$classes[] = 'remarkup-assist-button';
}
$href = idx($spec, 'href', '#');
if ($href == '#') {
$meta = array('action' => $action);
$mustcapture = true;
$target = null;
} else {
$meta = array();
$mustcapture = null;
$target = '_blank';
}
$content = null;
$tip = idx($spec, 'tip');
if ($tip) {
$meta['tip'] = $tip;
$content = javelin_tag(
'span',
array(
'aural' => true,
),
$tip);
}
$sigils = array();
$sigils[] = 'remarkup-assist';
if (!$this->getDisabled()) {
$sigils[] = 'has-tooltip';
}
$buttons[] = javelin_tag(
'a',
array(
'class' => implode(' ', $classes),
'href' => $href,
'sigil' => implode(' ', $sigils),
'meta' => $meta,
'mustcapture' => $mustcapture,
'target' => $target,
'tabindex' => -1,
),
phutil_tag(
'div',
array(
'class' =>
'remarkup-assist phui-icon-view phui-font-fa bluegrey '.$action,
),
$content));
}
$buttons = phutil_tag(
'div',
array(
'class' => 'remarkup-assist-bar',
),
$buttons);
$monospaced_textareas = null;
$monospaced_textareas_class = null;
$monospaced_textareas = $viewer
->loadPreferences()
->getPreference(
PhabricatorUserPreferences::PREFERENCE_MONOSPACED_TEXTAREAS);
if ($monospaced_textareas == 'enabled') {
$monospaced_textareas_class = 'PhabricatorMonospaced';
}
$this->setCustomClass(
'remarkup-assist-textarea '.$monospaced_textareas_class);
return javelin_tag(
'div',
array(
'sigil' => 'remarkup-assist-control',
'class' => $this->getDisabled() ? 'disabled-control' : null,
'id' => $root_id,
),
array(
$buttons,
parent::renderInput(),
));
}
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Thu, Dec 4, 1:00 PM (1 d, 13 h)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
ed/a0/611085dc593334077f068d314bfa
Attached To
rPHAB Phabricator
Event Timeline
Log In to Comment