diff --git a/resources/celerity/map.php b/resources/celerity/map.php --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -7,7 +7,7 @@ */ return array( 'names' => array( - 'core.pkg.css' => '6913fe66', + 'core.pkg.css' => 'c7fc5aec', 'core.pkg.js' => '10275c16', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => 'b3eea3f5', @@ -155,7 +155,7 @@ 'rsrc/css/phui/phui-spacing.css' => '042804d6', 'rsrc/css/phui/phui-status.css' => 'd5263e49', 'rsrc/css/phui/phui-tag-view.css' => '6bbd83e2', - 'rsrc/css/phui/phui-timeline-view.css' => '6e342216', + 'rsrc/css/phui/phui-timeline-view.css' => '8ea41b25', 'rsrc/css/phui/phui-two-column-view.css' => '9fb86c85', 'rsrc/css/phui/workboards/phui-workboard-color.css' => 'ac6fe6a7', 'rsrc/css/phui/workboards/phui-workboard.css' => 'e6d89647', @@ -860,7 +860,7 @@ 'phui-status-list-view-css' => 'd5263e49', 'phui-tag-view-css' => '6bbd83e2', 'phui-theme-css' => '027ba77e', - 'phui-timeline-view-css' => '6e342216', + 'phui-timeline-view-css' => '8ea41b25', 'phui-two-column-view-css' => '9fb86c85', 'phui-workboard-color-css' => 'ac6fe6a7', 'phui-workboard-view-css' => 'e6d89647', diff --git a/resources/sql/autopatches/20160616.phame.blog.header.1.sql b/resources/sql/autopatches/20160616.phame.blog.header.1.sql new file mode 100644 --- /dev/null +++ b/resources/sql/autopatches/20160616.phame.blog.header.1.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_phame.phame_blog + ADD headerImagePHID VARBINARY(64); diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -3777,6 +3777,7 @@ 'PhameBlogEditor' => 'applications/phame/editor/PhameBlogEditor.php', 'PhameBlogFeedController' => 'applications/phame/controller/blog/PhameBlogFeedController.php', 'PhameBlogFulltextEngine' => 'applications/phame/search/PhameBlogFulltextEngine.php', + 'PhameBlogHeaderPictureController' => 'applications/phame/controller/blog/PhameBlogHeaderPictureController.php', 'PhameBlogListController' => 'applications/phame/controller/blog/PhameBlogListController.php', 'PhameBlogListView' => 'applications/phame/view/PhameBlogListView.php', 'PhameBlogManageController' => 'applications/phame/controller/blog/PhameBlogManageController.php', @@ -8655,6 +8656,7 @@ 'PhameBlogEditor' => 'PhabricatorApplicationTransactionEditor', 'PhameBlogFeedController' => 'PhameBlogController', 'PhameBlogFulltextEngine' => 'PhabricatorFulltextEngine', + 'PhameBlogHeaderPictureController' => 'PhameBlogController', 'PhameBlogListController' => 'PhameBlogController', 'PhameBlogListView' => 'AphrontTagView', 'PhameBlogManageController' => 'PhameBlogController', diff --git a/src/applications/phame/application/PhabricatorPhameApplication.php b/src/applications/phame/application/PhabricatorPhameApplication.php --- a/src/applications/phame/application/PhabricatorPhameApplication.php +++ b/src/applications/phame/application/PhabricatorPhameApplication.php @@ -67,6 +67,7 @@ 'manage/(?P[^/]+)/' => 'PhameBlogManageController', 'feed/(?P[^/]+)/' => 'PhameBlogFeedController', 'picture/(?P[1-9]\d*)/' => 'PhameBlogProfilePictureController', + 'header/(?P[1-9]\d*)/' => 'PhameBlogHeaderPictureController', ), ) + $this->getResourceSubroutes(), ); diff --git a/src/applications/phame/controller/blog/PhameBlogHeaderPictureController.php b/src/applications/phame/controller/blog/PhameBlogHeaderPictureController.php new file mode 100644 --- /dev/null +++ b/src/applications/phame/controller/blog/PhameBlogHeaderPictureController.php @@ -0,0 +1,126 @@ +getViewer(); + $id = $request->getURIData('id'); + + $blog = id(new PhameBlogQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->needHeaderImage(true) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$blog) { + return new Aphront404Response(); + } + + $blog_uri = '/phame/blog/manage/'.$id; + + $supported_formats = PhabricatorFile::getTransformableImageFormats(); + $e_file = true; + $errors = array(); + $delete_header = ($request->getInt('delete') == 1); + + if ($request->isFormPost()) { + if ($request->getFileExists('header')) { + $file = PhabricatorFile::newFromPHPUpload( + $_FILES['header'], + array( + 'authorPHID' => $viewer->getPHID(), + 'canCDN' => true, + )); + } else if (!$delete_header) { + $e_file = pht('Required'); + $errors[] = pht( + 'You must choose a file when uploading a new blog header.'); + } + + if (!$errors && !$delete_header) { + if (!$file->isTransformableImage()) { + $e_file = pht('Not Supported'); + $errors[] = pht( + 'This server only supports these image formats: %s.', + implode(', ', $supported_formats)); + } + } + + if (!$errors) { + if ($delete_header) { + $blog->setHeaderImagePHID(null); + } else { + $blog->setHeaderImagePHID($file->getPHID()); + $file->attachToObject($blog->getPHID()); + } + $blog->save(); + return id(new AphrontRedirectResponse())->setURI($blog_uri); + } + } + + $title = pht('Edit Blog Header'); + + $upload_form = id(new AphrontFormView()) + ->setUser($viewer) + ->setEncType('multipart/form-data') + ->appendChild( + id(new AphrontFormFileControl()) + ->setName('header') + ->setLabel(pht('Upload Header')) + ->setError($e_file) + ->setCaption( + pht('Supported formats: %s', implode(', ', $supported_formats)))) + ->appendChild( + id(new AphrontFormCheckboxControl()) + ->setName('delete') + ->setLabel(pht('Delete Header')) + ->addCheckbox( + 'delete', + 1, + null, + null)) + ->appendChild( + id(new AphrontFormSubmitControl()) + ->addCancelButton($blog_uri) + ->setValue(pht('Upload Header'))); + + $upload_box = id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Upload New Header')) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->setForm($upload_form); + + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb( + pht('Blogs'), + $this->getApplicationURI('blog/')); + $crumbs->addTextCrumb( + $blog->getName(), + $this->getApplicationURI('blog/view/'.$id)); + $crumbs->addTextCrumb(pht('Blog Header')); + $crumbs->setBorder(true); + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Edit Blog Header')) + ->setHeaderIcon('fa-camera'); + + $view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setFooter(array( + $upload_box, + )); + + return $this->newPage() + ->setTitle($title) + ->setCrumbs($crumbs) + ->appendChild( + array( + $view, + )); + + } +} diff --git a/src/applications/phame/controller/blog/PhameBlogManageController.php b/src/applications/phame/controller/blog/PhameBlogManageController.php --- a/src/applications/phame/controller/blog/PhameBlogManageController.php +++ b/src/applications/phame/controller/blog/PhameBlogManageController.php @@ -14,6 +14,7 @@ ->setViewer($viewer) ->withIDs(array($id)) ->needProfileImage(true) + ->needHeaderImage(true) ->executeOne(); if (!$blog) { return new Aphront404Response(); @@ -40,6 +41,7 @@ $curtain = $this->buildCurtain($blog); $properties = $this->buildPropertyView($blog); + $file = $this->buildFileView($blog); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb( @@ -62,6 +64,7 @@ ->setHeader($header) ->setCurtain($curtain) ->addPropertySection(pht('Details'), $properties) + ->addPropertySection(pht('Header'), $file) ->setMainColumn( array( $timeline, @@ -159,6 +162,14 @@ $curtain->addAction( id(new PhabricatorActionView()) + ->setIcon('fa-camera') + ->setHref($this->getApplicationURI('blog/header/'.$blog->getID().'/')) + ->setName(pht('Edit Blog Header')) + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit)); + + $curtain->addAction( + id(new PhabricatorActionView()) ->setIcon('fa-picture-o') ->setHref($this->getApplicationURI('blog/picture/'.$blog->getID().'/')) ->setName(pht('Edit Blog Picture')) @@ -188,4 +199,24 @@ return $curtain; } + private function buildFileView( + PhameBlog $blog) { + $viewer = $this->getViewer(); + + $view = id(new PHUIPropertyListView()) + ->setUser($viewer); + + if ($blog->getHeaderImagePHID()) { + $view->addImageContent( + phutil_tag( + 'img', + array( + 'src' => $blog->getHeaderImageURI(), + 'class' => 'phabricator-image-macro-hero', + ))); + return $view; + } + return null; + } + } diff --git a/src/applications/phame/controller/blog/PhameBlogProfilePictureController.php b/src/applications/phame/controller/blog/PhameBlogProfilePictureController.php --- a/src/applications/phame/controller/blog/PhameBlogProfilePictureController.php +++ b/src/applications/phame/controller/blog/PhameBlogProfilePictureController.php @@ -3,10 +3,6 @@ final class PhameBlogProfilePictureController extends PhameBlogController { - public function shouldRequireAdmin() { - return false; - } - public function handleRequest(AphrontRequest $request) { $viewer = $request->getViewer(); $id = $request->getURIData('id'); diff --git a/src/applications/phame/query/PhameBlogQuery.php b/src/applications/phame/query/PhameBlogQuery.php --- a/src/applications/phame/query/PhameBlogQuery.php +++ b/src/applications/phame/query/PhameBlogQuery.php @@ -9,6 +9,7 @@ private $needBloggers; private $needProfileImage; + private $needHeaderImage; public function withIDs(array $ids) { $this->ids = $ids; @@ -35,6 +36,11 @@ return $this; } + public function needHeaderImage($need) { + $this->needHeaderImage = $need; + return $this; + } + public function newResultObject() { return new PhameBlog(); } @@ -107,6 +113,28 @@ $blog->attachProfileImageFile($file); } } + + if ($this->needHeaderImage) { + $file_phids = mpull($blogs, 'getHeaderImagePHID'); + $file_phids = array_filter($file_phids); + if ($file_phids) { + $files = id(new PhabricatorFileQuery()) + ->setParentQuery($this) + ->setViewer($this->getViewer()) + ->withPHIDs($file_phids) + ->execute(); + $files = mpull($files, null, 'getPHID'); + } else { + $files = array(); + } + + foreach ($blogs as $blog) { + $file = idx($files, $blog->getHeaderImagePHID()); + if ($file) { + $blog->attachHeaderImageFile($file); + } + } + } return $blogs; } diff --git a/src/applications/phame/storage/PhameBlog.php b/src/applications/phame/storage/PhameBlog.php --- a/src/applications/phame/storage/PhameBlog.php +++ b/src/applications/phame/storage/PhameBlog.php @@ -24,8 +24,10 @@ protected $status; protected $mailKey; protected $profileImagePHID; + protected $headerImagePHID; private $profileImageFile = self::ATTACHABLE; + private $headerImageFile = self::ATTACHABLE; const STATUS_ACTIVE = 'active'; const STATUS_ARCHIVED = 'archived'; @@ -43,6 +45,7 @@ 'status' => 'text32', 'mailKey' => 'bytes20', 'profileImagePHID' => 'phid?', + 'headerImagePHID' => 'phid?', // T6203/NULLABILITY // These policies should always be non-null. @@ -212,6 +215,19 @@ return $this->assertAttached($this->profileImageFile); } + public function getHeaderImageURI() { + return $this->getHeaderImageFile()->getBestURI(); + } + + public function attachHeaderImageFile(PhabricatorFile $file) { + $this->headerImageFile = $file; + return $this; + } + + public function getHeaderImageFile() { + return $this->assertAttached($this->headerImageFile); + } + /* -( PhabricatorPolicyInterface Implementation )-------------------------- */ diff --git a/webroot/rsrc/css/phui/phui-timeline-view.css b/webroot/rsrc/css/phui/phui-timeline-view.css --- a/webroot/rsrc/css/phui/phui-timeline-view.css +++ b/webroot/rsrc/css/phui/phui-timeline-view.css @@ -47,6 +47,7 @@ height: 9px; border-radius: 2px; margin-left: 76px; + margin-bottom: 20px; } .device-desktop .phui-timeline-wedge {