diff --git a/src/applications/auth/controller/PhabricatorLoginController.php b/src/applications/auth/controller/PhabricatorLoginController.php index f4bb0ab57a..19b589934d 100644 --- a/src/applications/auth/controller/PhabricatorLoginController.php +++ b/src/applications/auth/controller/PhabricatorLoginController.php @@ -1,311 +1,314 @@ getRequest(); $user = $request->getUser(); if ($user->isLoggedIn()) { // Kick the user out if they're already logged in. return id(new AphrontRedirectResponse())->setURI('/'); } if ($request->isAjax()) { // We end up here if the user clicks a workflow link that they need to // login to use. We give them a dialog saying "You need to login..". if ($request->isDialogFormPost()) { return id(new AphrontRedirectResponse())->setURI( $request->getRequestURI()); } $dialog = new AphrontDialogView(); $dialog->setUser($user); $dialog->setTitle(pht('Login Required')); $dialog->appendChild(phutil_tag('p', array(), pht( 'You must login to continue.'))); $dialog->addSubmitButton(pht('Login')); $dialog->addCancelButton('/', pht('Cancel')); return id(new AphrontDialogResponse())->setDialog($dialog); } if ($request->isConduit()) { // A common source of errors in Conduit client configuration is getting // the request path wrong. The client will end up here, so make some // effort to give them a comprehensible error message. $request_path = $this->getRequest()->getPath(); $conduit_path = '/api/'; $example_path = '/api/conduit.ping'; $message = "ERROR: You are making a Conduit API request to '{$request_path}', ". "but the correct HTTP request path to use in order to access a ". "Conduit method is '{$conduit_path}' (for example, ". "'{$example_path}'). Check your configuration."; return id(new AphrontPlainTextResponse())->setContent($message); } $error_view = null; if ($request->getCookie('phusr') && $request->getCookie('phsid')) { // The session cookie is invalid, so clear it. $request->clearCookie('phusr'); $request->clearCookie('phsid'); $error_view = new AphrontErrorView(); $error_view->setTitle(pht('Invalid Session')); $error_view->setErrors(array( pht("Your login session is invalid. Try logging in again. If that ". "doesn't work, clear your browser cookies.") )); } - $next_uri_path = $this->getRequest()->getPath(); - if ($next_uri_path == '/login/') { - $next_uri = '/'; - } else { - $next_uri = $this->getRequest()->getRequestURI(); + + $next_uri = $request->getStr('next'); + if (!$next_uri) { + $next_uri_path = $this->getRequest()->getPath(); + if ($next_uri_path == '/login/') { + $next_uri = '/'; + } else { + $next_uri = $this->getRequest()->getRequestURI(); + } } if (!$request->isFormPost()) { $request->setCookie('next_uri', $next_uri); } $password_auth = PhabricatorEnv::getEnvConfig('auth.password-auth-enabled'); $username_or_email = $request->getCookie('phusr'); $forms = array(); $errors = array(); if ($password_auth) { $require_captcha = false; $e_captcha = true; if ($request->isFormPost()) { if (AphrontFormRecaptchaControl::isRecaptchaEnabled()) { $failed_attempts = PhabricatorUserLog::loadRecentEventsFromThisIP( PhabricatorUserLog::ACTION_LOGIN_FAILURE, 60 * 15); if (count($failed_attempts) > 5) { $require_captcha = true; if (!AphrontFormRecaptchaControl::processCaptcha($request)) { if (AphrontFormRecaptchaControl::hasCaptchaResponse($request)) { $e_captcha = pht('Invalid'); $errors[] = pht('CAPTCHA was not entered correctly.'); } else { $e_captcha = pht('Required'); $errors[] = pht('Too many login failures recently. You must '. 'submit a CAPTCHA with your login request.'); } } } } $username_or_email = $request->getStr('username_or_email'); $user = id(new PhabricatorUser())->loadOneWhere( 'username = %s', $username_or_email); if (!$user) { $user = PhabricatorUser::loadOneWithEmailAddress($username_or_email); } if (!$errors) { // Perform username/password tests only if we didn't get rate limited // by the CAPTCHA. $envelope = new PhutilOpaqueEnvelope($request->getStr('password')); if (!$user || !$user->comparePassword($envelope)) { $errors[] = pht('Bad username/password.'); } } if (!$errors) { $session_key = $user->establishSession('web'); $request->setCookie('phusr', $user->getUsername()); $request->setCookie('phsid', $session_key); - $uri = new PhutilURI('/login/validate/'); - $uri->setQueryParams( - array( - 'phusr' => $user->getUsername(), + $uri = id(new PhutilURI('/login/validate/')) + ->setQueryParams( + array('phusr' => $user->getUsername() )); return id(new AphrontRedirectResponse()) ->setURI((string)$uri); } else { $log = PhabricatorUserLog::newLog( null, $user, PhabricatorUserLog::ACTION_LOGIN_FAILURE); $log->save(); $request->clearCookie('phusr'); $request->clearCookie('phsid'); } } if ($errors) { $error_view = new AphrontErrorView(); $error_view->setTitle(pht('Login Failed')); $error_view->setErrors($errors); } $form = new AphrontFormView(); $form ->setUser($request->getUser()) ->setAction('/login/') ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Username/Email')) ->setName('username_or_email') ->setValue($username_or_email)) ->appendChild( id(new AphrontFormPasswordControl()) ->setLabel(pht('Password')) ->setName('password') ->setCaption(hsprintf( '%s', pht('Forgot your password? / Email Login')))); if ($require_captcha) { $form->appendChild( id(new AphrontFormRecaptchaControl()) ->setError($e_captcha)); } $form ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Login'))); // $panel->setCreateButton('Register New Account', '/login/register/'); $forms['Phabricator Login'] = $form; } $ldap_provider = new PhabricatorLDAPProvider(); if ($ldap_provider->isProviderEnabled()) { $ldap_form = new AphrontFormView(); $ldap_form ->setUser($request->getUser()) ->setAction('/ldap/login/') ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('LDAP username')) ->setName('username') ->setValue($username_or_email)) ->appendChild( id(new AphrontFormPasswordControl()) ->setLabel(pht('Password')) ->setName('password')); $ldap_form ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Login'))); $forms['LDAP Login'] = $ldap_form; } $providers = PhabricatorOAuthProvider::getAllProviders(); foreach ($providers as $provider) { $enabled = $provider->isProviderEnabled(); if (!$enabled) { continue; } $auth_uri = $provider->getAuthURI(); $redirect_uri = $provider->getRedirectURI(); $client_id = $provider->getClientID(); $provider_name = $provider->getProviderName(); $minimum_scope = $provider->getMinimumScope(); $extra_auth = $provider->getExtraAuthParameters(); // TODO: In theory we should use 'state' to prevent CSRF, but the total // effect of the CSRF attack is that an attacker can cause a user to login // to Phabricator if they're already logged into some OAuth provider. This // does not seem like the most severe threat in the world, and generating // CSRF for logged-out users is vaugely tricky. if ($provider->isProviderRegistrationEnabled()) { $title = pht("Login or Register with %s", $provider_name); $body = pht('Login or register for Phabricator using your %s account.', $provider_name); $button = pht("Login or Register with %s", $provider_name); } else { $title = pht("Login with %s", $provider_name); $body = hsprintf( '%s

%s', pht( 'Login to your existing Phabricator account using your %s account.', $provider_name), pht( 'You can not use %s to register a new account.', $provider_name)); $button = pht("Log in with %s", $provider_name); } $auth_form = new AphrontFormView(); $auth_form ->setAction($auth_uri) ->addHiddenInput('client_id', $client_id) ->addHiddenInput('redirect_uri', $redirect_uri) ->addHiddenInput('scope', $minimum_scope); foreach ($extra_auth as $key => $value) { $auth_form->addHiddenInput($key, $value); } $auth_form ->setUser($request->getUser()) ->setMethod('GET') ->appendChild(hsprintf( '

%s

', $body)) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue("{$button} \xC2\xBB")); $forms[$title] = $auth_form; } $panel = new AphrontPanelView(); $panel->setWidth(AphrontPanelView::WIDTH_FORM); $panel->setNoBackground(); foreach ($forms as $name => $form) { $panel->appendChild(phutil_tag('h1', array(), $name)); $panel->appendChild($form); $panel->appendChild(phutil_tag('br')); } $login_message = PhabricatorEnv::getEnvConfig('auth.login-message'); return $this->buildApplicationPage( array( $error_view, phutil_safe_html($login_message), $panel, ), array( 'title' => pht('Login'), 'device' => true )); } } diff --git a/src/applications/pholio/controller/PholioMockViewController.php b/src/applications/pholio/controller/PholioMockViewController.php index f5f312f3e6..b9e6c569a1 100644 --- a/src/applications/pholio/controller/PholioMockViewController.php +++ b/src/applications/pholio/controller/PholioMockViewController.php @@ -1,212 +1,219 @@ id = $data['id']; $this->imageID = idx($data, 'imageID'); } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $mock = id(new PholioMockQuery()) ->setViewer($user) ->withIDs(array($this->id)) ->needImages(true) ->needCoverFiles(true) ->executeOne(); if (!$mock) { return new Aphront404Response(); } $xactions = id(new PholioTransactionQuery()) ->setViewer($user) ->withObjectPHIDs(array($mock->getPHID())) ->execute(); $subscribers = PhabricatorSubscribersQuery::loadSubscribersForPHID( $mock->getPHID()); $phids = array(); $phids[] = $mock->getAuthorPHID(); foreach ($subscribers as $subscriber) { $phids[] = $subscriber; } $this->loadHandles($phids); $engine = id(new PhabricatorMarkupEngine()) ->setViewer($user); $engine->addObject($mock, PholioMock::MARKUP_FIELD_DESCRIPTION); foreach ($xactions as $xaction) { if ($xaction->getComment()) { $engine->addObject( $xaction->getComment(), PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT); } } $engine->process(); $title = $mock->getName(); $header = id(new PhabricatorHeaderView()) ->setHeader($title); $actions = $this->buildActionView($mock); $properties = $this->buildPropertyView($mock, $engine, $subscribers); require_celerity_resource('pholio-css'); require_celerity_resource('pholio-inline-comments-css'); - $output = new PholioMockImagesView(); - $output->setMock($mock); - $output->setImageID($this->imageID); + $output = id(new PholioMockImagesView()) + ->setRequestURI($request->getRequestURI()) + ->setUser($user) + ->setMock($mock) + ->setImageID($this->imageID); $xaction_view = id(new PhabricatorApplicationTransactionView()) ->setUser($this->getRequest()->getUser()) ->setTransactions($xactions) ->setMarkupEngine($engine); $add_comment = $this->buildAddCommentView($mock); $crumbs = $this->buildApplicationCrumbs($this->buildSideNav()); $crumbs->addCrumb( id(new PhabricatorCrumbView()) ->setName('M'.$mock->getID()) ->setHref('/M'.$mock->getID())); $content = array( $crumbs, $header, $actions, $properties, $output->render(), $xaction_view, $add_comment, ); PhabricatorFeedStoryNotification::updateObjectNotificationViews( $user, $mock->getPHID()); return $this->buildApplicationPage( $content, array( 'title' => 'M'.$mock->getID().' '.$title, 'device' => true, 'pageObjects' => array($mock->getPHID()), )); } private function buildActionView(PholioMock $mock) { $user = $this->getRequest()->getUser(); $actions = id(new PhabricatorActionListView()) ->setUser($user) ->setObject($mock); $can_edit = PhabricatorPolicyFilter::hasCapability( $user, $mock, PhabricatorPolicyCapability::CAN_EDIT); $actions->addAction( id(new PhabricatorActionView()) ->setIcon('edit') ->setName(pht('Edit Mock')) ->setHref($this->getApplicationURI('/edit/'.$mock->getID())) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); return $actions; } private function buildPropertyView( PholioMock $mock, PhabricatorMarkupEngine $engine, array $subscribers) { $user = $this->getRequest()->getUser(); $properties = id(new PhabricatorPropertyListView()) ->setUser($user) ->setObject($mock); $properties->addProperty( pht('Author'), $this->getHandle($mock->getAuthorPHID())->renderLink()); $properties->addProperty( pht('Created'), phabricator_datetime($mock->getDateCreated(), $user)); $descriptions = PhabricatorPolicyQuery::renderPolicyDescriptions( $user, $mock); $properties->addProperty( pht('Visible To'), $descriptions[PhabricatorPolicyCapability::CAN_VIEW]); if ($subscribers) { $sub_view = array(); foreach ($subscribers as $subscriber) { $sub_view[] = $this->getHandle($subscriber)->renderLink(); } $sub_view = phutil_implode_html(', ', $sub_view); } else { $sub_view = phutil_tag('em', array(), pht('None')); } $properties->addProperty( pht('Subscribers'), $sub_view); $properties->invokeWillRenderEvent(); $properties->addImageContent( $engine->getOutput($mock, PholioMock::MARKUP_FIELD_DESCRIPTION)); return $properties; } private function buildAddCommentView(PholioMock $mock) { $user = $this->getRequest()->getUser(); $draft = PhabricatorDraft::newFromUserAndKey($user, $mock->getPHID()); $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); $title = $is_serious ? pht('Add Comment') : pht('History Beckons'); $header = id(new PhabricatorHeaderView()) ->setHeader($title); $button_name = $is_serious ? pht('Add Comment') : pht('Answer The Call'); $form = id(new PhabricatorApplicationTransactionCommentView()) ->setUser($user) ->setDraft($draft) ->setSubmitButtonName($button_name) - ->setAction($this->getApplicationURI('/comment/'.$mock->getID().'/')); + ->setAction($this->getApplicationURI('/comment/'.$mock->getID().'/')) + ->setRequestURI($this->getRequest()->getRequestURI()); return array( $header, $form, ); } } diff --git a/src/applications/pholio/view/PholioMockImagesView.php b/src/applications/pholio/view/PholioMockImagesView.php index 72116f14ea..a8f3a6c209 100644 --- a/src/applications/pholio/view/PholioMockImagesView.php +++ b/src/applications/pholio/view/PholioMockImagesView.php @@ -1,150 +1,163 @@ requestURI = $request_uri; + return $this; + } + public function getRequestURI() { + return $this->requestURI; + } public function setImageID($image_id) { $this->imageID = $image_id; return $this; } public function getImageID() { return $this->imageID; } public function setMock(PholioMock $mock) { $this->mock = $mock; return $this; } public function render() { if (!$this->mock) { throw new Exception("Call setMock() before render()!"); } $mock = $this->mock; require_celerity_resource('javelin-behavior-pholio-mock-view'); $images = array(); $panel_id = celerity_generate_unique_node_id(); $viewport_id = celerity_generate_unique_node_id(); $ids = mpull($mock->getImages(), 'getID'); if ($this->imageID && isset($ids[$this->imageID])) { $selected_id = $this->imageID; } else { $selected_id = head_key($ids); } foreach ($mock->getImages() as $image) { $file = $image->getFile(); $metadata = $file->getMetadata(); $x = idx($metadata, PhabricatorFile::METADATA_IMAGE_WIDTH); $y = idx($metadata, PhabricatorFile::METADATA_IMAGE_HEIGHT); $images[] = array( 'id' => $image->getID(), 'fullURI' => $image->getFile()->getBestURI(), 'pageURI' => '/M'.$mock->getID().'/'.$image->getID().'/', 'width' => $x, 'height' => $y, 'title' => $file->getName(), 'desc' => 'Lorem ipsum dolor sit amet: there is no way to set any '. 'descriptive text yet; were there, it would appear here.', ); } + $login_uri = id(new PhutilURI('/login/')) + ->setQueryParam('next', (string) $this->getRequestURI()); $config = array( 'mockID' => $mock->getID(), 'panelID' => $panel_id, 'viewportID' => $viewport_id, 'images' => $images, 'selectedID' => $selected_id, + 'loggedIn' => $this->getUser()->isLoggedIn(), + 'logInLink' => (string) $login_uri ); Javelin::initBehavior('pholio-mock-view', $config); $mockview = ''; $mock_wrapper = javelin_tag( 'div', array( 'id' => $viewport_id, 'sigil' => 'mock-viewport', 'class' => 'pholio-mock-image-viewport' ), ''); $mock_wrapper = javelin_tag( 'div', array( 'id' => $panel_id, 'sigil' => 'mock-panel', 'class' => 'pholio-mock-image-panel', ), $mock_wrapper); $inline_comments_holder = javelin_tag( 'div', array( 'id' => 'mock-inline-comments', 'sigil' => 'mock-inline-comments', 'class' => 'pholio-mock-inline-comments' ), ''); $carousel_holder = ''; if (count($mock->getImages()) > 0) { $thumbnails = array(); foreach ($mock->getImages() as $image) { $thumbfile = $image->getFile(); $dimensions = PhabricatorImageTransformer::getPreviewDimensions( $thumbfile, 140); $tag = phutil_tag( 'img', array( 'width' => $dimensions['sdx'], 'height' => $dimensions['sdy'], 'src' => $thumbfile->getPreview140URI(), 'class' => 'pholio-mock-carousel-thumbnail', 'style' => 'top: '.floor((140 - $dimensions['sdy'] ) / 2).'px', )); $thumbnails[] = javelin_tag( 'a', array( 'sigil' => 'mock-thumbnail', 'class' => 'pholio-mock-carousel-thumb-item', 'href' => '/M'.$mock->getID().'/'.$image->getID().'/', 'meta' => array( 'imageID' => $image->getID(), ), ), $tag); } $carousel_holder = phutil_tag( 'div', array( 'id' => 'pholio-mock-carousel', 'class' => 'pholio-mock-carousel', ), $thumbnails); } $mockview[] = phutil_tag( 'div', array( 'class' => 'pholio-mock-image-container', 'id' => 'pholio-mock-image-container' ), array($mock_wrapper, $carousel_holder, $inline_comments_holder)); return $this->renderSingleView($mockview); } } diff --git a/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php b/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php index a943ec6179..ec26997456 100644 --- a/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php +++ b/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php @@ -1,174 +1,203 @@ requestURI = $request_uri; + return $this; + } + public function getRequestURI() { + return $this->requestURI; + } public function setDraft(PhabricatorDraft $draft) { $this->draft = $draft; return $this; } public function getDraft() { return $this->draft; } public function setSubmitButtonName($submit_button_name) { $this->submitButtonName = $submit_button_name; return $this; } public function getSubmitButtonName() { return $this->submitButtonName; } public function setAction($action) { $this->action = $action; return $this; } public function getAction() { return $this->action; } public function render() { + $user = $this->getUser(); + if (!$user->isLoggedIn()) { + $uri = id(new PhutilURI('/login/')) + ->setQueryParam('next', (string) $this->getRequestURI()); + return self::renderSingleView( + phutil_tag( + 'div', + array( + 'class' => 'login-to-comment' + ), + javelin_tag( + 'a', + array( + 'class' => 'button', + 'sigil' => 'workflow', + 'href' => $uri + ), + pht('Login to Comment')))); + } + $data = array(); $comment = $this->renderCommentPanel(); $preview = $this->renderPreviewPanel(); Javelin::initBehavior( 'phabricator-transaction-comment-form', array( 'formID' => $this->getFormID(), 'timelineID' => $this->getPreviewTimelineID(), 'panelID' => $this->getPreviewPanelID(), 'statusID' => $this->getStatusID(), 'commentID' => $this->getCommentID(), 'loadingString' => pht('Loading Preview...'), 'savingString' => pht('Saving Draft...'), 'draftString' => pht('Saved Draft'), 'actionURI' => $this->getAction(), 'draftKey' => $this->getDraft()->getDraftKey(), )); return self::renderSingleView( array( $comment, $preview, )); } private function renderCommentPanel() { $status = phutil_tag( 'div', array( 'id' => $this->getStatusID(), ), ''); $draft_comment = ''; if ($this->getDraft()) { $draft_comment = $this->getDraft()->getDraft(); } return id(new AphrontFormView()) ->setUser($this->getUser()) ->setFlexible(true) ->addSigil('transaction-append') ->setWorkflow(true) ->setAction($this->getAction()) ->setID($this->getFormID()) ->appendChild( id(new PhabricatorRemarkupControl()) ->setID($this->getCommentID()) ->setName('comment') ->setLabel(pht('Comment')) ->setUser($this->getUser()) ->setValue($draft_comment)) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue($this->getSubmitButtonName())) ->appendChild( id(new AphrontFormMarkupControl()) ->setValue($status)); } private function renderPreviewPanel() { $preview = id(new PhabricatorTimelineView()) ->setID($this->getPreviewTimelineID()); $header = phutil_tag( 'div', array( 'class' => 'phabricator-timeline-preview-header', ), pht('Preview')); return phutil_tag( 'div', array( 'id' => $this->getPreviewPanelID(), 'style' => 'display: none', ), self::renderSingleView( array( $header, $preview, ))); } private function getPreviewPanelID() { if (!$this->previewPanelID) { $this->previewPanelID = celerity_generate_unique_node_id(); } return $this->previewPanelID; } private function getPreviewTimelineID() { if (!$this->previewTimelineID) { $this->previewTimelineID = celerity_generate_unique_node_id(); } return $this->previewTimelineID; } private function getFormID() { if (!$this->formID) { $this->formID = celerity_generate_unique_node_id(); } return $this->formID; } private function getStatusID() { if (!$this->statusID) { $this->statusID = celerity_generate_unique_node_id(); } return $this->statusID; } private function getCommentID() { if (!$this->commentID) { $this->commentID = celerity_generate_unique_node_id(); } return $this->commentID; } } diff --git a/webroot/rsrc/css/aphront/form-view.css b/webroot/rsrc/css/aphront/form-view.css index abca6a6b2d..40eb7b0226 100644 --- a/webroot/rsrc/css/aphront/form-view.css +++ b/webroot/rsrc/css/aphront/form-view.css @@ -1,365 +1,370 @@ /** * @provides aphront-form-view-css */ /** * These styles are overrides for .aphront-form-view */ .aphront-form-view-shaded { border: 1px solid #d4dae0; background: #f4f5f8; } .aphront-form-view-padded { padding: 1em; } .aphront-form-view label.aphront-form-label { padding-top: 4px; width: 19%; float: left; text-align: right; font-weight: bold; font-size: 13px; color: #666666; } .device-phone .aphront-form-view label.aphront-form-label { display: block; float: none; text-align: left; width: 100%; } .aphront-form-input { margin-left: 20%; margin-right: 25%; width: 55%; } .device-phone .aphront-form-input { margin-left: 0%; margin-right: 0%; width: 100%; } .aphront-form-control-text .aphront-form-input input, .aphront-form-control-password .aphront-form-input input { font-size: 13px; padding: 4px 4px; color: #333; vertical-align: middle; background-color: #ffffff; border: 1px solid #96A6C5; border-radius: 3px; box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); } .aphront-form-error { width: 23%; float: right; color: #aa0000; font-weight: bold; padding-top: 4px; } .device-phone .aphront-form-error { float: none; width: 100%; } .device-phone .aphront-form-drag-and-drop-upload { display: none; } .aphront-form-required { font-weight: normal; color: #888888; font-size: 11px; } .aphront-form-input input, .aphront-form-input textarea { font-size: 13px; display: block; width: 100%; box-sizing: border-box; } .aphront-form-input textarea { height: 12em; } .aphront-form-control { padding: 4px; } .aphront-form-control-submit button, .aphront-form-control-submit a.button { float: right; margin: 0.5em 0 0em 2%; } .aphront-form-control-textarea textarea.aphront-textarea-very-short { height: 3em; border: 1px solid #96A6C5; border-radius: 3px; box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); } .aphront-form-control-textarea textarea.aphront-textarea-very-tall { height: 24em; } .aphront-form-control-select .aphront-form-input { padding-top: 2px; } .aphront-form-view .aphront-form-caption { font-size: 12px; color: #888; padding: 2px; text-align: right; margin-right: 25%; margin-left: 20%; } .device-phone .aphront-form-view .aphront-form-caption { margin-right: 0%; } /* override for when inside an aphront-panel-view */ .aphront-panel-view .aphront-form-view h1 { padding: 0em 0em .8em 0em; } .aphront-form-instructions { margin: 0.75em 3% 1.25em; } .aphront-form-important { margin: .5em 0; background: #ffffdd; padding: .5em 1em; } .aphront-form-important code { display: block; padding: .25em; margin: .5em 2em; } .aphront-form-control-static .aphront-form-input, .aphront-form-control-markup .aphront-form-input { padding-top: 4px; font-size: 13px; } .aphront-form-control-togglebuttons .aphront-form-input { padding-top: 5px; } table.aphront-form-control-radio-layout, table.aphront-form-control-checkbox-layout { margin-top: 3px; font-size: 13px; } table.aphront-form-control-radio-layout th, table.aphront-form-control-checkbox-layout th { padding-top: 2px; padding-left: 0.35em; padding-bottom: 4px; } .aphront-form-control-radio-layout td input, .aphront-form-control-checkbox-layout td input { margin-top: 4px; width: auto; } .aphront-form-radio-caption { font-size: 11px; color: #444444; max-width: 400px; } .aphront-form-control-image span { margin: 0px 4px 0px 2px; } .aphront-form-control-image .default-image { display: inline; width: 12px; } .aphront-form-input hr { border: none; background: #bbbbbb; height: 1px; position: relative; } .aphront-form-inset { margin: 0 0 1em; padding: .75em; background: #f3f3f3; border: 1px solid #afafaf; } .aphront-form-drag-and-drop-file-list { width: 400px; } .drag-and-drop-instructions { color: #333333; font-size: 11px; padding: 6px 8px; } .drag-and-drop-file-target { border: 1px dashed #bfbfbf; padding-top: 10px; padding-bottom: 10px; } .aphront-textarea-drag-and-drop { background: #99ff99; border-color: #669966; } .aphront-form-crop .crop-box { cursor: move; overflow: hidden; } .aphront-form-crop .crop-box .crop-image { position: relative; top: 0px; left: 0px; } .calendar-button { display: inline; background: url(/rsrc/image/icon/fatcow/calendar_edit.png) no-repeat center center; padding: 8px 12px; margin: 2px 8px 2px 2px; position: relative; border: 1px solid transparent; } .aphront-form-date-container { position: relative; display: inline; } .aphront-form-date-container select { margin: 2px; display: inline; } .aphront-form-date-container input.aphront-form-date-time-input { width: 7em; display: inline; } .fancy-datepicker { position: absolute; width: 240px; } .fancy-datepicker-core { padding: 1px; font-size: 11px; font-family: Verdana; text-align: center; } .fancy-datepicker-core .month-table, .fancy-datepicker-core .day-table { margin: 0 auto; border-collapse: separate; border-spacing: 1px; width: 100%; } .fancy-datepicker-core .month-table { margin-bottom: 6px; } .fancy-datepicker-core .month-table td.lrbutton { width: 20%; } .fancy-datepicker-core .month-table td { padding: 4px; font-weight: bold; color: #444444; } .fancy-datepicker-core .month-table td.lrbutton { background: #e6e6e6; border: 1px solid; border-color: #a6a6a6 #969696 #868686 #a6a6a6; } .fancy-datepicker-core .day-table td { overflow: hidden; background: #f6f6f6; vertical-align: center; text-align: center; border: 1px solid #d6d6d6; padding: 4px 0; } .fancy-datepicker-core .day-table td.day-placeholder { border-color: transparent; background: transparent; } .fancy-datepicker-core .day-table td.weekend { color: #666666; border-color: #e6e6e6; } .fancy-datepicker-core .day-table td.day-name { background: transparent; border: 1px transparent; vertical-align: bottom; color: #888888; } .fancy-datepicker-core .day-table td.today { background: #eeee99; border-color: #aaaa66; } .fancy-datepicker-core .day-table td.datepicker-selected { background: #0099ff; border-color: #0066cc; } .fancy-datepicker-core td { cursor: pointer; } .fancy-datepicker-core td.novalue { cursor: inherit; } .picker-open .calendar-button, .fancy-datepicker-core { background-color: white; border: 1px solid #777777; box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.25); } .picker-open .calendar-button { border-left: 1px solid white; } + +.login-to-comment { + padding: 12px; + float: right; +} diff --git a/webroot/rsrc/js/application/pholio/behavior-pholio-mock-view.js b/webroot/rsrc/js/application/pholio/behavior-pholio-mock-view.js index 095722e014..4d5105fe93 100644 --- a/webroot/rsrc/js/application/pholio/behavior-pholio-mock-view.js +++ b/webroot/rsrc/js/application/pholio/behavior-pholio-mock-view.js @@ -1,714 +1,719 @@ /** * @provides javelin-behavior-pholio-mock-view * @requires javelin-behavior * javelin-util * javelin-stratcom * javelin-dom * javelin-vector * javelin-magical-init * javelin-request * javelin-history * javelin-workflow * javelin-mask * javelin-behavior-device * phabricator-keyboard-shortcut */ JX.behavior('pholio-mock-view', function(config) { var is_dragging = false; var drag_begin; var drag_end; var panel = JX.$(config.panelID); var viewport = JX.$(config.viewportID); var selection_border; var selection_fill; var active_image; var inline_comments = {}; /* -( Stage )-------------------------------------------------------------- */ var stage = (function() { var loading = false; var stageElement = JX.$(config.panelID); var viewElement = JX.$(config.viewportID); var gutterElement = JX.$('mock-inline-comments'); var reticles = []; var cards = []; var inline_phid_map = {}; function begin_load() { if (loading) { return; } loading = true; clear_stage(); draw_loading(); } function end_load() { if (!loading) { return; } loading = false; draw_loading(); } function draw_loading() { JX.DOM.alterClass(stageElement, 'pholio-image-loading', loading); } function add_inline_node(node, phid) { inline_phid_map[phid] = (inline_phid_map[phid] || []); inline_phid_map[phid].push(node); } function add_reticle(reticle, phid) { mark_ref(reticle, phid); reticles.push(reticle); add_inline_node(reticle, phid); viewElement.appendChild(reticle); } function clear_stage() { for (var ii = 0; ii < reticles.length; ii++) { JX.DOM.remove(reticles[ii]); } for (var ii = 0; ii < cards.length; ii++) { JX.DOM.remove(cards[ii]); } reticles = []; cards = []; inline_phid_map = {}; } function highlight_inline(phid, show) { var nodes = inline_phid_map[phid] || []; var cls = 'pholio-mock-inline-comment-highlight'; for (var ii = 0; ii < nodes.length; ii++) { JX.DOM.alterClass(nodes[ii], cls, show); } } function remove_inline(phid) { var nodes = inline_phid_map[phid] || []; for (var ii = 0; ii < nodes.length; ii++) { JX.DOM.remove(nodes[ii]); } delete inline_phid_map[phid]; } function mark_ref(node, phid) { JX.Stratcom.addSigil(node, 'pholio-inline-ref'); JX.Stratcom.addData(node, {phid: phid}); } function add_card(card, phid) { mark_ref(card, phid); cards.push(card); add_inline_node(card, phid); gutterElement.appendChild(card); } return { beginLoad: begin_load, endLoad: end_load, addReticle: add_reticle, clearStage: clear_stage, highlightInline: highlight_inline, removeInline: remove_inline, addCard: add_card }; })(); function get_image_index(id) { for (var ii = 0; ii < config.images.length; ii++) { if (config.images[ii].id == id) { return ii; } } return null; } function get_image(id) { var idx = get_image_index(id); if (idx === null) { return idx; } return config.images[idx]; } function onload_image(id) { if (active_image.id != id) { // The user has clicked another image before this one loaded, so just // bail. return; } active_image.tag = this; redraw_image(); } function switch_image(delta) { if (!active_image) { return; } var idx = get_image_index(active_image.id) idx = (idx + delta + config.images.length) % config.images.length; select_image(config.images[idx].id); } function redraw_image() { // Force the stage to scale as a function of the viewport size. Broadly, // we make the stage 95% of the height of the viewport, then scale images // to fit within it. var new_y = (JX.Vector.getViewport().y * 0.90) - 150; new_y = Math.max(320, new_y); panel.style.height = new_y + 'px'; if (!active_image || !active_image.tag) { return; } var tag = active_image.tag; // If the image is too wide or tall for the viewport, scale it down so it // fits. var w = JX.Vector.getDim(panel); w.x -= 40; w.y -= 40; var scale = 1; if (w.x < tag.naturalWidth) { scale = Math.min(scale, w.x / tag.naturalWidth); } if (w.y < tag.naturalHeight) { scale = Math.min(scale, w.y / tag.naturalHeight); } if (scale < 1) { tag.width = Math.floor(scale * tag.naturalWidth); tag.height = Math.floor(scale * tag.naturalHeight); } else { tag.width = tag.naturalWidth; tag.height = tag.naturalHeight; } viewport.style.top = Math.floor((new_y - tag.height) / 2) + 'px'; stage.endLoad(); JX.DOM.setContent(viewport, tag); redraw_inlines(active_image.id); } function select_image(image_id) { active_image = get_image(image_id); active_image.tag = null; stage.beginLoad(); var img = JX.$N('img', {className: 'pholio-mock-image'}); img.onload = JX.bind(img, onload_image, active_image.id); img.src = active_image.fullURI; var thumbs = JX.DOM.scry( JX.$('pholio-mock-carousel'), 'a', 'mock-thumbnail'); for(var k in thumbs) { var thumb_meta = JX.Stratcom.getData(thumbs[k]); JX.DOM.alterClass( thumbs[k], 'pholio-mock-carousel-thumb-current', (active_image.id == thumb_meta.imageID)); } load_inline_comments(); if (image_id != config.selectedID) { JX.History.replace(active_image.pageURI); } } JX.Stratcom.listen( ['mousedown', 'click'], 'mock-thumbnail', function(e) { if (!e.isNormalMouseEvent()) { return; } e.kill(); select_image(e.getNodeData('mock-thumbnail').imageID); }); select_image(config.selectedID); JX.Stratcom.listen('mousedown', 'mock-viewport', function(e) { if (!e.isNormalMouseEvent()) { return; } if (JX.Device.getDevice() != 'desktop') { return; } if (drag_begin) { return; } e.kill(); is_dragging = true; drag_begin = get_image_xy(JX.$V(e)); drag_end = drag_begin; redraw_selection(); }); JX.enableDispatch(document.body, 'mousemove'); JX.Stratcom.listen('mousemove', null, function(e) { if (!is_dragging) { return; } drag_end = get_image_xy(JX.$V(e)); redraw_selection(); }); JX.Stratcom.listen( ['mouseover', 'mouseout'], 'pholio-inline-ref', function(e) { var phid = e.getNodeData('pholio-inline-ref').phid; var show = (e.getType() == 'mouseover'); stage.highlightInline(phid, show); }); JX.Stratcom.listen( 'mouseup', null, function(e) { if (!is_dragging) { return; } is_dragging = false; + if (!config.loggedIn) { + new JX.Workflow(config.logInLink).start(); + return; + } + drag_end = get_image_xy(JX.$V(e)); var data = {mockID: config.mockID}; var handler = function(r) { var dialog = JX.$H(r).getFragment().firstChild; JX.DOM.appendContent(viewport, dialog); JX.$V( Math.min(drag_begin.x, drag_end.x), Math.max(drag_begin.y, drag_end.y) + 4 ).setPos(dialog); JX.DOM.focus(JX.DOM.find(dialog, 'textarea')); } new JX.Workflow('/pholio/inline/save/', data) .setHandler(handler) .start(); }); function redraw_inlines(id) { if (!active_image) { return; } if (active_image.id != id) { return; } stage.clearStage(); var comment_holder = JX.$('mock-inline-comments'); JX.DOM.setContent(comment_holder, render_image_info(active_image)); var inlines = inline_comments[active_image.id]; if (!inlines || !inlines.length) { return; } for (var ii = 0; ii < inlines.length; ii++) { var inline = inlines[ii]; var card = JX.$H(inline.contentHTML).getFragment().firstChild; stage.addCard(card, inline.phid); if (!active_image.tag) { // The image itself hasn't loaded yet, so we can't draw the inline // reticles. continue; } var inline_selection = render_reticle_fill(); stage.addReticle(inline_selection, inline.phid); position_inline_rectangle(inline, inline_selection); if (!inline.transactionphid) { var inline_draft = render_reticle_border(); stage.addReticle(inline_draft, inline.phid); position_inline_rectangle(inline, inline_draft); } } } function position_inline_rectangle(inline, rect) { var scale = active_image.tag.width / active_image.tag.naturalWidth; JX.$V(scale * inline.x, scale * inline.y).setPos(rect); JX.$V(scale * inline.width, scale * inline.height).setDim(rect); } function get_image_xy(p) { var img = active_image.tag; var imgp = JX.$V(img); var scale = 1 / get_image_scale(); var x = scale * Math.max(0, Math.min(p.x - imgp.x, img.width)); var y = scale * Math.max(0, Math.min(p.y - imgp.y, img.height)); return { x: x, y: y }; } function get_image_scale() { var img = active_image.tag; return img.width / img.naturalWidth; } function redraw_selection() { selection_border = selection_border || render_reticle_border(); selection_fill = selection_fill || render_reticle_fill(); var p = JX.$V( Math.min(drag_begin.x, drag_end.x), Math.min(drag_begin.y, drag_end.y)); var d = JX.$V( Math.max(drag_begin.x, drag_end.x) - p.x, Math.max(drag_begin.y, drag_end.y) - p.y); var scale = get_image_scale(); p.x *= scale; p.y *= scale; d.x *= scale; d.y *= scale; var nodes = [selection_fill, selection_border]; for (var ii = 0; ii < nodes.length; ii++) { var node = nodes[ii]; viewport.appendChild(node); p.setPos(node); d.setDim(node); } } function clear_selection() { selection_fill && JX.DOM.remove(selection_fill); selection_border && JX.DOM.remove(selection_border); } function load_inline_comments() { var id = active_image.id; var inline_comments_uri = "/pholio/inline/" + id + "/"; new JX.Request(inline_comments_uri, function(r) { inline_comments[id] = r; redraw_inlines(id); }).send(); } JX.Stratcom.listen( 'click', 'inline-delete', function(e) { var data = e.getNodeData('inline-delete'); e.kill(); interrupt_typing(); stage.removeInline(data.phid); var deleteURI = '/pholio/inline/delete/' + data.id + '/'; var del = new JX.Request(deleteURI, function(r) { }); del.send(); }); JX.Stratcom.listen( 'click', 'inline-edit', function(e) { var data = e.getNodeData('inline-edit'); e.kill(); interrupt_typing(); var editURI = "/pholio/inline/edit/" + data.id + '/'; var edit_dialog = new JX.Request(editURI, function(r) { var dialog = JX.$N( 'div', { className: 'pholio-edit-inline-popup' }, JX.$H(r)); JX.DOM.setContent(JX.$(data.phid + '_comment'), dialog); }); edit_dialog.send(); }); JX.Stratcom.listen( 'click', 'inline-edit-cancel', function(e) { var data = e.getNodeData('inline-edit-cancel'); e.kill(); load_inline_comment(data.id); }); JX.Stratcom.listen( 'click', 'inline-edit-submit', function(e) { var data = e.getNodeData('inline-edit-submit'); var editURI = "/pholio/inline/edit/" + data.id + '/'; e.kill(); var edit = new JX.Request(editURI, function(r) { load_inline_comment(data.id); }); edit.addData({ op: 'update', content: JX.DOM.find(JX.$(data.phid + '_comment'), 'textarea').value }); edit.send(); }); JX.Stratcom.listen( 'click', 'inline-save-cancel', function(e) { e.kill(); interrupt_typing(); } ); JX.Stratcom.listen( 'click', 'inline-save-submit', function(e) { e.kill(); var form = JX.$('pholio-new-inline-comment-dialog'); var text = JX.DOM.find(form, 'textarea').value; if (!text.length) { interrupt_typing(); return; } var data = { mockID: config.mockID, imageID: active_image.id, startX: Math.min(drag_begin.x, drag_end.x), startY: Math.min(drag_begin.y, drag_end.y), endX: Math.max(drag_begin.x, drag_end.x), endY: Math.max(drag_begin.y, drag_end.y) }; var handler = function(r) { if (!inline_comments[active_image.id]) { inline_comments[active_image.id] = []; } inline_comments[active_image.id].push(r); interrupt_typing(); redraw_inlines(active_image.id); }; JX.Workflow.newFromForm(form, data) .setHandler(handler) .start(); } ); function load_inline_comment(id) { var viewInlineURI = '/pholio/inline/view/' + id + '/'; var inline_comment = new JX.Request(viewInlineURI, function(r) { JX.DOM.replace(JX.$(r.phid + '_comment'), JX.$H(r.contentHTML)); }); inline_comment.send(); } function interrupt_typing() { clear_selection(); try { JX.DOM.remove(JX.$('pholio-new-inline-comment-dialog')); } catch (x) { // TODO: For now, ignore this. } drag_begin = null; } load_inline_comments(); JX.Stratcom.listen('resize', null, redraw_image); redraw_image(); /* -( Keyboard Shortcuts )------------------------------------------------- */ new JX.KeyboardShortcut('j', 'Show next image.') .setHandler(function() { switch_image(1); }) .register(); new JX.KeyboardShortcut('k', 'Show previous image.') .setHandler(function() { switch_image(-1); }) .register(); /* -( Render )------------------------------------------------------------- */ function render_image_info(image) { var info = []; var title = JX.$N( 'div', {className: 'pholio-image-title'}, image.title); info.push(title); var desc = JX.$N( 'div', {className: 'pholio-image-description'}, image.desc); info.push(desc); var visible = null; if (image.tag) { var area = Math.round(100 * (image.tag.width / image.width)); area = ['(' + area + '%' + ')']; visible = [' ', JX.$N('span', {className: 'pholio-visible-size'}, area)]; } info.push([image.width, '\u00d7', image.height, 'px', visible]); var full_link = JX.$N( 'a', {href: image.fullURI, target: '_blank'}, 'View Full Image'); info.push(full_link); for (var ii = 0; ii < info.length; ii++) { info[ii] = JX.$N('div', {className: 'pholio-image-info-item'}, info[ii]); } info = JX.$N('div', {className: 'pholio-image-info'}, info); return info; } function render_reticle_border() { return JX.$N( 'div', {className: 'pholio-mock-select-border'}); } function render_reticle_fill() { return JX.$N( 'div', {className: 'pholio-mock-select-fill'}); } /* -( Device Lightbox )---------------------------------------------------- */ // On devices, we show images full-size when the user taps them instead of // attempting to implement inlines. var lightbox = null; JX.Stratcom.listen('click', 'mock-viewport', function(e) { if (!e.isNormalMouseEvent()) { return; } if (JX.Device.getDevice() == 'desktop') { return; } lightbox_attach(); e.kill(); }); JX.Stratcom.listen('click', 'pholio-device-lightbox', lightbox_detach); JX.Stratcom.listen('resize', null, lightbox_resize); function lightbox_attach() { JX.DOM.alterClass(document.body, 'lightbox-attached', true); JX.Mask.show('jx-dark-mask'); lightbox = lightbox_render(); var image = JX.$N('img'); image.onload = lightbox_loaded; setTimeout(function() { image.src = active_image.fullURI; }, 1000); JX.DOM.setContent(lightbox, image); JX.DOM.alterClass(lightbox, 'pholio-device-lightbox-loading', true); lightbox_resize(); document.body.appendChild(lightbox); } function lightbox_detach() { JX.DOM.remove(lightbox); JX.Mask.hide(); JX.DOM.alterClass(document.body, 'lightbox-attached', false); lightbox = null; } function lightbox_resize(e) { if (!lightbox) { return; } JX.Vector.getScroll().setPos(lightbox); JX.Vector.getViewport().setDim(lightbox); } function lightbox_loaded() { JX.DOM.alterClass(lightbox, 'pholio-device-lightbox-loading', false); } function lightbox_render() { var el = JX.$N('div', {className: 'pholio-device-lightbox'}); JX.Stratcom.addSigil(el, 'pholio-device-lightbox'); return el; } });