@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Implement versioned drafts in EditEngine comment forms

Summary:
Ref T9132. Fixes T5031. This approximately implements the plan described in T5031#67988:

When we recieve a preview request, don't write a draft if the form is from a version of the object before the last update the viewer made.

This should fix the race-related (?) zombie draft comments that sometimes show up.

I just added a new object for this stuff to make it easier to do stacked actions (or whatever we end up with) a little later, since I needed to do some schema adjustments anyway.

Test Plan:
- Typed some text.
- Reloaded page.
- Draft stayed there.
- Tried real hard to get it to ghost by submitting stuff in multiple windows and typing a lot and couldn't, although I didn't bother specifically narrowing down the race condition.

Reviewers: chad

Reviewed By: chad

Maniphest Tasks: T5031, T9132

Differential Revision: https://secure.phabricator.com/D14640

+193 -7
+10
resources/sql/autopatches/20151202.versioneddraft.1.sql
··· 1 + CREATE TABLE {$NAMESPACE}_draft.draft_versioneddraft ( 2 + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, 3 + objectPHID VARBINARY(64) NOT NULL, 4 + authorPHID VARBINARY(64) NOT NULL, 5 + version INT UNSIGNED NOT NULL, 6 + properties LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT}, 7 + dateCreated INT UNSIGNED NOT NULL, 8 + dateModified INT UNSIGNED NOT NULL, 9 + UNIQUE KEY `key_object` (objectPHID, authorPHID, version) 10 + ) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};
+2
src/__phutil_library_map__.php
··· 3226 3226 'PhabricatorUsersPolicyRule' => 'applications/people/policyrule/PhabricatorUsersPolicyRule.php', 3227 3227 'PhabricatorUsersSearchField' => 'applications/people/searchfield/PhabricatorUsersSearchField.php', 3228 3228 'PhabricatorVCSResponse' => 'applications/repository/response/PhabricatorVCSResponse.php', 3229 + 'PhabricatorVersionedDraft' => 'applications/draft/storage/PhabricatorVersionedDraft.php', 3229 3230 'PhabricatorVeryWowEnglishTranslation' => 'infrastructure/internationalization/translation/PhabricatorVeryWowEnglishTranslation.php', 3230 3231 'PhabricatorViewerDatasource' => 'applications/people/typeahead/PhabricatorViewerDatasource.php', 3231 3232 'PhabricatorWatcherHasObjectEdgeType' => 'applications/transactions/edges/PhabricatorWatcherHasObjectEdgeType.php', ··· 7536 7537 'PhabricatorUsersPolicyRule' => 'PhabricatorPolicyRule', 7537 7538 'PhabricatorUsersSearchField' => 'PhabricatorSearchTokenizerField', 7538 7539 'PhabricatorVCSResponse' => 'AphrontResponse', 7540 + 'PhabricatorVersionedDraft' => 'PhabricatorDraftDAO', 7539 7541 'PhabricatorVeryWowEnglishTranslation' => 'PhutilTranslation', 7540 7542 'PhabricatorViewerDatasource' => 'PhabricatorTypeaheadDatasource', 7541 7543 'PhabricatorWatcherHasObjectEdgeType' => 'PhabricatorEdgeType',
+83
src/applications/draft/storage/PhabricatorVersionedDraft.php
··· 1 + <?php 2 + 3 + final class PhabricatorVersionedDraft extends PhabricatorDraftDAO { 4 + 5 + const KEY_VERSION = 'draft.version'; 6 + 7 + protected $objectPHID; 8 + protected $authorPHID; 9 + protected $version; 10 + protected $properties = array(); 11 + 12 + protected function getConfiguration() { 13 + return array( 14 + self::CONFIG_SERIALIZATION => array( 15 + 'properties' => self::SERIALIZATION_JSON, 16 + ), 17 + self::CONFIG_COLUMN_SCHEMA => array( 18 + 'version' => 'uint32', 19 + ), 20 + self::CONFIG_KEY_SCHEMA => array( 21 + 'key_object' => array( 22 + 'columns' => array('objectPHID', 'authorPHID', 'version'), 23 + 'unique' => true, 24 + ), 25 + ), 26 + ) + parent::getConfiguration(); 27 + } 28 + 29 + public function setProperty($key, $value) { 30 + $this->properties[$key] = $value; 31 + return $this; 32 + } 33 + 34 + public function getProperty($key, $default = null) { 35 + return idx($this->properties, $key, $default); 36 + } 37 + 38 + public static function loadDraft( 39 + $object_phid, 40 + $viewer_phid) { 41 + 42 + return id(new PhabricatorVersionedDraft())->loadOneWhere( 43 + 'objectPHID = %s AND authorPHID = %s ORDER BY version DESC LIMIT 1', 44 + $object_phid, 45 + $viewer_phid); 46 + } 47 + 48 + public static function loadOrCreateDraft( 49 + $object_phid, 50 + $viewer_phid, 51 + $version) { 52 + 53 + $draft = self::loadDraft($object_phid, $viewer_phid); 54 + if ($draft) { 55 + return $draft; 56 + } 57 + 58 + return id(new PhabricatorVersionedDraft()) 59 + ->setObjectPHID($object_phid) 60 + ->setAuthorPHID($viewer_phid) 61 + ->setVersion($version) 62 + ->save(); 63 + } 64 + 65 + public static function purgeDrafts( 66 + $object_phid, 67 + $viewer_phid, 68 + $version) { 69 + 70 + $draft = new PhabricatorVersionedDraft(); 71 + $conn_w = $draft->establishConnection('w'); 72 + 73 + queryfx( 74 + $conn_w, 75 + 'DELETE FROM %T WHERE objectPHID = %s AND authorPHID = %s 76 + AND version <= %d', 77 + $draft->getTableName(), 78 + $object_phid, 79 + $viewer_phid, 80 + $version); 81 + } 82 + 83 + }
+67 -7
src/applications/transactions/editengine/PhabricatorEditEngine.php
··· 879 879 $header_text = $this->getCommentViewHeaderText($object); 880 880 $button_text = $this->getCommentViewButtonText($object); 881 881 882 - // TODO: Drafts. 883 - // $draft = PhabricatorDraft::newFromUserAndKey( 884 - // $viewer, 885 - // $object_phid); 886 - 887 882 $comment_uri = $this->getEditURI($object, 'comment/'); 888 883 889 - return id(new PhabricatorApplicationTransactionCommentView()) 884 + $view = id(new PhabricatorApplicationTransactionCommentView()) 890 885 ->setUser($viewer) 891 886 ->setObjectPHID($object_phid) 892 887 ->setHeaderText($header_text) 893 888 ->setAction($comment_uri) 894 889 ->setSubmitButtonName($button_text); 890 + 891 + $draft = PhabricatorVersionedDraft::loadDraft( 892 + $object_phid, 893 + $viewer->getPHID()); 894 + if ($draft) { 895 + $view->setVersionedDraft($draft); 896 + } 897 + 898 + $view->setCurrentVersion($this->loadDraftVersion($object)); 899 + 900 + return $view; 895 901 } 896 902 903 + protected function loadDraftVersion($object) { 904 + $viewer = $this->getViewer(); 905 + 906 + if (!$viewer->isLoggedIn()) { 907 + return null; 908 + } 909 + 910 + $template = $object->getApplicationTransactionTemplate(); 911 + $conn_r = $template->establishConnection('r'); 912 + 913 + // Find the most recent transaction the user has written. We'll use this 914 + // as a version number to make sure that out-of-date drafts get discarded. 915 + $result = queryfx_one( 916 + $conn_r, 917 + 'SELECT id AS version FROM %T 918 + WHERE objectPHID = %s AND authorPHID = %s 919 + ORDER BY id DESC LIMIT 1', 920 + $template->getTableName(), 921 + $object->getPHID(), 922 + $viewer->getPHID()); 923 + 924 + if ($result) { 925 + return (int)$result['version']; 926 + } else { 927 + return null; 928 + } 929 + } 930 + 931 + 897 932 /* -( Responding to HTTP Parameter Requests )------------------------------ */ 898 933 899 934 ··· 970 1005 $template = $object->getApplicationTransactionTemplate(); 971 1006 $comment_template = $template->getApplicationTransactionCommentObject(); 972 1007 1008 + $comment_text = $request->getStr('comment'); 1009 + 1010 + if ($is_preview) { 1011 + $version_key = PhabricatorVersionedDraft::KEY_VERSION; 1012 + $request_version = $request->getInt($version_key); 1013 + $current_version = $this->loadDraftVersion($object); 1014 + if ($request_version >= $current_version) { 1015 + $draft = PhabricatorVersionedDraft::loadOrCreateDraft( 1016 + $object->getPHID(), 1017 + $viewer->getPHID(), 1018 + $current_version); 1019 + 1020 + // TODO: This is just a proof of concept. 1021 + $draft->setProperty('temporary.comment', $comment_text); 1022 + $draft->save(); 1023 + } 1024 + } 1025 + 973 1026 $xactions = array(); 974 1027 975 1028 $xactions[] = id(clone $template) 976 1029 ->setTransactionType(PhabricatorTransactions::TYPE_COMMENT) 977 1030 ->attachComment( 978 1031 id(clone $comment_template) 979 - ->setContent($request->getStr('comment'))); 1032 + ->setContent($comment_text)); 980 1033 981 1034 $editor = $object->getApplicationTransactionEditor() 982 1035 ->setActor($viewer) ··· 990 1043 return id(new PhabricatorApplicationTransactionNoEffectResponse()) 991 1044 ->setCancelURI($view_uri) 992 1045 ->setException($ex); 1046 + } 1047 + 1048 + if (!$is_preview) { 1049 + PhabricatorVersionedDraft::purgeDrafts( 1050 + $object->getPHID(), 1051 + $viewer->getPHID(), 1052 + $this->loadDraftVersion($object)); 993 1053 } 994 1054 995 1055 if ($request->isAjax() && $is_preview) {
+31
src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php
··· 20 20 private $objectPHID; 21 21 private $headerText; 22 22 23 + private $currentVersion; 24 + private $versionedDraft; 25 + 23 26 public function setObjectPHID($object_phid) { 24 27 $this->objectPHID = $object_phid; 25 28 return $this; ··· 44 47 } 45 48 public function getRequestURI() { 46 49 return $this->requestURI; 50 + } 51 + 52 + public function setCurrentVersion($current_version) { 53 + $this->currentVersion = $current_version; 54 + return $this; 55 + } 56 + 57 + public function getCurrentVersion() { 58 + return $this->currentVersion; 59 + } 60 + 61 + public function setVersionedDraft( 62 + PhabricatorVersionedDraft $versioned_draft) { 63 + $this->versionedDraft = $versioned_draft; 64 + return $this; 65 + } 66 + 67 + public function getVersionedDraft() { 68 + return $this->versionedDraft; 47 69 } 48 70 49 71 public function setDraft(PhabricatorDraft $draft) { ··· 148 170 $draft_key = $this->getDraft()->getDraftKey(); 149 171 } 150 172 173 + $versioned_draft = $this->getVersionedDraft(); 174 + if ($versioned_draft) { 175 + $draft_comment = $versioned_draft->getProperty('temporary.comment', ''); 176 + } 177 + 151 178 if (!$this->getObjectPHID()) { 152 179 throw new PhutilInvalidStateException('setObjectPHID', 'render'); 153 180 } 181 + 182 + $version_key = PhabricatorVersionedDraft::KEY_VERSION; 183 + $version_value = $this->getCurrentVersion(); 154 184 155 185 return id(new AphrontFormView()) 156 186 ->setUser($this->getUser()) ··· 163 193 ->setAction($this->getAction()) 164 194 ->setID($this->getFormID()) 165 195 ->addHiddenInput('__draft__', $draft_key) 196 + ->addHiddenInput($version_key, $version_value) 166 197 ->appendChild( 167 198 id(new PhabricatorRemarkupControl()) 168 199 ->setID($this->getCommentID())