From 105cbaaee1dd52fe3ae6c05f531560163f29cd34 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 2 Nov 2015 18:58:32 -0800 Subject: [PATCH] Implement a basic version of ApplicationEditor in Paste Summary: Ref T9132. Ref T4768. This is a rough v0 of ApplicationEditor, which replaces the edit workflow in Paste. This mostly looks and works like ApplicationSearch, and is heavily modeled on it. Roughly, we define a set of editable fields and the ApplicationEditor stuff builds everything else. This has no functional changes, except: - I removed "Fork Paste" since I don't think it's particularly useful now that pastes are editable. We could restore it if users miss it. - Subscribers are now editable. - Form field order is a little goofy (this will be fixed in a future diff). - Subscribers and projects are now race-resistant. The race-resistance works like this: instead of submitting just the new value ("subscribers=apple, dog") and doing a set operation ("set subscribers = apple, dog"), we submit the old and new values ("original=apple" + "new=apple, dog") then apply the user's changes as an add + remove ("add=dog", "remove="). This means that two users who do "Edit Paste" at around the same time and each add or remove a couple of subscribers won't overwrite each other, unless they actually add or remove the exact same subscribers (in which case their edits legitimately conflict). Previously, the last user to save would win, and whatever was in their field would overwrite the prior state, potentially losing the first user's edits. Test Plan: - Created pastes. - Created pastes via API. - Edited pastes. - Edited every field. - Opened a paste in two windows and did project/subscriber edits in each, saved in arbitrary order, had edits respected. Reviewers: chad Reviewed By: chad Maniphest Tasks: T4768, T9132 Differential Revision: https://secure.phabricator.com/D14390 --- src/__phutil_library_map__.php | 20 ++ .../base/controller/PhabricatorController.php | 8 + .../conduit/PasteCreateConduitAPIMethod.php | 7 +- .../PhabricatorPasteEditController.php | 245 +------------- .../PhabricatorPasteViewController.php | 9 - .../editor/PhabricatorPasteEditEngine.php | 69 ++++ .../paste/editor/PhabricatorPasteEditor.php | 48 ++- .../paste/mail/PasteCreateMailReceiver.php | 7 +- .../paste/storage/PhabricatorPaste.php | 3 +- .../PhabricatorApplicationEditEngine.php | 303 ++++++++++++++++++ .../PhabricatorDatasourceEditField.php | 21 ++ .../editfield/PhabricatorEditField.php | 212 ++++++++++++ .../editfield/PhabricatorPolicyEditField.php | 54 ++++ .../editfield/PhabricatorSelectEditField.php | 25 ++ .../editfield/PhabricatorSpaceEditField.php | 12 + .../PhabricatorTextAreaEditField.php | 42 +++ .../editfield/PhabricatorTextEditField.php | 10 + .../PhabricatorTokenizerEditField.php | 90 ++++++ ...habricatorApplicationTransactionEditor.php | 15 +- .../control/AphrontTokenizerTemplateView.php | 25 ++ .../control/AphrontFormTokenizerControl.php | 23 +- 21 files changed, 978 insertions(+), 270 deletions(-) create mode 100644 src/applications/paste/editor/PhabricatorPasteEditEngine.php create mode 100644 src/applications/transactions/editengine/PhabricatorApplicationEditEngine.php create mode 100644 src/applications/transactions/editfield/PhabricatorDatasourceEditField.php create mode 100644 src/applications/transactions/editfield/PhabricatorEditField.php create mode 100644 src/applications/transactions/editfield/PhabricatorPolicyEditField.php create mode 100644 src/applications/transactions/editfield/PhabricatorSelectEditField.php create mode 100644 src/applications/transactions/editfield/PhabricatorSpaceEditField.php create mode 100644 src/applications/transactions/editfield/PhabricatorTextAreaEditField.php create mode 100644 src/applications/transactions/editfield/PhabricatorTextEditField.php create mode 100644 src/applications/transactions/editfield/PhabricatorTokenizerEditField.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index c8240728ce..de98b8f92b 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1570,6 +1570,7 @@ phutil_register_library_map(array( 'PhabricatorApplicationDatasource' => 'applications/meta/typeahead/PhabricatorApplicationDatasource.php', 'PhabricatorApplicationDetailViewController' => 'applications/meta/controller/PhabricatorApplicationDetailViewController.php', 'PhabricatorApplicationEditController' => 'applications/meta/controller/PhabricatorApplicationEditController.php', + 'PhabricatorApplicationEditEngine' => 'applications/transactions/editengine/PhabricatorApplicationEditEngine.php', 'PhabricatorApplicationEmailCommandsController' => 'applications/meta/controller/PhabricatorApplicationEmailCommandsController.php', 'PhabricatorApplicationLaunchView' => 'applications/meta/view/PhabricatorApplicationLaunchView.php', 'PhabricatorApplicationPanelController' => 'applications/meta/controller/PhabricatorApplicationPanelController.php', @@ -2063,6 +2064,7 @@ phutil_register_library_map(array( 'PhabricatorDataCacheSpec' => 'applications/cache/spec/PhabricatorDataCacheSpec.php', 'PhabricatorDataNotAttachedException' => 'infrastructure/storage/lisk/PhabricatorDataNotAttachedException.php', 'PhabricatorDatabaseSetupCheck' => 'applications/config/check/PhabricatorDatabaseSetupCheck.php', + 'PhabricatorDatasourceEditField' => 'applications/transactions/editfield/PhabricatorDatasourceEditField.php', 'PhabricatorDateTimeSettingsPanel' => 'applications/settings/panel/PhabricatorDateTimeSettingsPanel.php', 'PhabricatorDebugController' => 'applications/system/controller/PhabricatorDebugController.php', 'PhabricatorDefaultRequestExceptionHandler' => 'aphront/handler/PhabricatorDefaultRequestExceptionHandler.php', @@ -2097,6 +2099,7 @@ phutil_register_library_map(array( 'PhabricatorEdgeTestCase' => 'infrastructure/edges/__tests__/PhabricatorEdgeTestCase.php', 'PhabricatorEdgeType' => 'infrastructure/edges/type/PhabricatorEdgeType.php', 'PhabricatorEdgeTypeTestCase' => 'infrastructure/edges/type/__tests__/PhabricatorEdgeTypeTestCase.php', + 'PhabricatorEditField' => 'applications/transactions/editfield/PhabricatorEditField.php', 'PhabricatorEditor' => 'infrastructure/PhabricatorEditor.php', 'PhabricatorElasticSearchEngine' => 'applications/search/engine/PhabricatorElasticSearchEngine.php', 'PhabricatorElasticSearchSetupCheck' => 'applications/config/check/PhabricatorElasticSearchSetupCheck.php', @@ -2554,6 +2557,7 @@ phutil_register_library_map(array( 'PhabricatorPasteController' => 'applications/paste/controller/PhabricatorPasteController.php', 'PhabricatorPasteDAO' => 'applications/paste/storage/PhabricatorPasteDAO.php', 'PhabricatorPasteEditController' => 'applications/paste/controller/PhabricatorPasteEditController.php', + 'PhabricatorPasteEditEngine' => 'applications/paste/editor/PhabricatorPasteEditEngine.php', 'PhabricatorPasteEditor' => 'applications/paste/editor/PhabricatorPasteEditor.php', 'PhabricatorPasteListController' => 'applications/paste/controller/PhabricatorPasteListController.php', 'PhabricatorPastePastePHIDType' => 'applications/paste/phid/PhabricatorPastePastePHIDType.php', @@ -2654,6 +2658,7 @@ phutil_register_library_map(array( 'PhabricatorPolicyDAO' => 'applications/policy/storage/PhabricatorPolicyDAO.php', 'PhabricatorPolicyDataTestCase' => 'applications/policy/__tests__/PhabricatorPolicyDataTestCase.php', 'PhabricatorPolicyEditController' => 'applications/policy/controller/PhabricatorPolicyEditController.php', + 'PhabricatorPolicyEditField' => 'applications/transactions/editfield/PhabricatorPolicyEditField.php', 'PhabricatorPolicyException' => 'applications/policy/exception/PhabricatorPolicyException.php', 'PhabricatorPolicyExplainController' => 'applications/policy/controller/PhabricatorPolicyExplainController.php', 'PhabricatorPolicyFilter' => 'applications/policy/filter/PhabricatorPolicyFilter.php', @@ -2917,6 +2922,7 @@ phutil_register_library_map(array( 'PhabricatorSearchWorker' => 'applications/search/worker/PhabricatorSearchWorker.php', 'PhabricatorSecurityConfigOptions' => 'applications/config/option/PhabricatorSecurityConfigOptions.php', 'PhabricatorSecuritySetupCheck' => 'applications/config/check/PhabricatorSecuritySetupCheck.php', + 'PhabricatorSelectEditField' => 'applications/transactions/editfield/PhabricatorSelectEditField.php', 'PhabricatorSendGridConfigOptions' => 'applications/config/option/PhabricatorSendGridConfigOptions.php', 'PhabricatorSessionsSettingsPanel' => 'applications/settings/panel/PhabricatorSessionsSettingsPanel.php', 'PhabricatorSettingsAddEmailAction' => 'applications/settings/action/PhabricatorSettingsAddEmailAction.php', @@ -2957,6 +2963,7 @@ phutil_register_library_map(array( 'PhabricatorSlugTestCase' => 'infrastructure/util/__tests__/PhabricatorSlugTestCase.php', 'PhabricatorSortTableUIExample' => 'applications/uiexample/examples/PhabricatorSortTableUIExample.php', 'PhabricatorSourceCodeView' => 'view/layout/PhabricatorSourceCodeView.php', + 'PhabricatorSpaceEditField' => 'applications/transactions/editfield/PhabricatorSpaceEditField.php', 'PhabricatorSpacesApplication' => 'applications/spaces/application/PhabricatorSpacesApplication.php', 'PhabricatorSpacesArchiveController' => 'applications/spaces/controller/PhabricatorSpacesArchiveController.php', 'PhabricatorSpacesCapabilityCreateSpaces' => 'applications/spaces/capability/PhabricatorSpacesCapabilityCreateSpaces.php', @@ -3063,6 +3070,8 @@ phutil_register_library_map(array( 'PhabricatorTestNoCycleEdgeType' => 'applications/transactions/edges/PhabricatorTestNoCycleEdgeType.php', 'PhabricatorTestStorageEngine' => 'applications/files/engine/PhabricatorTestStorageEngine.php', 'PhabricatorTestWorker' => 'infrastructure/daemon/workers/__tests__/PhabricatorTestWorker.php', + 'PhabricatorTextAreaEditField' => 'applications/transactions/editfield/PhabricatorTextAreaEditField.php', + 'PhabricatorTextEditField' => 'applications/transactions/editfield/PhabricatorTextEditField.php', 'PhabricatorTime' => 'infrastructure/time/PhabricatorTime.php', 'PhabricatorTimeGuard' => 'infrastructure/time/PhabricatorTimeGuard.php', 'PhabricatorTimeTestCase' => 'infrastructure/time/__tests__/PhabricatorTimeTestCase.php', @@ -3084,6 +3093,7 @@ phutil_register_library_map(array( 'PhabricatorTokenReceiverQuery' => 'applications/tokens/query/PhabricatorTokenReceiverQuery.php', 'PhabricatorTokenTokenPHIDType' => 'applications/tokens/phid/PhabricatorTokenTokenPHIDType.php', 'PhabricatorTokenUIEventListener' => 'applications/tokens/event/PhabricatorTokenUIEventListener.php', + 'PhabricatorTokenizerEditField' => 'applications/transactions/editfield/PhabricatorTokenizerEditField.php', 'PhabricatorTokensApplication' => 'applications/tokens/application/PhabricatorTokensApplication.php', 'PhabricatorTokensSettingsPanel' => 'applications/settings/panel/PhabricatorTokensSettingsPanel.php', 'PhabricatorTooltipUIExample' => 'applications/uiexample/examples/PhabricatorTooltipUIExample.php', @@ -5489,6 +5499,7 @@ phutil_register_library_map(array( 'PhabricatorApplicationDatasource' => 'PhabricatorTypeaheadDatasource', 'PhabricatorApplicationDetailViewController' => 'PhabricatorApplicationsController', 'PhabricatorApplicationEditController' => 'PhabricatorApplicationsController', + 'PhabricatorApplicationEditEngine' => 'Phobject', 'PhabricatorApplicationEmailCommandsController' => 'PhabricatorApplicationsController', 'PhabricatorApplicationLaunchView' => 'AphrontTagView', 'PhabricatorApplicationPanelController' => 'PhabricatorApplicationsController', @@ -6080,6 +6091,7 @@ phutil_register_library_map(array( 'PhabricatorDataCacheSpec' => 'PhabricatorCacheSpec', 'PhabricatorDataNotAttachedException' => 'Exception', 'PhabricatorDatabaseSetupCheck' => 'PhabricatorSetupCheck', + 'PhabricatorDatasourceEditField' => 'PhabricatorTokenizerEditField', 'PhabricatorDateTimeSettingsPanel' => 'PhabricatorSettingsPanel', 'PhabricatorDebugController' => 'PhabricatorController', 'PhabricatorDefaultRequestExceptionHandler' => 'PhabricatorRequestExceptionHandler', @@ -6113,6 +6125,7 @@ phutil_register_library_map(array( 'PhabricatorEdgeTestCase' => 'PhabricatorTestCase', 'PhabricatorEdgeType' => 'Phobject', 'PhabricatorEdgeTypeTestCase' => 'PhabricatorTestCase', + 'PhabricatorEditField' => 'Phobject', 'PhabricatorEditor' => 'Phobject', 'PhabricatorElasticSearchEngine' => 'PhabricatorSearchEngine', 'PhabricatorElasticSearchSetupCheck' => 'PhabricatorSetupCheck', @@ -6647,6 +6660,7 @@ phutil_register_library_map(array( 'PhabricatorPasteController' => 'PhabricatorController', 'PhabricatorPasteDAO' => 'PhabricatorLiskDAO', 'PhabricatorPasteEditController' => 'PhabricatorPasteController', + 'PhabricatorPasteEditEngine' => 'PhabricatorApplicationEditEngine', 'PhabricatorPasteEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorPasteListController' => 'PhabricatorPasteController', 'PhabricatorPastePastePHIDType' => 'PhabricatorPHIDType', @@ -6762,6 +6776,7 @@ phutil_register_library_map(array( 'PhabricatorPolicyDAO' => 'PhabricatorLiskDAO', 'PhabricatorPolicyDataTestCase' => 'PhabricatorTestCase', 'PhabricatorPolicyEditController' => 'PhabricatorPolicyController', + 'PhabricatorPolicyEditField' => 'PhabricatorEditField', 'PhabricatorPolicyException' => 'Exception', 'PhabricatorPolicyExplainController' => 'PhabricatorPolicyController', 'PhabricatorPolicyFilter' => 'Phobject', @@ -7090,6 +7105,7 @@ phutil_register_library_map(array( 'PhabricatorSearchWorker' => 'PhabricatorWorker', 'PhabricatorSecurityConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorSecuritySetupCheck' => 'PhabricatorSetupCheck', + 'PhabricatorSelectEditField' => 'PhabricatorEditField', 'PhabricatorSendGridConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorSessionsSettingsPanel' => 'PhabricatorSettingsPanel', 'PhabricatorSettingsAddEmailAction' => 'PhabricatorSystemAction', @@ -7140,6 +7156,7 @@ phutil_register_library_map(array( 'PhabricatorSlugTestCase' => 'PhabricatorTestCase', 'PhabricatorSortTableUIExample' => 'PhabricatorUIExample', 'PhabricatorSourceCodeView' => 'AphrontView', + 'PhabricatorSpaceEditField' => 'PhabricatorEditField', 'PhabricatorSpacesApplication' => 'PhabricatorApplication', 'PhabricatorSpacesArchiveController' => 'PhabricatorSpacesController', 'PhabricatorSpacesCapabilityCreateSpaces' => 'PhabricatorPolicyCapability', @@ -7252,6 +7269,8 @@ phutil_register_library_map(array( 'PhabricatorTestNoCycleEdgeType' => 'PhabricatorEdgeType', 'PhabricatorTestStorageEngine' => 'PhabricatorFileStorageEngine', 'PhabricatorTestWorker' => 'PhabricatorWorker', + 'PhabricatorTextAreaEditField' => 'PhabricatorEditField', + 'PhabricatorTextEditField' => 'PhabricatorEditField', 'PhabricatorTime' => 'Phobject', 'PhabricatorTimeGuard' => 'Phobject', 'PhabricatorTimeTestCase' => 'PhabricatorTestCase', @@ -7278,6 +7297,7 @@ phutil_register_library_map(array( 'PhabricatorTokenReceiverQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorTokenTokenPHIDType' => 'PhabricatorPHIDType', 'PhabricatorTokenUIEventListener' => 'PhabricatorEventListener', + 'PhabricatorTokenizerEditField' => 'PhabricatorEditField', 'PhabricatorTokensApplication' => 'PhabricatorApplication', 'PhabricatorTokensSettingsPanel' => 'PhabricatorSettingsPanel', 'PhabricatorTooltipUIExample' => 'PhabricatorUIExample', diff --git a/src/applications/base/controller/PhabricatorController.php b/src/applications/base/controller/PhabricatorController.php index 1ab2b601e2..7999626709 100644 --- a/src/applications/base/controller/PhabricatorController.php +++ b/src/applications/base/controller/PhabricatorController.php @@ -521,6 +521,14 @@ abstract class PhabricatorController extends AphrontController { } + public function buildApplicationCrumbsForEditEngine() { + // TODO: This is kind of gross, I'm bascially just making this public so + // I can use it in EditEngine. We could do this without making it public + // by using controller delegation, or make it properly public. + return $this->buildApplicationCrumbs(); + } + + /* -( Deprecated )--------------------------------------------------------- */ diff --git a/src/applications/paste/conduit/PasteCreateConduitAPIMethod.php b/src/applications/paste/conduit/PasteCreateConduitAPIMethod.php index 379f5b6e8d..d5ce021f13 100644 --- a/src/applications/paste/conduit/PasteCreateConduitAPIMethod.php +++ b/src/applications/paste/conduit/PasteCreateConduitAPIMethod.php @@ -44,16 +44,11 @@ final class PasteCreateConduitAPIMethod extends PasteConduitAPIMethod { $paste = PhabricatorPaste::initializeNewPaste($viewer); - $file = PhabricatorPasteEditor::initializeFileForPaste( - $viewer, - $title, - $content); - $xactions = array(); $xactions[] = id(new PhabricatorPasteTransaction()) ->setTransactionType(PhabricatorPasteTransaction::TYPE_CONTENT) - ->setNewValue($file->getPHID()); + ->setNewValue($content); $xactions[] = id(new PhabricatorPasteTransaction()) ->setTransactionType(PhabricatorPasteTransaction::TYPE_TITLE) diff --git a/src/applications/paste/controller/PhabricatorPasteEditController.php b/src/applications/paste/controller/PhabricatorPasteEditController.php index 331acf7f45..c9ee5a289e 100644 --- a/src/applications/paste/controller/PhabricatorPasteEditController.php +++ b/src/applications/paste/controller/PhabricatorPasteEditController.php @@ -3,248 +3,9 @@ final class PhabricatorPasteEditController extends PhabricatorPasteController { public function handleRequest(AphrontRequest $request) { - $viewer = $request->getViewer(); - $id = $request->getURIData('id'); - - $parent = null; - $parent_id = null; - if (!$id) { - $is_create = true; - - $paste = PhabricatorPaste::initializeNewPaste($viewer); - - $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($viewer) - ->withIDs(array($parent_id)) - ->needContent(true) - ->needRawContent(true) - ->execute(); - $parent = head($parent); - - if ($parent) { - $paste->setParentPHID($parent->getPHID()); - $paste->setViewPolicy($parent->getViewPolicy()); - } - } - - $paste->setAuthorPHID($viewer->getPHID()); - $paste->attachRawContent(''); - } else { - $is_create = false; - - $paste = id(new PhabricatorPasteQuery()) - ->setViewer($viewer) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) - ->withIDs(array($id)) - ->needRawContent(true) - ->executeOne(); - if (!$paste) { - return new Aphront404Response(); - } - } - - $v_space = $paste->getSpacePHID(); - if ($is_create && $parent) { - $v_title = pht('Fork of %s', $parent->getFullName()); - $v_language = $parent->getLanguage(); - $v_text = $parent->getRawContent(); - $v_space = $parent->getSpacePHID(); - } else { - $v_title = $paste->getTitle(); - $v_language = $paste->getLanguage(); - $v_text = $paste->getRawContent(); - } - $v_view_policy = $paste->getViewPolicy(); - $v_edit_policy = $paste->getEditPolicy(); - $v_status = $paste->getStatus(); - - if ($is_create) { - $v_projects = array(); - } else { - $v_projects = PhabricatorEdgeQuery::loadDestinationPHIDs( - $paste->getPHID(), - PhabricatorProjectObjectHasProjectEdgeType::EDGECONST); - $v_projects = array_reverse($v_projects); - } - - $validation_exception = null; - if ($request->isFormPost()) { - $xactions = array(); - - $v_text = $request->getStr('text'); - $v_title = $request->getStr('title'); - $v_language = $request->getStr('language'); - $v_view_policy = $request->getStr('can_view'); - $v_edit_policy = $request->getStr('can_edit'); - $v_projects = $request->getArr('projects'); - $v_space = $request->getStr('spacePHID'); - $v_status = $request->getStr('status'); - - // NOTE: The author is the only editor and can always view the paste, - // so it's impossible for them to choose an invalid policy. - - if ($is_create || ($v_text !== $paste->getRawContent())) { - $file = PhabricatorPasteEditor::initializeFileForPaste( - $viewer, - $v_title, - $v_text); - - $xactions[] = id(new PhabricatorPasteTransaction()) - ->setTransactionType(PhabricatorPasteTransaction::TYPE_CONTENT) - ->setNewValue($file->getPHID()); - } - - $xactions[] = id(new PhabricatorPasteTransaction()) - ->setTransactionType(PhabricatorPasteTransaction::TYPE_TITLE) - ->setNewValue($v_title); - $xactions[] = id(new PhabricatorPasteTransaction()) - ->setTransactionType(PhabricatorPasteTransaction::TYPE_LANGUAGE) - ->setNewValue($v_language); - $xactions[] = id(new PhabricatorPasteTransaction()) - ->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY) - ->setNewValue($v_view_policy); - $xactions[] = id(new PhabricatorPasteTransaction()) - ->setTransactionType(PhabricatorTransactions::TYPE_EDIT_POLICY) - ->setNewValue($v_edit_policy); - $xactions[] = id(new PhabricatorPasteTransaction()) - ->setTransactionType(PhabricatorTransactions::TYPE_SPACE) - ->setNewValue($v_space); - $xactions[] = id(new PhabricatorPasteTransaction()) - ->setTransactionType(PhabricatorPasteTransaction::TYPE_STATUS) - ->setNewValue($v_status); - - $proj_edge_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST; - $xactions[] = id(new PhabricatorPasteTransaction()) - ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) - ->setMetadataValue('edge:type', $proj_edge_type) - ->setNewValue(array('=' => array_fuse($v_projects))); - - $editor = id(new PhabricatorPasteEditor()) - ->setActor($viewer) - ->setContentSourceFromRequest($request) - ->setContinueOnNoEffect(true); - - try { - $xactions = $editor->applyTransactions($paste, $xactions); - return id(new AphrontRedirectResponse())->setURI($paste->getURI()); - } catch (PhabricatorApplicationTransactionValidationException $ex) { - $validation_exception = $ex; - } - } - - $form = new AphrontFormView(); - - $langs = array( - '' => pht('(Detect From Filename in Title)'), - ) + PhabricatorEnv::getEnvConfig('pygments.dropdown-choices'); - - $form - ->setUser($viewer) - ->addHiddenInput('parent', $parent_id) - ->appendChild( - id(new AphrontFormTextControl()) - ->setLabel(pht('Title')) - ->setValue($v_title) - ->setName('title')) - ->appendChild( - id(new AphrontFormSelectControl()) - ->setLabel(pht('Language')) - ->setName('language') - ->setValue($v_language) - ->setOptions($langs)); - - $policies = id(new PhabricatorPolicyQuery()) - ->setViewer($viewer) - ->setObject($paste) - ->execute(); - - $form->appendChild( - id(new AphrontFormPolicyControl()) - ->setUser($viewer) - ->setCapability(PhabricatorPolicyCapability::CAN_VIEW) - ->setPolicyObject($paste) - ->setPolicies($policies) - ->setValue($v_view_policy) - ->setSpacePHID($v_space) - ->setName('can_view')); - - $form->appendChild( - id(new AphrontFormPolicyControl()) - ->setUser($viewer) - ->setCapability(PhabricatorPolicyCapability::CAN_EDIT) - ->setPolicyObject($paste) - ->setPolicies($policies) - ->setValue($v_edit_policy) - ->setName('can_edit')); - - $form->appendChild( - id(new AphrontFormSelectControl()) - ->setLabel(pht('Status')) - ->setName('status') - ->setValue($v_status) - ->setOptions($paste->getStatusNameMap())); - - $form->appendControl( - id(new AphrontFormTokenizerControl()) - ->setLabel(pht('Projects')) - ->setName('projects') - ->setValue($v_projects) - ->setDatasource(new PhabricatorProjectDatasource())); - - $form - ->appendChild( - id(new AphrontFormTextAreaControl()) - ->setLabel(pht('Text')) - ->setValue($v_text) - ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL) - ->setCustomClass('PhabricatorMonospaced') - ->setName('text')); - - $submit = new AphrontFormSubmitControl(); - - if (!$is_create) { - $submit->addCancelButton($paste->getURI()); - $submit->setValue(pht('Save Paste')); - $title = pht('Edit %s', $paste->getFullName()); - $short = pht('Edit'); - } else { - $submit->setValue(pht('Create Paste')); - $title = pht('Create New Paste'); - $short = pht('Create'); - } - - $form->appendChild($submit); - - $form_box = id(new PHUIObjectBoxView()) - ->setHeaderText($title) - ->setForm($form); - - if ($validation_exception) { - $form_box->setValidationException($validation_exception); - } - - $crumbs = $this->buildApplicationCrumbs(); - if (!$is_create) { - $crumbs->addTextCrumb('P'.$paste->getID(), '/P'.$paste->getID()); - } - $crumbs->addTextCrumb($short); - - return $this->buildApplicationPage( - array( - $crumbs, - $form_box, - ), - array( - 'title' => $title, - )); + return id(new PhabricatorPasteEditEngine()) + ->setController($this) + ->buildResponse(); } } diff --git a/src/applications/paste/controller/PhabricatorPasteViewController.php b/src/applications/paste/controller/PhabricatorPasteViewController.php index 724821dc3c..43a7058f59 100644 --- a/src/applications/paste/controller/PhabricatorPasteViewController.php +++ b/src/applications/paste/controller/PhabricatorPasteViewController.php @@ -137,9 +137,7 @@ final class PhabricatorPasteViewController extends PhabricatorPasteController { $paste, PhabricatorPolicyCapability::CAN_EDIT); - $can_fork = $viewer->isLoggedIn(); $id = $paste->getID(); - $fork_uri = $this->getApplicationURI('/create/?parent='.$id); return id(new PhabricatorActionListView()) ->setUser($viewer) @@ -152,13 +150,6 @@ final class PhabricatorPasteViewController extends PhabricatorPasteController { ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit) ->setHref($this->getApplicationURI("edit/{$id}/"))) - ->addAction( - id(new PhabricatorActionView()) - ->setName(pht('Fork This Paste')) - ->setIcon('fa-code-fork') - ->setDisabled(!$can_fork) - ->setWorkflow(!$can_fork) - ->setHref($fork_uri)) ->addAction( id(new PhabricatorActionView()) ->setName(pht('View Raw File')) diff --git a/src/applications/paste/editor/PhabricatorPasteEditEngine.php b/src/applications/paste/editor/PhabricatorPasteEditEngine.php new file mode 100644 index 0000000000..98f7399381 --- /dev/null +++ b/src/applications/paste/editor/PhabricatorPasteEditEngine.php @@ -0,0 +1,69 @@ +getViewer()); + } + + protected function newObjectQuery() { + return id(new PhabricatorPasteQuery()) + ->needRawContent(true); + } + + protected function getObjectCreateTitleText($object) { + return pht('Create New Paste'); + } + + protected function getObjectEditTitleText($object) { + return pht('Edit %s %s', $object->getMonogram(), $object->getTitle()); + } + + protected function getObjectEditShortText($object) { + return $object->getMonogram(); + } + + protected function getObjectCreateShortText($object) { + return pht('Create Paste'); + } + + protected function getObjectViewURI($object) { + return '/P'.$object->getID(); + } + + protected function buildCustomEditFields($object) { + $langs = array( + '' => pht('(Detect From Filename in Title)'), + ) + PhabricatorEnv::getEnvConfig('pygments.dropdown-choices'); + + return array( + id(new PhabricatorTextEditField()) + ->setKey('title') + ->setLabel(pht('Title')) + ->setTransactionType(PhabricatorPasteTransaction::TYPE_TITLE) + ->setValue($object->getTitle()), + id(new PhabricatorSelectEditField()) + ->setKey('language') + ->setLabel(pht('Language')) + ->setAliases(array('lang')) + ->setTransactionType(PhabricatorPasteTransaction::TYPE_LANGUAGE) + ->setValue($object->getLanguage()) + ->setOptions($langs), + id(new PhabricatorSelectEditField()) + ->setKey('status') + ->setLabel(pht('Status')) + ->setTransactionType(PhabricatorPasteTransaction::TYPE_STATUS) + ->setValue($object->getStatus()) + ->setOptions(PhabricatorPaste::getStatusNameMap()), + id(new PhabricatorTextAreaEditField()) + ->setKey('text') + ->setLabel(pht('Text')) + ->setTransactionType(PhabricatorPasteTransaction::TYPE_CONTENT) + ->setMonospaced(true) + ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL) + ->setValue($object->getRawContent()), + ); + } + +} diff --git a/src/applications/paste/editor/PhabricatorPasteEditor.php b/src/applications/paste/editor/PhabricatorPasteEditor.php index bae2003afe..da39b8beeb 100644 --- a/src/applications/paste/editor/PhabricatorPasteEditor.php +++ b/src/applications/paste/editor/PhabricatorPasteEditor.php @@ -3,6 +3,8 @@ final class PhabricatorPasteEditor extends PhabricatorApplicationTransactionEditor { + private $fileName; + public function getEditorApplicationClass() { return 'PhabricatorPasteApplication'; } @@ -41,6 +43,35 @@ final class PhabricatorPasteEditor return $types; } + protected function shouldApplyInitialEffects( + PhabricatorLiskDAO $object, + array $xactions) { + return true; + } + + protected function applyInitialEffects( + PhabricatorLiskDAO $object, + array $xactions) { + + // Find the most user-friendly filename we can by examining the title of + // the paste and the pending transactions. We'll use this if we create a + // new file to store raw content later. + + $name = $object->getTitle(); + if (!strlen($name)) { + $name = 'paste.raw'; + } + + $type_title = PhabricatorPasteTransaction::TYPE_TITLE; + foreach ($xactions as $xaction) { + if ($xaction->getTransactionType() == $type_title) { + $name = $xaction->getNewValue(); + } + } + + $this->fileName = $name; + } + protected function getCustomTransactionOldValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { @@ -62,11 +93,26 @@ final class PhabricatorPasteEditor PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { - case PhabricatorPasteTransaction::TYPE_CONTENT: case PhabricatorPasteTransaction::TYPE_TITLE: case PhabricatorPasteTransaction::TYPE_LANGUAGE: case PhabricatorPasteTransaction::TYPE_STATUS: return $xaction->getNewValue(); + case PhabricatorPasteTransaction::TYPE_CONTENT: + // If this transaction does not really change the paste content, return + // the current file PHID so this transaction no-ops. + $new_content = $xaction->getNewValue(); + $old_content = $object->getRawContent(); + $file_phid = $object->getFilePHID(); + if (($new_content === $old_content) && $file_phid) { + return $file_phid; + } + + $file = self::initializeFileForPaste( + $this->getActor(), + $this->fileName, + $xaction->getNewValue()); + + return $file->getPHID(); } } diff --git a/src/applications/paste/mail/PasteCreateMailReceiver.php b/src/applications/paste/mail/PasteCreateMailReceiver.php index d0c50d5fcf..672667cd65 100644 --- a/src/applications/paste/mail/PasteCreateMailReceiver.php +++ b/src/applications/paste/mail/PasteCreateMailReceiver.php @@ -21,16 +21,11 @@ final class PasteCreateMailReceiver extends PhabricatorMailReceiver { $title = pht('Email Paste'); } - $file = PhabricatorPasteEditor::initializeFileForPaste( - $sender, - $title, - $mail->getCleanTextBody()); - $xactions = array(); $xactions[] = id(new PhabricatorPasteTransaction()) ->setTransactionType(PhabricatorPasteTransaction::TYPE_CONTENT) - ->setNewValue($file->getPHID()); + ->setNewValue($mail->getCleanTextBody()); $xactions[] = id(new PhabricatorPasteTransaction()) ->setTransactionType(PhabricatorPasteTransaction::TYPE_TITLE) diff --git a/src/applications/paste/storage/PhabricatorPaste.php b/src/applications/paste/storage/PhabricatorPaste.php index 8aeda09aae..e156a87ea0 100644 --- a/src/applications/paste/storage/PhabricatorPaste.php +++ b/src/applications/paste/storage/PhabricatorPaste.php @@ -45,7 +45,8 @@ final class PhabricatorPaste extends PhabricatorPasteDAO ->setAuthorPHID($actor->getPHID()) ->setViewPolicy($view_policy) ->setEditPolicy($edit_policy) - ->setSpacePHID($actor->getDefaultSpacePHID()); + ->setSpacePHID($actor->getDefaultSpacePHID()) + ->attachRawContent(null); } public static function getStatusNameMap() { diff --git a/src/applications/transactions/editengine/PhabricatorApplicationEditEngine.php b/src/applications/transactions/editengine/PhabricatorApplicationEditEngine.php new file mode 100644 index 0000000000..4115a508ec --- /dev/null +++ b/src/applications/transactions/editengine/PhabricatorApplicationEditEngine.php @@ -0,0 +1,303 @@ +viewer = $viewer; + return $this; + } + + final public function getViewer() { + return $this->viewer; + } + + final public function setController(PhabricatorController $controller) { + $this->controller = $controller; + $this->setViewer($controller->getViewer()); + return $this; + } + + final public function getController() { + return $this->controller; + } + + final protected function buildEditFields($object) { + $viewer = $this->getViewer(); + $editor = $object->getApplicationTransactionEditor(); + + $types = $editor->getTransactionTypesForObject($object); + $types = array_fuse($types); + + $fields = $this->buildCustomEditFields($object); + + if ($object instanceof PhabricatorPolicyInterface) { + $policies = id(new PhabricatorPolicyQuery()) + ->setViewer($viewer) + ->setObject($object) + ->execute(); + + $map = array( + PhabricatorTransactions::TYPE_VIEW_POLICY => array( + 'key' => 'policy.view', + 'aliases' => array('view'), + 'capability' => PhabricatorPolicyCapability::CAN_VIEW, + ), + PhabricatorTransactions::TYPE_EDIT_POLICY => array( + 'key' => 'policy.edit', + 'aliases' => array('edit'), + 'capability' => PhabricatorPolicyCapability::CAN_EDIT, + ), + PhabricatorTransactions::TYPE_JOIN_POLICY => array( + 'key' => 'policy.join', + 'aliases' => array('join'), + 'capability' => PhabricatorPolicyCapability::CAN_JOIN, + ), + ); + + foreach ($map as $type => $spec) { + if (empty($types[$type])) { + continue; + } + + $capability = $spec['capability']; + $key = $spec['key']; + $aliases = $spec['aliases']; + + $policy_field = id(new PhabricatorPolicyEditField()) + ->setKey($key) + ->setAliases($aliases) + ->setCapability($capability) + ->setPolicies($policies) + ->setTransactionType($type) + ->setValue($object->getPolicy($capability)); + $fields[] = $policy_field; + + if ($object instanceof PhabricatorSpacesInterface) { + if ($capability == PhabricatorPolicyCapability::CAN_VIEW) { + $type_space = PhabricatorTransactions::TYPE_SPACE; + if (isset($types[$type_space])) { + $space_field = id(new PhabricatorSpaceEditField()) + ->setKey('spacePHID') + ->setAliases(array('space', 'policy.space')) + ->setTransactionType($type_space) + ->setValue($object->getSpacePHID()); + $fields[] = $space_field; + + $policy_field->setSpaceField($space_field); + } + } + } + } + } + + $edge_type = PhabricatorTransactions::TYPE_EDGE; + $object_phid = $object->getPHID(); + + $project_edge_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST; + + if ($object instanceof PhabricatorProjectInterface) { + if (isset($types[$edge_type])) { + if ($object_phid) { + $project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( + $object_phid, + $project_edge_type); + $project_phids = array_reverse($project_phids); + } else { + $project_phids = array(); + } + + $edge_field = id(new PhabricatorDatasourceEditField()) + ->setKey('projectPHIDs') + ->setLabel(pht('Projects')) + ->setDatasource(new PhabricatorProjectDatasource()) + ->setAliases(array('project', 'projects')) + ->setTransactionType($edge_type) + ->setMetadataValue('edge:type', $project_edge_type) + ->setValue($project_phids); + $fields[] = $edge_field; + } + } + + $subscribers_type = PhabricatorTransactions::TYPE_SUBSCRIBERS; + + if ($object instanceof PhabricatorSubscribableInterface) { + if (isset($types[$subscribers_type])) { + if ($object_phid) { + $sub_phids = PhabricatorSubscribersQuery::loadSubscribersForPHID( + $object_phid); + } else { + // TODO: Allow applications to provide default subscribers; Maniphest + // does this at a minimum. + $sub_phids = array(); + } + + $subscribers_field = id(new PhabricatorDatasourceEditField()) + ->setKey('subscriberPHIDs') + ->setLabel(pht('Subscribers')) + ->setDatasource(new PhabricatorMetaMTAMailableDatasource()) + ->setAliases(array('subscriber', 'subscribers')) + ->setTransactionType($subscribers_type) + ->setValue($sub_phids); + $fields[] = $subscribers_field; + } + } + + return $fields; + } + + abstract protected function newEditableObject(); + abstract protected function newObjectQuery(); + abstract protected function buildCustomEditFields($object); + + abstract protected function getObjectCreateTitleText($object); + abstract protected function getObjectEditTitleText($object); + abstract protected function getObjectCreateShortText($object); + abstract protected function getObjectEditShortText($object); + abstract protected function getObjectViewURI($object); + + protected function getObjectCreateCancelURI($object) { + return $this->getController()->getApplicationURI(); + } + + protected function getObjectEditCancelURI($object) { + return $this->getObjectViewURI($object); + } + + protected function getObjectCreateButtonText($object) { + return $this->getObjectCreateTitleText($object); + } + + protected function getObjectEditButtonText($object) { + return pht('Save Changes'); + } + + final public function buildResponse() { + $controller = $this->getController(); + $viewer = $this->getViewer(); + $request = $controller->getRequest(); + + $id = $request->getURIData('id'); + if ($id) { + $object = $this->newObjectQuery() + ->setViewer($viewer) + ->withIDs(array($id)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$object) { + return new Aphront404Response(); + } + + $is_create = false; + } else { + $object = $this->newEditableObject(); + + $is_create = true; + } + + $fields = $this->buildEditFields($object); + + foreach ($fields as $field) { + $field + ->setViewer($viewer) + ->setObject($object); + } + + $validation_exception = null; + if ($request->isFormPost()) { + foreach ($fields as $field) { + $field->readValueFromSubmit($request); + } + + $template = $object->getApplicationTransactionTemplate(); + + $xactions = array(); + foreach ($fields as $field) { + $xactions[] = $field->generateTransaction(clone $template); + } + + $editor = $object->getApplicationTransactionEditor() + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(false); + + try { + + $editor->applyTransactions($object, $xactions); + + return id(new AphrontRedirectResponse()) + ->setURI($this->getObjectViewURI($object)); + } catch (PhabricatorApplicationTransactionValidationException $ex) { + $validation_exception = $ex; + } + } else { + if ($is_create) { + foreach ($fields as $field) { + $field->readValueFromRequest($request); + } + } else { + foreach ($fields as $field) { + $field->readValueFromObject($object); + } + } + } + + $box = id(new PHUIObjectBoxView()) + ->setUser($viewer); + + $crumbs = $controller->buildApplicationCrumbsForEditEngine(); + + if ($is_create) { + $header_text = $this->getObjectCreateTitleText($object); + + $crumbs->addTextCrumb( + $this->getObjectCreateShortText($object)); + + $cancel_uri = $this->getObjectCreateCancelURI($object); + $submit_button = $this->getObjectCreateButtonText($object); + } else { + $header_text = $this->getObjectEditTitleText($object); + + $crumbs->addTextCrumb( + $this->getObjectEditShortText($object), + $this->getObjectViewURI($object)); + + $cancel_uri = $this->getObjectEditCancelURI($object); + $submit_button = $this->getObjectEditButtonText($object); + } + + $box->setHeaderText($header_text); + + $form = id(new AphrontFormView()) + ->setUser($viewer); + + foreach ($fields as $field) { + $field->appendToForm($form); + } + + $form->appendControl( + id(new AphrontFormSubmitControl()) + ->addCancelButton($cancel_uri) + ->setValue($submit_button)); + + $box->appendChild($form); + + if ($validation_exception) { + $box->setValidationException($validation_exception); + } + + return $controller->newPage() + ->setTitle($header_text) + ->setCrumbs($crumbs) + ->appendChild($box); + } + + +} diff --git a/src/applications/transactions/editfield/PhabricatorDatasourceEditField.php b/src/applications/transactions/editfield/PhabricatorDatasourceEditField.php new file mode 100644 index 0000000000..f3cf5fe079 --- /dev/null +++ b/src/applications/transactions/editfield/PhabricatorDatasourceEditField.php @@ -0,0 +1,21 @@ +datasource = $datasource; + return $this; + } + + public function getDatasource() { + return $this->datasource; + } + + protected function newDatasource() { + return id(clone $this->getDatasource()); + } + +} diff --git a/src/applications/transactions/editfield/PhabricatorEditField.php b/src/applications/transactions/editfield/PhabricatorEditField.php new file mode 100644 index 0000000000..127fa2e0a5 --- /dev/null +++ b/src/applications/transactions/editfield/PhabricatorEditField.php @@ -0,0 +1,212 @@ +key = $key; + return $this; + } + + public function getKey() { + return $this->key; + } + + public function setLabel($label) { + $this->label = $label; + return $this; + } + + public function getLabel() { + return $this->label; + } + + public function setViewer(PhabricatorUser $viewer) { + $this->viewer = $viewer; + return $this; + } + + public function getViewer() { + return $this->viewer; + } + + public function setAliases(array $aliases) { + $this->aliases = $aliases; + return $this; + } + + public function getAliases() { + return $this->aliases; + } + + public function setObject($object) { + $this->object = $object; + return $this; + } + + public function getObject() { + return $this->object; + } + + abstract protected function newControl(); + + protected function renderControl() { + $control = $this->newControl(); + if ($control === null) { + return null; + } + + $control + ->setValue($this->getValueForControl()) + ->setName($this->getKey()); + + if (!$control->getLabel()) { + $control->setLabel($this->getLabel()); + } + + return $control; + } + + public function appendToForm(AphrontFormView $form) { + $control = $this->renderControl(); + if ($control !== null) { + $form->appendControl($control); + } + return $this; + } + + protected function getValueForControl() { + return $this->getValue(); + } + + protected function getValue() { + return $this->value; + } + + public function setValue($value) { + $this->hasValue = true; + $this->value = $value; + return $this; + } + + public function generateTransaction( + PhabricatorApplicationTransaction $xaction) { + + $xaction + ->setTransactionType($this->getTransactionType()) + ->setNewValue($this->getValueForTransaction()); + + foreach ($this->metadata as $key => $value) { + $xaction->setMetadataValue($key, $value); + } + + return $xaction; + } + + public function setMetadataValue($key, $value) { + $this->metadata[$key] = $value; + return $this; + } + + protected function getValueForTransaction() { + return $this->getValue(); + } + + public function getTransactionType() { + if (!$this->transactionType) { + throw new PhutilInvalidStateException('setTransactionType'); + } + return $this->transactionType; + } + + public function setTransactionType($type) { + $this->transactionType = $type; + return $this; + } + + public function readValueFromRequest(AphrontRequest $request) { + $check = array_merge(array($this->getKey()), $this->getAliases()); + foreach ($check as $key) { + if (!$this->getValueExistsInRequest($request, $key)) { + continue; + } + + $this->value = $this->getValueFromRequest($request, $key); + return; + } + + $this->readValueFromObject($this->getObject()); + + return $this; + } + + public function readValueFromObject($object) { + $this->value = $this->getValueFromObject($object); + return $this; + } + + protected function getValueFromObject($object) { + if ($this->hasValue) { + return $this->value; + } else { + return $this->getDefaultValue(); + } + } + + protected function getValueExistsInRequest(AphrontRequest $request, $key) { + return $this->getValueExistsInSubmit($request, $key); + } + + protected function getValueFromRequest(AphrontRequest $request, $key) { + return $this->getValueFromSubmit($request, $key); + } + + public function readValueFromSubmit(AphrontRequest $request) { + $key = $this->getKey(); + if ($this->getValueExistsInSubmit($request, $key)) { + $value = $this->getValueFromSubmit($request, $key); + } else { + $value = $this->getDefaultValue(); + } + $this->value = $value; + return $this; + } + + protected function getValueExistsInSubmit(AphrontRequest $request, $key) { + return $request->getExists($key); + } + + protected function getValueFromSubmit(AphrontRequest $request, $key) { + return $request->getStr($key); + } + + protected function getDefaultValue() { + return null; + } + + protected function getListFromRequest( + AphrontRequest $request, + $key) { + + $list = $request->getArr($key, null); + if ($list === null) { + $list = $request->getStrList($key); + } + + if (!$list) { + return array(); + } + + return $list; + } + +} diff --git a/src/applications/transactions/editfield/PhabricatorPolicyEditField.php b/src/applications/transactions/editfield/PhabricatorPolicyEditField.php new file mode 100644 index 0000000000..26c606e307 --- /dev/null +++ b/src/applications/transactions/editfield/PhabricatorPolicyEditField.php @@ -0,0 +1,54 @@ +policies = $policies; + return $this; + } + + public function getPolicies() { + if ($this->policies === null) { + throw new PhutilInvalidStateException('setPolicies'); + } + return $this->policies; + } + + public function setCapability($capability) { + $this->capability = $capability; + return $this; + } + + public function getCapability() { + return $this->capability; + } + + public function setSpaceField(PhabricatorSpaceEditField $space_field) { + $this->spaceField = $space_field; + return $this; + } + + public function getSpaceField() { + return $this->spaceField; + } + + protected function newControl() { + $control = id(new AphrontFormPolicyControl()) + ->setCapability($this->getCapability()) + ->setPolicyObject($this->getObject()) + ->setPolicies($this->getPolicies()); + + $space_field = $this->getSpaceField(); + if ($space_field) { + $control->setSpacePHID($space_field->getValueForControl()); + } + + return $control; + } + +} diff --git a/src/applications/transactions/editfield/PhabricatorSelectEditField.php b/src/applications/transactions/editfield/PhabricatorSelectEditField.php new file mode 100644 index 0000000000..496d95c7c4 --- /dev/null +++ b/src/applications/transactions/editfield/PhabricatorSelectEditField.php @@ -0,0 +1,25 @@ +options = $options; + return $this; + } + + public function getOptions() { + if ($this->options === null) { + throw new PhutilInvalidStateException('setOptions'); + } + return $this->options; + } + + protected function newControl() { + return id(new AphrontFormSelectControl()) + ->setOptions($this->getOptions()); + } + +} diff --git a/src/applications/transactions/editfield/PhabricatorSpaceEditField.php b/src/applications/transactions/editfield/PhabricatorSpaceEditField.php new file mode 100644 index 0000000000..d01350ee5f --- /dev/null +++ b/src/applications/transactions/editfield/PhabricatorSpaceEditField.php @@ -0,0 +1,12 @@ +monospaced = $monospaced; + return $this; + } + + public function getMonospaced() { + return $this->monospaced; + } + + public function setHeight($height) { + $this->height = $height; + return $this; + } + + public function getHeight() { + return $this->height; + } + + protected function newControl() { + $control = new AphrontFormTextAreaControl(); + + if ($this->getMonospaced()) { + $control->setCustomClass('PhabricatorMonospaced'); + } + + $height = $this->getHeight(); + if ($height) { + $control->setHeight($height); + } + + return $control; + } + +} diff --git a/src/applications/transactions/editfield/PhabricatorTextEditField.php b/src/applications/transactions/editfield/PhabricatorTextEditField.php new file mode 100644 index 0000000000..25659d5710 --- /dev/null +++ b/src/applications/transactions/editfield/PhabricatorTextEditField.php @@ -0,0 +1,10 @@ +setDatasource($this->newDatasource()); + + if ($this->originalValue !== null) { + $control->setOriginalValue($this->originalValue); + } + + return $control; + } + + public function setOriginalValue(array $value) { + $this->originalValue = $value; + return $this; + } + + public function setValue($value) { + $this->originalValue = $value; + return parent::setValue($value); + } + + protected function getValueFromSubmit(AphrontRequest $request, $key) { + // TODO: Maybe move this unusual read somewhere else so subclassing this + // correctly is easier? + $this->originalValue = $request->getArr($key.'.original'); + + return $this->getListFromRequest($request, $key); + } + + protected function getDefaultValue() { + return array(); + } + + protected function getValueForTransaction() { + $new = parent::getValueForTransaction(); + + $edge_types = array( + PhabricatorTransactions::TYPE_EDGE => true, + PhabricatorTransactions::TYPE_SUBSCRIBERS => true, + ); + + if (isset($edge_types[$this->getTransactionType()])) { + if ($this->originalValue !== null) { + // If we're building an edge transaction and the request has data + // about the original value the user saw when they loaded the form, + // interpret the edit as a mixture of "+" and "-" operations instead + // of a single "=" operation. This limits our exposure to race + // conditions by making most concurrent edits merge correctly. + + $new = parent::getValueForTransaction(); + $old = $this->originalValue; + + $add = array_diff($new, $old); + $rem = array_diff($old, $new); + + $value = array(); + + if ($add) { + $value['+'] = array_fuse($add); + } + if ($rem) { + $value['-'] = array_fuse($rem); + } + + return $value; + } else { + + if (!is_array($new)) { + throw new Exception(print_r($new, true)); + } + + return array( + '=' => array_fuse($new), + ); + } + } + + return $new; + } + +} diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php index 2efd19a000..ec5c227ffa 100644 --- a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php +++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php @@ -242,6 +242,19 @@ abstract class PhabricatorApplicationTransactionEditor return $this->applicationEmail; } + public function getTransactionTypesForObject($object) { + $old = $this->object; + try { + $this->object = $object; + $result = $this->getTransactionTypes(); + $this->object = $old; + } catch (Exception $ex) { + $this->object = $old; + throw $ex; + } + return $result; + } + public function getTransactionTypes() { $types = array(); @@ -1650,7 +1663,7 @@ abstract class PhabricatorApplicationTransactionEditor throw new Exception( pht( "Invalid '%s' value for PHID transaction. Value should contain only ". - "keys '%s' (add PHIDs), '%' (remove PHIDs) and '%s' (set PHIDS).", + "keys '%s' (add PHIDs), '%s' (remove PHIDs) and '%s' (set PHIDS).", 'new', '+', '-', diff --git a/src/view/control/AphrontTokenizerTemplateView.php b/src/view/control/AphrontTokenizerTemplateView.php index fd4893dd4d..b8c2c6f2cc 100644 --- a/src/view/control/AphrontTokenizerTemplateView.php +++ b/src/view/control/AphrontTokenizerTemplateView.php @@ -6,6 +6,7 @@ final class AphrontTokenizerTemplateView extends AphrontView { private $name; private $id; private $browseURI; + private $originalValue; public function setBrowseURI($browse_uri) { $this->browseURI = $browse_uri; @@ -36,6 +37,15 @@ final class AphrontTokenizerTemplateView extends AphrontView { return $this->name; } + public function setOriginalValue(array $original_value) { + $this->originalValue = $original_value; + return $this; + } + + public function getOriginalValue() { + return $this->originalValue; + } + public function render() { require_celerity_resource('aphront-tokenizer-control-css'); @@ -85,6 +95,20 @@ final class AphrontTokenizerTemplateView extends AphrontView { $classes[] = 'has-browse'; } + $original = array(); + $original_value = $this->getOriginalValue(); + if ($original_value) { + foreach ($this->getOriginalValue() as $value) { + $original[] = phutil_tag( + 'input', + array( + 'type' => 'hidden', + 'name' => $name.'.original[]', + 'value' => $value, + )); + } + } + $frame = javelin_tag( 'div', array( @@ -94,6 +118,7 @@ final class AphrontTokenizerTemplateView extends AphrontView { array( $container, $browse, + $original, )); return $frame; diff --git a/src/view/form/control/AphrontFormTokenizerControl.php b/src/view/form/control/AphrontFormTokenizerControl.php index fd25f66a1f..60c8962e78 100644 --- a/src/view/form/control/AphrontFormTokenizerControl.php +++ b/src/view/form/control/AphrontFormTokenizerControl.php @@ -7,6 +7,7 @@ final class AphrontFormTokenizerControl extends AphrontFormControl { private $limit; private $placeholder; private $handles; + private $originalValue; public function setDatasource(PhabricatorTypeaheadDatasource $datasource) { $this->datasource = $datasource; @@ -32,6 +33,15 @@ final class AphrontFormTokenizerControl extends AphrontFormControl { return $this; } + public function setOriginalValue(array $original_value) { + $this->originalValue = $original_value; + return $this; + } + + public function getOriginalValue() { + return $this->originalValue; + } + public function willRender() { // Load the handles now so we'll get a bulk load later on when we actually // render them. @@ -69,10 +79,15 @@ final class AphrontFormTokenizerControl extends AphrontFormControl { $token->setInputName($this->getName()); } - $template = new AphrontTokenizerTemplateView(); - $template->setName($name); - $template->setID($id); - $template->setValue($tokens); + $template = id(new AphrontTokenizerTemplateView()) + ->setName($name) + ->setID($id) + ->setValue($tokens); + + $original_value = $this->getOriginalValue(); + if ($original_value !== null) { + $template->setOriginalValue($original_value); + } $username = null; if ($this->user) { -- 2.51.2