diff --git a/scripts/celerity/generate_sprites.php b/scripts/celerity/generate_sprites.php index 32be7420f9..df40173c85 100755 --- a/scripts/celerity/generate_sprites.php +++ b/scripts/celerity/generate_sprites.php @@ -1,176 +1,177 @@ #!/usr/bin/env php setTagline('regenerate CSS sprite sheets'); $args->setSynopsis(<<parseStandardArguments(); $args->parse( array( array( 'name' => 'source', 'param' => 'directory', 'help' => 'Directory with sprite sources.', ) )); $srcroot = $args->getArg('source'); if (!$srcroot) { throw new Exception( "You must specify a source directory with '--source'."); } $webroot = dirname(phutil_get_library_root('phabricator')).'/webroot/rsrc'; $webroot = Filesystem::readablePath($webroot); function glx($x) { return (60 + (48 * $x)); } function gly($y) { return (110 + (48 * $y)); } $sheet = new PhutilSpriteSheet(); $sheet->setCSSHeader(<<setSourceFile($srcroot.'/menu_normal_1x.png') ->setSourceSize(26, 26); $menu_hover_template = id(new PhutilSprite()) ->setSourceFile($srcroot.'/menu_hover_1x.png') ->setSourceSize(26, 26); $menu_selected_template = id(new PhutilSprite()) ->setSourceFile($srcroot.'/menu_selected_1x.png') ->setSourceSize(26, 26); $menu_map = array( '' => $menu_normal_template, '-selected' => $menu_selected_template, ':hover' => $menu_hover_template, ); $icon_map = array( 'help' => array(4, 19), 'settings' => array(0, 28), 'logout' => array(3, 6), 'notifications' => array(5, 20), 'task' => array(1, 15), ); foreach ($icon_map as $icon => $coords) { list($x, $y) = $coords; foreach ($menu_map as $suffix => $template) { $sheet->addSprite( id(clone $template) ->setSourcePosition(glx($x), gly($y)) ->setTargetCSS('.main-menu-item-icon-'.$icon.$suffix)); } } $app_template_full = id(new PhutilSprite()) ->setSourceFile($srcroot.'/application_normal_2x.png') ->setSourceSize(60, 60); $app_template_mini = id(new PhutilSprite()) ->setSourceFile($srcroot.'/menu_normal_1x.png') ->setSourceSize(30, 30); $app_source_map = array( '-full' => array($app_template_full, 2), '' => array($app_template_mini, 1), ); $app_map = array( 'differential' => array(9, 1), 'fact' => array(2, 4), 'mail' => array(0, 1), 'diffusion' => array(7, 13), 'slowvote' => array(1, 4), 'phriction' => array(1, 7), 'maniphest' => array(3, 24), 'flags' => array(6, 26), 'settings' => array(9, 11), 'applications' => array(0, 34), 'default' => array(9, 9), 'people' => array(3, 0), 'ponder' => array(4, 35), 'calendar' => array(5, 4), 'files' => array(6, 3), 'projects' => array(7, 35), 'daemons' => array(7, 6), 'herald' => array(1, 5), 'countdown' => array(7, 5), 'conduit' => array(7, 30), 'feed' => array(3, 11), 'paste' => array(9, 2), 'audit' => array(8, 19), ); $xadj = -1; foreach ($app_map as $icon => $coords) { list($x, $y) = $coords; foreach ($app_source_map as $suffix => $spec) { list($template, $scale) = $spec; $sheet->addSprite( id(clone $template) ->setSourcePosition(($xadj + glx($x)) * $scale, gly($y) * $scale) ->setTargetCSS('.app-'.$icon.$suffix)); } } $action_template = id(new PhutilSprite()) ->setSourcePosition(0, 0) ->setSourceSize(16, 16); $action_map = array( 'file' => 'icon/page_white_text.png', 'fork' => 'icon/arrow_branch.png', + 'edit' => 'icon/page_white_edit.png', ); foreach ($action_map as $icon => $source) { $sheet->addSprite( id(clone $action_template) ->setSourceFile($srcroot.$source) ->setTargetCSS('.action-'.$icon)); } $sheet->generateImage($webroot.'/image/autosprite.png'); $sheet->generateCSS($webroot.'/css/autosprite.css'); echo "Done.\n"; diff --git a/src/applications/paste/application/PhabricatorApplicationPaste.php b/src/applications/paste/application/PhabricatorApplicationPaste.php index 925db87345..0ca94dd5c4 100644 --- a/src/applications/paste/application/PhabricatorApplicationPaste.php +++ b/src/applications/paste/application/PhabricatorApplicationPaste.php @@ -1,43 +1,44 @@ \d+)' => 'PhabricatorPasteViewController', '/paste/' => array( '' => 'PhabricatorPasteEditController', + 'edit/(?P\d+)/' => 'PhabricatorPasteEditController', 'filter/(?P\w+)/' => 'PhabricatorPasteListController', ), ); } } diff --git a/src/applications/paste/controller/PhabricatorPasteEditController.php b/src/applications/paste/controller/PhabricatorPasteEditController.php index e9d15ad541..27a00b7f9a 100644 --- a/src/applications/paste/controller/PhabricatorPasteEditController.php +++ b/src/applications/paste/controller/PhabricatorPasteEditController.php @@ -1,154 +1,198 @@ id = idx($data, 'id'); + } + + public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); - $paste = new PhabricatorPaste(); - $title = 'Create Paste'; - - $parent_id = $request->getStr('parent'); $parent = null; - if ($parent_id) { - // NOTE: If the Paste is forked from a paste which the user no longer - // has permission to see, we still let them edit it. - $parent = id(new PhabricatorPasteQuery()) - ->setViewer($user) - ->withIDs(array($parent_id)) - ->execute(); - $parent = head($parent); + $parent_id = null; + if (!$this->id) { + $is_create = true; + + $paste = new PhabricatorPaste(); + + $parent_id = $request->getStr('parent'); + if ($parent_id) { + // NOTE: If the Paste is forked from a paste which the user no longer + // has permission to see, we still let them edit it. + $parent = id(new PhabricatorPasteQuery()) + ->setViewer($user) + ->withIDs(array($parent_id)) + ->execute(); + $parent = head($parent); + + if ($parent) { + $paste->setParentPHID($parent->getPHID()); + } + } + + $paste->setAuthorPHID($user->getPHID()); + } else { + $is_create = false; - if ($parent) { - $paste->setParentPHID($parent->getPHID()); + $paste = id(new PhabricatorPasteQuery()) + ->setViewer($user) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->withIDs(array($this->id)) + ->executeOne(); + if (!$paste) { + return new Aphront404Response(); } } $text = null; $e_text = true; $errors = array(); if ($request->isFormPost()) { - $text = $request->getStr('text'); - if (!strlen($text)) { - $e_text = 'Required'; - $errors[] = 'The paste may not be blank.'; - } else { - $e_text = null; + + if ($is_create) { + $text = $request->getStr('text'); + if (!strlen($text)) { + $e_text = 'Required'; + $errors[] = 'The paste may not be blank.'; + } else { + $e_text = null; + } } $paste->setTitle($request->getStr('title')); $paste->setLanguage($request->getStr('language')); if (!$errors) { - $paste_file = PhabricatorFile::newFromFileData( - $text, - array( - 'name' => $title, - 'mime-type' => 'text/plain; charset=utf-8', - 'authorPHID' => $user->getPHID(), - )); - $paste->setFilePHID($paste_file->getPHID()); - $paste->setAuthorPHID($user->getPHID()); + if ($is_create) { + $paste_file = PhabricatorFile::newFromFileData( + $text, + array( + 'name' => $paste->getTitle(), + 'mime-type' => 'text/plain; charset=utf-8', + 'authorPHID' => $user->getPHID(), + )); + $paste->setFilePHID($paste_file->getPHID()); + } $paste->save(); - return id(new AphrontRedirectResponse())->setURI($paste->getURI()); } } else { - if ($parent) { + if ($is_create && $parent) { $paste->setTitle('Fork of '.$parent->getFullName()); $paste->setLanguage($parent->getLanguage()); $parent_file = id(new PhabricatorFile())->loadOneWhere( 'phid = %s', $parent->getFilePHID()); $text = $parent_file->loadFileData(); } } $error_view = null; if ($errors) { $error_view = id(new AphrontErrorView()) ->setTitle('A fatal omission!') ->setErrors($errors); } $form = new AphrontFormView(); $form->setFlexible(true); $langs = array( '' => '(Detect With Wizardly Powers)', ) + PhabricatorEnv::getEnvConfig('pygments.dropdown-choices'); - $submit = id(new AphrontFormSubmitControl()) - ->setValue('Create Paste'); - $form ->setUser($user) ->addHiddenInput('parent', $parent_id) ->appendChild( id(new AphrontFormTextControl()) ->setLabel('Title') ->setValue($paste->getTitle()) ->setName('title')) ->appendChild( id(new AphrontFormSelectControl()) ->setLabel('Language') ->setName('language') ->setValue($paste->getLanguage()) - ->setOptions($langs)) - ->appendChild( - id(new AphrontFormTextAreaControl()) - ->setLabel('Text') - ->setError($e_text) - ->setValue($text) - ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL) - ->setCustomClass('PhabricatorMonospaced') - ->setName('text')) + ->setOptions($langs)); + + if ($is_create) { + $form + ->appendChild( + id(new AphrontFormTextAreaControl()) + ->setLabel('Text') + ->setError($e_text) + ->setValue($text) + ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL) + ->setCustomClass('PhabricatorMonospaced') + ->setName('text')); + } /* TODO: Doesn't have any useful options yet. ->appendChild( id(new AphrontFormPolicyControl()) ->setLabel('Visible To') ->setUser($user) ->setValue( $new_paste->getPolicy(PhabricatorPolicyCapability::CAN_VIEW)) ->setName('policy')) */ + $submit = new AphrontFormSubmitControl(); + + if (!$is_create) { + $submit->addCancelButton($paste->getURI()); + $submit->setValue('Save Paste'); + $title = 'Edit '.$paste->getFullName(); + } else { + $submit->setValue('Create Paste'); + $title = 'Create Paste'; + } + + $form ->appendChild($submit); $nav = $this->buildSideNavView(); $nav->selectFilter('edit'); $nav->appendChild( array( - id(new PhabricatorHeaderView())->setHeader('Create Paste'), + id(new PhabricatorHeaderView())->setHeader($title), $error_view, $form, )); return $this->buildApplicationPage( $nav, array( 'title' => $title, 'device' => true, )); } } diff --git a/src/applications/paste/controller/PhabricatorPasteViewController.php b/src/applications/paste/controller/PhabricatorPasteViewController.php index ce975f83cf..971840f05c 100644 --- a/src/applications/paste/controller/PhabricatorPasteViewController.php +++ b/src/applications/paste/controller/PhabricatorPasteViewController.php @@ -1,161 +1,174 @@ id = $data['id']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $paste = id(new PhabricatorPasteQuery()) ->setViewer($user) ->withIDs(array($this->id)) ->executeOne(); if (!$paste) { return new Aphront404Response(); } $file = id(new PhabricatorFile())->loadOneWhere( 'phid = %s', $paste->getFilePHID()); if (!$file) { return new Aphront400Response(); } $forks = id(new PhabricatorPasteQuery()) ->setViewer($user) ->withParentPHIDs(array($paste->getPHID())) ->execute(); $fork_phids = mpull($forks, 'getPHID'); $this->loadHandles( array_merge( array( $paste->getAuthorPHID(), $paste->getParentPHID(), ), $fork_phids)); $header = $this->buildHeaderView($paste); - $actions = $this->buildActionView($paste, $file); + $actions = $this->buildActionView($user, $paste, $file); $properties = $this->buildPropertyView($paste, $fork_phids); $source_code = $this->buildSourceCodeView($paste, $file); $nav = $this->buildSideNavView($paste); $nav->selectFilter('paste'); $nav->appendChild( array( $header, $actions, $properties, $source_code, )); return $this->buildApplicationPage( $nav, array( 'title' => $paste->getFullName(), 'device' => true, )); } private function buildHeaderView(PhabricatorPaste $paste) { return id(new PhabricatorHeaderView()) ->setObjectName('P'.$paste->getID()) ->setHeader($paste->getTitle()); } private function buildActionView( + PhabricatorUser $user, PhabricatorPaste $paste, PhabricatorFile $file) { + $can_edit = PhabricatorPolicyFilter::hasCapability( + $user, + $paste, + PhabricatorPolicyCapability::CAN_EDIT); + return id(new PhabricatorActionListView()) ->addAction( id(new PhabricatorActionView()) ->setName(pht('Fork This Paste')) ->setIcon('fork') ->setHref($this->getApplicationURI('?parent='.$paste->getID()))) ->addAction( id(new PhabricatorActionView()) ->setName(pht('View Raw File')) ->setIcon('file') - ->setHref($file->getBestURI())); + ->setHref($file->getBestURI())) + ->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Edit Paste')) + ->setIcon('edit') + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit) + ->setHref($this->getApplicationURI('/edit/'.$paste->getID().'/'))); } private function buildPropertyView( PhabricatorPaste $paste, array $child_phids) { $user = $this->getRequest()->getUser(); $properties = new PhabricatorPropertyListView(); $properties->addProperty( pht('Author'), $this->getHandle($paste->getAuthorPHID())->renderLink()); $properties->addProperty( pht('Created'), phabricator_datetime($paste->getDateCreated(), $user)); if ($paste->getParentPHID()) { $properties->addProperty( pht('Forked From'), $this->getHandle($paste->getParentPHID())->renderLink()); } if ($child_phids) { $properties->addProperty( pht('Forks'), $this->renderHandlesForPHIDs($child_phids)); } return $properties; } private function buildSourceCodeView( PhabricatorPaste $paste, PhabricatorFile $file) { $language = $paste->getLanguage(); $source = $file->loadFileData(); if (empty($language)) { $source = PhabricatorSyntaxHighlighter::highlightWithFilename( $paste->getTitle(), $source); } else { $source = PhabricatorSyntaxHighlighter::highlightWithLanguage( $language, $source); } $lines = explode("\n", $source); return id(new PhabricatorSourceCodeView()) ->setLines($lines); } } diff --git a/src/applications/paste/storage/PhabricatorPaste.php b/src/applications/paste/storage/PhabricatorPaste.php index b3b73546e4..9064dac2b8 100644 --- a/src/applications/paste/storage/PhabricatorPaste.php +++ b/src/applications/paste/storage/PhabricatorPaste.php @@ -1,66 +1,70 @@ getID(); } public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorPHIDConstants::PHID_TYPE_PSTE); } public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { - return PhabricatorPolicies::POLICY_USER; + if ($capability == PhabricatorPolicyCapability::CAN_VIEW) { + return PhabricatorPolicies::POLICY_USER; + } + return PhabricatorPolicies::POLICY_NOONE; } public function hasAutomaticCapability($capability, PhabricatorUser $user) { return ($user->getPHID() == $this->getAuthorPHID()); } public function getFullName() { $title = $this->getTitle(); if (!$title) { $title = '(An Untitled Masterwork)'; } return 'P'.$this->getID().' '.$title; } } diff --git a/src/view/layout/PhabricatorActionView.php b/src/view/layout/PhabricatorActionView.php index 778e0d4845..73294baad3 100644 --- a/src/view/layout/PhabricatorActionView.php +++ b/src/view/layout/PhabricatorActionView.php @@ -1,79 +1,98 @@ href = $href; return $this; } public function setIcon($icon) { $this->icon = $icon; return $this; } public function setName($name) { $this->name = $name; return $this; } + public function setDisabled($disabled) { + $this->disabled = $disabled; + return $this; + } + + public function setWorkflow($workflow) { + $this->workflow = $workflow; + return $this; + } + public function render() { $icon = null; if ($this->icon) { $icon = phutil_render_tag( 'span', array( 'class' => 'phabricator-action-view-icon autosprite '. 'action-'.$this->icon, ), ''); } if ($this->href) { - $item = phutil_render_tag( + $item = javelin_render_tag( 'a', array( 'href' => $this->href, 'class' => 'phabricator-action-view-item', + 'sigil' => $this->workflow ? 'workflow' : null, ), phutil_escape_html($this->name)); } else { $item = phutil_render_tag( 'span', array( 'class' => 'phabricator-action-view-item', ), phutil_escape_html($this->name)); } + $classes = array(); + $classes[] = 'phabricator-action-view'; + if ($this->disabled) { + $classes[] = 'phabricator-action-view-disabled'; + } + return phutil_render_tag( 'li', array( - 'class' => 'phabricator-action-view', + 'class' => implode(' ', $classes), ), $icon.$item); } } diff --git a/webroot/rsrc/css/autosprite.css b/webroot/rsrc/css/autosprite.css index fa9492afbb..4d62d03452 100644 --- a/webroot/rsrc/css/autosprite.css +++ b/webroot/rsrc/css/autosprite.css @@ -1,260 +1,264 @@ /** * @provides autosprite-css */ .autosprite { background-image: url(/rsrc/image/autosprite.png); background-repeat: no-repeat; } .main-menu-item-icon-help { background-position: 0px 0px; } .main-menu-item-icon-help-selected { background-position: 0px -27px; } .main-menu-item-icon-help:hover { background-position: 0px -54px; } .main-menu-item-icon-settings { background-position: 0px -81px; } .main-menu-item-icon-settings-selected { background-position: 0px -108px; } .main-menu-item-icon-settings:hover { background-position: 0px -135px; } .main-menu-item-icon-logout { background-position: 0px -162px; } .main-menu-item-icon-logout-selected { background-position: 0px -189px; } .main-menu-item-icon-logout:hover { background-position: 0px -216px; } .main-menu-item-icon-notifications { background-position: 0px -243px; } .main-menu-item-icon-notifications-selected { background-position: 0px -270px; } .main-menu-item-icon-notifications:hover { background-position: 0px -297px; } .main-menu-item-icon-task { background-position: 0px -324px; } .main-menu-item-icon-task-selected { background-position: 0px -351px; } .main-menu-item-icon-task:hover { background-position: 0px -378px; } .app-differential-full { background-position: 0px -405px; } .app-differential { background-position: 0px -466px; } .app-fact-full { background-position: 0px -497px; } .app-fact { background-position: 0px -558px; } .app-mail-full { background-position: 0px -589px; } .app-mail { background-position: 0px -650px; } .app-diffusion-full { background-position: 0px -681px; } .app-diffusion { background-position: 0px -742px; } .app-slowvote-full { background-position: 0px -773px; } .app-slowvote { background-position: 0px -834px; } .app-phriction-full { background-position: 0px -865px; } .app-phriction { background-position: 0px -926px; } .app-maniphest-full { background-position: 0px -957px; } .app-maniphest { background-position: 0px -1018px; } .app-flags-full { background-position: 0px -1049px; } .app-flags { background-position: 0px -1110px; } .app-settings-full { background-position: 0px -1141px; } .app-settings { background-position: 0px -1202px; } .app-applications-full { background-position: 0px -1233px; } .app-applications { background-position: 0px -1294px; } .app-default-full { background-position: 0px -1325px; } .app-default { background-position: 0px -1386px; } .app-people-full { background-position: 0px -1417px; } .app-people { background-position: 0px -1478px; } .app-ponder-full { background-position: 0px -1509px; } .app-ponder { background-position: 0px -1570px; } .app-calendar-full { background-position: 0px -1601px; } .app-calendar { background-position: 0px -1662px; } .app-files-full { background-position: 0px -1693px; } .app-files { background-position: 0px -1754px; } .app-projects-full { background-position: 0px -1785px; } .app-projects { background-position: 0px -1846px; } .app-daemons-full { background-position: 0px -1877px; } .app-daemons { background-position: 0px -1938px; } .app-herald-full { background-position: 0px -1969px; } .app-herald { background-position: 0px -2030px; } .app-countdown-full { background-position: 0px -2061px; } .app-countdown { background-position: 0px -2122px; } .app-conduit-full { background-position: 0px -2153px; } .app-conduit { background-position: 0px -2214px; } .app-feed-full { background-position: 0px -2245px; } .app-feed { background-position: 0px -2306px; } .app-paste-full { background-position: 0px -2337px; } .app-paste { background-position: 0px -2398px; } .app-audit-full { background-position: 0px -2429px; } .app-audit { background-position: 0px -2490px; } .action-file { background-position: 0px -2521px; } .action-fork { background-position: 0px -2538px; } + +.action-edit { + background-position: 0px -2555px; +} diff --git a/webroot/rsrc/css/layout/phabricator-action-list-view.css b/webroot/rsrc/css/layout/phabricator-action-list-view.css index 37b076dbb8..65fb124fd9 100644 --- a/webroot/rsrc/css/layout/phabricator-action-list-view.css +++ b/webroot/rsrc/css/layout/phabricator-action-list-view.css @@ -1,54 +1,63 @@ /** * @provides phabricator-action-list-view-css */ .phabricator-action-list-view { background: #ffffff; } .device-desktop .phabricator-action-list-view { border: 1px solid #dcdcdc; padding: .5em 0; position: absolute; margin-top: -30px; right: 1%; width: 20%; border-radius: 2px; font-size: 12px; box-shadow: 0px 1px 4px rgba(0, 0, 0, 0.10); } .device-tablet .phabricator-action-list-view, .device-phone .phabricator-action-list-view { background: #f3f3f3; border-top: 1px solid #dcdcdc; padding: .5em 0; } .phabricator-action-view { padding: 2px 0; position: relative; } .phabricator-action-view-item { line-height: 20px; padding-left: 34px; display: block; font-size: 12px; } .phabricator-action-view-icon { width: 16px; height: 16px; position: absolute; top: 4px; left: 12px; } .device-desktop .phabricator-action-view-item:hover { background-color: #3875d7; color: #ffffff; text-decoration: none; } + +.phabricator-action-view-disabled .phabricator-action-view-item { + color: #888888; +} + +.phabricator-action-view-disabled .phabricator-action-view-item:hover { + background-color: #dfdfdf; + color: #888888; +} diff --git a/webroot/rsrc/image/autosprite.png b/webroot/rsrc/image/autosprite.png index 6730b0097a..ec0e480ec1 100644 Binary files a/webroot/rsrc/image/autosprite.png and b/webroot/rsrc/image/autosprite.png differ