diff --git a/src/applications/project/controller/PhabricatorProjectProfilePictureController.php b/src/applications/project/controller/PhabricatorProjectProfilePictureController.php index a523945792..a821bb1e60 100644 --- a/src/applications/project/controller/PhabricatorProjectProfilePictureController.php +++ b/src/applications/project/controller/PhabricatorProjectProfilePictureController.php @@ -1,258 +1,270 @@ id = $data['id']; } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $project = id(new PhabricatorProjectQuery()) ->setViewer($viewer) ->withIDs(array($this->id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$project) { return new Aphront404Response(); } $project_uri = $this->getApplicationURI('view/'.$project->getID().'/'); $supported_formats = PhabricatorFile::getTransformableImageFormats(); $e_file = true; $errors = array(); if ($request->isFormPost()) { $phid = $request->getStr('phid'); $is_default = false; if ($phid == PhabricatorPHIDConstants::PHID_VOID) { $phid = null; $is_default = true; } else if ($phid) { $file = id(new PhabricatorFileQuery()) ->setViewer($viewer) ->withPHIDs(array($phid)) ->executeOne(); } else { if ($request->getFileExists('picture')) { $file = PhabricatorFile::newFromPHPUpload( $_FILES['picture'], array( 'authorPHID' => $viewer->getPHID(), )); } else { $e_file = pht('Required'); $errors[] = pht( 'You must choose a file when uploading a new project picture.'); } } if (!$errors && !$is_default) { if (!$file->isTransformableImage()) { $e_file = pht('Not Supported'); $errors[] = pht( 'This server only supports these image formats: %s.', implode(', ', $supported_formats)); } else { $xformer = new PhabricatorImageTransformer(); $xformed = $xformer->executeProfileTransform( $file, $width = 50, $min_height = 50, $max_height = 50); } } if (!$errors) { if ($is_default) { - $project->setProfileImagePHID(null); + $new_value = null; } else { - $project->setProfileImagePHID($xformed->getPHID()); - $xformed->attachToObject($viewer, $project->getPHID()); + $new_value = $xformed->getPHID(); } - $project->save(); + + $xactions = array(); + $xactions[] = id(new PhabricatorProjectTransaction()) + ->setTransactionType(PhabricatorProjectTransaction::TYPE_IMAGE) + ->setNewValue($new_value); + + $editor = id(new PhabricatorProjectTransactionEditor()) + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnMissingFields(true) + ->setContinueOnNoEffect(true); + + $editor->applyTransactions($project, $xactions); + return id(new AphrontRedirectResponse())->setURI($project_uri); } } $title = pht('Edit Project Picture'); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb($project->getName(), $project_uri); $crumbs->addTextCrumb($title); $form = id(new PHUIFormLayoutView()) ->setUser($viewer); $default_image = PhabricatorFile::loadBuiltin($viewer, 'project.png'); $images = array(); $current = $project->getProfileImagePHID(); $has_current = false; if ($current) { $files = id(new PhabricatorFileQuery()) ->setViewer($viewer) ->withPHIDs(array($current)) ->execute(); if ($files) { $file = head($files); if ($file->isTransformableImage()) { $has_current = true; $images[$current] = array( 'uri' => $file->getBestURI(), 'tip' => pht('Current Picture'), ); } } } $images[PhabricatorPHIDConstants::PHID_VOID] = array( 'uri' => $default_image->getBestURI(), 'tip' => pht('Default Picture'), ); require_celerity_resource('people-profile-css'); Javelin::initBehavior('phabricator-tooltips', array()); $buttons = array(); foreach ($images as $phid => $spec) { $button = javelin_tag( 'button', array( 'class' => 'grey profile-image-button', 'sigil' => 'has-tooltip', 'meta' => array( 'tip' => $spec['tip'], 'size' => 300, ), ), phutil_tag( 'img', array( 'height' => 50, 'width' => 50, 'src' => $spec['uri'], ))); $button = array( phutil_tag( 'input', array( 'type' => 'hidden', 'name' => 'phid', 'value' => $phid, )), $button); $button = phabricator_form( $viewer, array( 'class' => 'profile-image-form', 'method' => 'POST', ), $button); $buttons[] = $button; } if ($has_current) { $form->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('Current Picture')) ->setValue(array_shift($buttons))); } $form->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('Use Picture')) ->setValue($buttons)); $launch_id = celerity_generate_unique_node_id(); $input_id = celerity_generate_unique_node_id(); Javelin::initBehavior( 'launch-icon-composer', array( 'launchID' => $launch_id, 'inputID' => $input_id, )); $compose_button = javelin_tag( 'button', array( 'class' => 'grey', 'id' => $launch_id, 'sigil' => 'icon-composer', ), pht('Choose Icon and Color...')); $compose_input = javelin_tag( 'input', array( 'type' => 'hidden', 'id' => $input_id, 'name' => 'phid', )); $compose_form = phabricator_form( $viewer, array( 'class' => 'profile-image-form', 'method' => 'POST', ), array( $compose_input, $compose_button, )); $form->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('Quick Create')) ->setValue($compose_form)); $upload_form = id(new AphrontFormView()) ->setUser($viewer) ->setEncType('multipart/form-data') ->appendChild( id(new AphrontFormFileControl()) ->setName('picture') ->setLabel(pht('Upload Picture')) ->setError($e_file) ->setCaption( pht('Supported formats: %s', implode(', ', $supported_formats)))) ->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton($project_uri) ->setValue(pht('Upload Picture'))); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setFormErrors($errors) ->setForm($form); $upload_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Upload New Picture')) ->setForm($upload_form); return $this->buildApplicationPage( array( $crumbs, $form_box, $upload_box, ), array( 'title' => $title, 'device' => true, )); } } diff --git a/src/applications/project/editor/PhabricatorProjectTransactionEditor.php b/src/applications/project/editor/PhabricatorProjectTransactionEditor.php index 2230746f3f..43cc9924a8 100644 --- a/src/applications/project/editor/PhabricatorProjectTransactionEditor.php +++ b/src/applications/project/editor/PhabricatorProjectTransactionEditor.php @@ -1,205 +1,230 @@ getTransactionType()) { case PhabricatorProjectTransaction::TYPE_NAME: return $object->getName(); case PhabricatorProjectTransaction::TYPE_STATUS: return $object->getStatus(); + case PhabricatorProjectTransaction::TYPE_IMAGE: + return $object->getProfileImagePHID(); } 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: return $xaction->getNewValue(); } return parent::getCustomTransactionNewValue($object, $xaction); } protected function applyCustomInternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorProjectTransaction::TYPE_NAME: $object->setName($xaction->getNewValue()); return; case PhabricatorProjectTransaction::TYPE_STATUS: $object->setStatus($xaction->getNewValue()); return; + case PhabricatorProjectTransaction::TYPE_IMAGE: + $object->setProfileImagePHID($xaction->getNewValue()); + return; case PhabricatorTransactions::TYPE_EDGE: return; case PhabricatorTransactions::TYPE_VIEW_POLICY: $object->setViewPolicy($xaction->getNewValue()); return; case PhabricatorTransactions::TYPE_EDIT_POLICY: $object->setEditPolicy($xaction->getNewValue()); return; case PhabricatorTransactions::TYPE_JOIN_POLICY: $object->setJoinPolicy($xaction->getNewValue()); return; } return parent::applyCustomInternalTransaction($object, $xaction); } protected function applyCustomExternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorProjectTransaction::TYPE_NAME: $old_slug = $object->getFullPhrictionSlug(); $object->setPhrictionSlug($xaction->getNewValue()); $changed_slug = $old_slug != $object->getFullPhrictionSlug(); if ($xaction->getOldValue() && $changed_slug) { $old_document = id(new PhrictionDocument()) ->loadOneWhere( 'slug = %s', $old_slug); if ($old_document && $old_document->getStatus() == PhrictionDocumentStatus::STATUS_EXISTS) { $content = id(new PhrictionContent()) ->load($old_document->getContentID()); $from_editor = id(PhrictionDocumentEditor::newForSlug($old_slug)) ->setActor($this->getActor()) ->setTitle($content->getTitle()) ->setContent($content->getContent()) ->setDescription($content->getDescription()); $target_editor = id(PhrictionDocumentEditor::newForSlug( $object->getFullPhrictionSlug())) ->setActor($this->getActor()) ->setTitle($content->getTitle()) ->setContent($content->getContent()) ->setDescription($content->getDescription()) ->moveHere($old_document->getID(), $old_document->getPHID()); $target_document = $target_editor->getDocument(); $from_editor->moveAway($target_document->getID()); } } return; case PhabricatorTransactions::TYPE_VIEW_POLICY: case PhabricatorTransactions::TYPE_EDIT_POLICY: case PhabricatorTransactions::TYPE_JOIN_POLICY: case PhabricatorTransactions::TYPE_EDGE: case PhabricatorProjectTransaction::TYPE_STATUS: + case PhabricatorProjectTransaction::TYPE_IMAGE: return; } return parent::applyCustomExternalTransaction($object, $xaction); } 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; } 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: PhabricatorPolicyFilter::requireCapability( $this->requireActor(), $object, PhabricatorPolicyCapability::CAN_EDIT); return; case PhabricatorTransactions::TYPE_EDGE: switch ($xaction->getMetadataValue('edge:type')) { case PhabricatorEdgeConfig::TYPE_PROJ_MEMBER: $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 don't need any capabilities to leave a project. } 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 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); + } + } diff --git a/src/applications/project/storage/PhabricatorProjectTransaction.php b/src/applications/project/storage/PhabricatorProjectTransaction.php index 040e676e3f..b31ff340a8 100644 --- a/src/applications/project/storage/PhabricatorProjectTransaction.php +++ b/src/applications/project/storage/PhabricatorProjectTransaction.php @@ -1,105 +1,130 @@ getOldValue(); $new = $this->getNewValue(); $req_phids = array(); switch ($this->getTransactionType()) { case PhabricatorProjectTransaction::TYPE_MEMBERS: $add = array_diff($new, $old); $rem = array_diff($old, $new); $req_phids = array_merge($add, $rem); break; + case PhabricatorProjectTransaction::TYPE_IMAGE: + $req_phids[] = $old; + $req_phids[] = $new; + break; } return array_merge($req_phids, parent::getRequiredHandlePHIDs()); } public function getTitle() { $old = $this->getOldValue(); $new = $this->getNewValue(); $author_handle = $this->renderHandleLink($this->getAuthorPHID()); switch ($this->getTransactionType()) { case PhabricatorProjectTransaction::TYPE_NAME: if ($old === null) { return pht( '%s created this project.', $author_handle); } else { return pht( '%s renamed this project from "%s" to "%s".', $author_handle, $old, $new); } case PhabricatorProjectTransaction::TYPE_STATUS: if ($old == 0) { return pht( '%s closed this project.', $author_handle); } else { return pht( '%s reopened this project.', $author_handle); } + case PhabricatorProjectTransaction::TYPE_IMAGE: + // TODO: Some day, it would be nice to show the images. + if (!$old) { + return pht( + '%s set this project\'s image to %s.', + $author_handle, + $this->renderHandleLink($new)); + } else if (!$new) { + return pht( + '%s removed this project\'s image.', + $author_handle); + } else { + return pht( + '%s updated this project\'s image from %s to %s.', + $author_handle, + $this->renderHandleLink($old), + $this->renderHandleLink($new)); + } case PhabricatorProjectTransaction::TYPE_MEMBERS: $add = array_diff($new, $old); $rem = array_diff($old, $new); if ($add && $rem) { return pht( '%s changed project member(s), added %d: %s; removed %d: %s', $author_handle, count($add), $this->renderHandleList($add), count($rem), $this->renderHandleList($rem)); } else if ($add) { if (count($add) == 1 && (head($add) == $this->getAuthorPHID())) { return pht( '%s joined this project.', $author_handle); } else { return pht( '%s added %d project member(s): %s', $author_handle, count($add), $this->renderHandleList($add)); } } else if ($rem) { if (count($rem) == 1 && (head($rem) == $this->getAuthorPHID())) { return pht( '%s left this project.', $author_handle); } else { return pht( '%s removed %d project member(s): %s', $author_handle, count($rem), $this->renderHandleList($rem)); } } } return parent::getTitle(); } }