@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<?php
2
3final class PhabricatorWorkboardViewState
4 extends Phobject {
5
6 private $viewer;
7 private $project;
8 private $requestState = array();
9 private $savedQuery;
10 private $searchEngine;
11 private $layoutEngine;
12 private $objects;
13
14 public function setProject(PhabricatorProject $project) {
15 $this->project = $project;
16 return $this;
17 }
18
19 public function getProject() {
20 return $this->project;
21 }
22
23 public function readFromRequest(AphrontRequest $request) {
24 if ($request->getExists('hidden')) {
25 $this->requestState['hidden'] = $request->getBool('hidden');
26 }
27
28 if ($request->getExists('order')) {
29 $this->requestState['order'] = $request->getStr('order');
30 }
31
32 // On some pathways, the search engine query key may be specified with
33 // either a "?filter=X" query parameter or with a "/query/X/" URI
34 // component. If both are present, the URI component is controlling.
35
36 // In particular, the "queryKey" URI parameter is used by
37 // "buildSavedQueryFromRequest()" when we are building custom board filters
38 // by invoking SearchEngine code.
39
40 if ($request->getExists('filter')) {
41 $this->requestState['filter'] = $request->getStr('filter');
42 }
43
44 if (phutil_nonempty_string($request->getURIData('queryKey'))) {
45 $this->requestState['filter'] = $request->getURIData('queryKey');
46 }
47
48 $this->viewer = $request->getViewer();
49
50 return $this;
51 }
52
53 public function getViewer() {
54 return $this->viewer;
55 }
56
57 public function getSavedQuery() {
58 if ($this->savedQuery === null) {
59 $this->savedQuery = $this->newSavedQuery();
60 }
61
62 return $this->savedQuery;
63 }
64
65 private function newSavedQuery() {
66 $search_engine = $this->getSearchEngine();
67 $query_key = $this->getQueryKey();
68 $viewer = $this->getViewer();
69
70 if ($search_engine->isBuiltinQuery($query_key)) {
71 $saved_query = $search_engine->buildSavedQueryFromBuiltin($query_key);
72 } else {
73 $saved_query = id(new PhabricatorSavedQueryQuery())
74 ->setViewer($viewer)
75 ->withQueryKeys(array($query_key))
76 ->executeOne();
77 }
78
79 return $saved_query;
80 }
81
82 public function getSearchEngine() {
83 if ($this->searchEngine === null) {
84 $this->searchEngine = $this->newSearchEngine();
85 }
86
87 return $this->searchEngine;
88 }
89
90 private function newSearchEngine() {
91 $viewer = $this->getViewer();
92
93 // TODO: This URI is not fully state-preserving, because "SearchEngine"
94 // does not preserve URI parameters when constructing some URIs at time of
95 // writing.
96 $board_uri = $this->getProject()->getWorkboardURI();
97
98 return id(new ManiphestTaskSearchEngine())
99 ->setViewer($viewer)
100 ->setBaseURI($board_uri)
101 ->setIsBoardView(true);
102 }
103
104 public function newWorkboardURI($path = null) {
105 $project = $this->getProject();
106 $uri = urisprintf('%s%s', $project->getWorkboardURI(), $path);
107 return $this->newURI($uri);
108 }
109
110 public function newURI($path) {
111 $project = $this->getProject();
112 $uri = new PhutilURI($path);
113
114 $request_order = $this->getOrder();
115 $default_order = $this->getDefaultOrder();
116 if ($request_order !== $default_order) {
117 $request_value = idx($this->requestState, 'order');
118 if ($request_value !== null) {
119 $uri->replaceQueryParam('order', $request_value);
120 } else {
121 $uri->removeQueryParam('order');
122 }
123 } else {
124 $uri->removeQueryParam('order');
125 }
126
127 $request_query = $this->getQueryKey();
128 $default_query = $this->getDefaultQueryKey();
129 if ($request_query !== $default_query) {
130 $request_value = idx($this->requestState, 'filter');
131 if ($request_value !== null) {
132 $uri->replaceQueryParam('filter', $request_value);
133 } else {
134 $uri->removeQueryParam('filter');
135 }
136 } else {
137 $uri->removeQueryParam('filter');
138 }
139
140 if ($this->getShowHidden()) {
141 $uri->replaceQueryParam('hidden', 'true');
142 } else {
143 $uri->removeQueryParam('hidden');
144 }
145
146 return $uri;
147 }
148
149 public function getShowHidden() {
150 $request_show = idx($this->requestState, 'hidden');
151
152 if ($request_show !== null) {
153 return $request_show;
154 }
155
156 return false;
157 }
158
159 public function getOrder() {
160 $request_order = idx($this->requestState, 'order');
161 if ($request_order !== null) {
162 if ($this->isValidOrder($request_order)) {
163 return $request_order;
164 }
165 }
166
167 return $this->getDefaultOrder();
168 }
169
170 public function getQueryKey() {
171 $request_query = idx($this->requestState, 'filter');
172 if (phutil_nonempty_string($request_query)) {
173 return $request_query;
174 }
175
176 return $this->getDefaultQueryKey();
177 }
178
179 public function setQueryKey($query_key) {
180 $this->requestState['filter'] = $query_key;
181 return $this;
182 }
183
184 private function isValidOrder($order) {
185 $map = PhabricatorProjectColumnOrder::getEnabledOrders();
186 return isset($map[$order]);
187 }
188
189 private function getDefaultOrder() {
190 $project = $this->getProject();
191
192 $default_order = $project->getDefaultWorkboardSort();
193
194 if ($this->isValidOrder($default_order)) {
195 return $default_order;
196 }
197
198 return PhabricatorProjectColumnNaturalOrder::ORDERKEY;
199 }
200
201 private function getDefaultQueryKey() {
202 $project = $this->getProject();
203
204 $default_query = $project->getDefaultWorkboardFilter();
205
206 if (phutil_nonempty_string($default_query)) {
207 return $default_query;
208 }
209
210 return 'open';
211 }
212
213 public function getQueryParameters() {
214 return $this->requestState;
215 }
216
217 public function getLayoutEngine() {
218 if ($this->layoutEngine === null) {
219 $this->layoutEngine = $this->newLayoutEngine();
220 }
221 return $this->layoutEngine;
222 }
223
224 private function newLayoutEngine() {
225 $project = $this->getProject();
226 $viewer = $this->getViewer();
227
228 $board_phid = $project->getPHID();
229 $objects = $this->getObjects();
230
231 // Regardless of display order, pass tasks to the layout engine in ID order
232 // so layout is consistent.
233 $objects = msort($objects, 'getID');
234
235 $layout_engine = id(new PhabricatorBoardLayoutEngine())
236 ->setViewer($viewer)
237 ->setObjectPHIDs(array_keys($objects))
238 ->setBoardPHIDs(array($board_phid))
239 ->setFetchAllBoards(true)
240 ->executeLayout();
241
242 return $layout_engine;
243 }
244
245 public function getBoardContainerPHIDs() {
246 $project = $this->getProject();
247 $viewer = $this->getViewer();
248
249 $container_phids = array($project->getPHID());
250 if ($project->getHasSubprojects() || $project->getHasMilestones()) {
251 $descendants = id(new PhabricatorProjectQuery())
252 ->setViewer($viewer)
253 ->withAncestorProjectPHIDs($container_phids)
254 ->execute();
255 foreach ($descendants as $descendant) {
256 $container_phids[] = $descendant->getPHID();
257 }
258 }
259
260 return $container_phids;
261 }
262
263 public function getObjects() {
264 if ($this->objects === null) {
265 $this->objects = $this->newObjects();
266 }
267
268 return $this->objects;
269 }
270
271 private function newObjects() {
272 $viewer = $this->getViewer();
273 $saved_query = $this->getSavedQuery();
274 $search_engine = $this->getSearchEngine();
275
276 $container_phids = $this->getBoardContainerPHIDs();
277
278 $task_query = $search_engine->buildQueryFromSavedQuery($saved_query)
279 ->setViewer($viewer)
280 ->withEdgeLogicPHIDs(
281 PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
282 PhabricatorQueryConstraint::OPERATOR_ANCESTOR,
283 array($container_phids));
284
285 $tasks = $task_query->execute();
286 $tasks = mpull($tasks, null, 'getPHID');
287
288 return $tasks;
289 }
290
291}