Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/webroot/rsrc/js/application/projects/WorkboardBoard.js
12242 views
1
/**
2
* @provides javelin-workboard-board
3
* @requires javelin-install
4
* javelin-dom
5
* javelin-util
6
* javelin-stratcom
7
* javelin-workflow
8
* phabricator-draggable-list
9
* javelin-workboard-column
10
* javelin-workboard-header-template
11
* javelin-workboard-card-template
12
* javelin-workboard-order-template
13
* @javelin
14
*/
15
16
JX.install('WorkboardBoard', {
17
18
construct: function(controller, phid, root) {
19
this._controller = controller;
20
this._phid = phid;
21
this._root = root;
22
23
this._headers = {};
24
this._cards = {};
25
this._orders = {};
26
27
this._buildColumns();
28
},
29
30
properties: {
31
order: null,
32
pointsEnabled: false
33
},
34
35
members: {
36
_controller: null,
37
_phid: null,
38
_root: null,
39
_columns: null,
40
_headers: null,
41
_cards: null,
42
_dropPreviewNode: null,
43
_dropPreviewListNode: null,
44
_previewPHID: null,
45
_hidePreivew: false,
46
_previewPositionVector: null,
47
_previewDimState: false,
48
49
getRoot: function() {
50
return this._root;
51
},
52
53
getColumns: function() {
54
return this._columns;
55
},
56
57
getColumn: function(k) {
58
return this._columns[k];
59
},
60
61
getPHID: function() {
62
return this._phid;
63
},
64
65
getCardTemplate: function(phid) {
66
if (!this._cards[phid]) {
67
this._cards[phid] = new JX.WorkboardCardTemplate(phid);
68
}
69
70
return this._cards[phid];
71
},
72
73
getHeaderTemplate: function(header_key) {
74
if (!this._headers[header_key]) {
75
this._headers[header_key] = new JX.WorkboardHeaderTemplate(header_key);
76
}
77
78
return this._headers[header_key];
79
},
80
81
getOrderTemplate: function(order_key) {
82
if (!this._orders[order_key]) {
83
this._orders[order_key] = new JX.WorkboardOrderTemplate(order_key);
84
}
85
86
return this._orders[order_key];
87
},
88
89
getHeaderTemplatesForOrder: function(order) {
90
var templates = [];
91
92
for (var k in this._headers) {
93
var header = this._headers[k];
94
95
if (header.getOrder() !== order) {
96
continue;
97
}
98
99
templates.push(header);
100
}
101
102
templates.sort(JX.bind(this, this._sortHeaderTemplates));
103
104
return templates;
105
},
106
107
_sortHeaderTemplates: function(u, v) {
108
return this.compareVectors(u.getVector(), v.getVector());
109
},
110
111
getController: function() {
112
return this._controller;
113
},
114
115
compareVectors: function(u_vec, v_vec) {
116
for (var ii = 0; ii < u_vec.length; ii++) {
117
if (u_vec[ii] > v_vec[ii]) {
118
return 1;
119
}
120
121
if (u_vec[ii] < v_vec[ii]) {
122
return -1;
123
}
124
}
125
126
return 0;
127
},
128
129
start: function() {
130
this._setupDragHandlers();
131
132
// TODO: This is temporary code to make it easier to debug this workflow
133
// by pressing the "R" key.
134
var on_reload = JX.bind(this, this._reloadCards);
135
new JX.KeyboardShortcut('R', 'Reload Card State (Prototype)')
136
.setHandler(on_reload)
137
.register();
138
139
var board_phid = this.getPHID();
140
141
JX.Stratcom.listen('aphlict-server-message', null, function(e) {
142
var message = e.getData();
143
144
if (message.type != 'workboards') {
145
return;
146
}
147
148
// Check if this update notification is about the currently visible
149
// board. If it is, update the board state.
150
151
var found_board = false;
152
for (var ii = 0; ii < message.subscribers.length; ii++) {
153
var subscriber_phid = message.subscribers[ii];
154
if (subscriber_phid === board_phid) {
155
found_board = true;
156
break;
157
}
158
}
159
160
if (found_board) {
161
on_reload();
162
}
163
});
164
165
JX.Stratcom.listen('aphlict-reconnect', null, function(e) {
166
on_reload();
167
});
168
169
for (var k in this._columns) {
170
this._columns[k].redraw();
171
}
172
},
173
174
_buildColumns: function() {
175
var nodes = JX.DOM.scry(this.getRoot(), 'ul', 'project-column');
176
177
this._columns = {};
178
for (var ii = 0; ii < nodes.length; ii++) {
179
var node = nodes[ii];
180
var data = JX.Stratcom.getData(node);
181
var phid = data.columnPHID;
182
183
this._columns[phid] = new JX.WorkboardColumn(this, phid, node);
184
}
185
186
var on_over = JX.bind(this, this._showTriggerPreview);
187
var on_out = JX.bind(this, this._hideTriggerPreview);
188
JX.Stratcom.listen('mouseover', 'trigger-preview', on_over);
189
JX.Stratcom.listen('mouseout', 'trigger-preview', on_out);
190
191
var on_move = JX.bind(this, this._dimPreview);
192
JX.Stratcom.listen('mousemove', null, on_move);
193
},
194
195
_dimPreview: function(e) {
196
var p = this._previewPositionVector;
197
if (!p) {
198
return;
199
}
200
201
// When the mouse cursor gets near the drop preview element, fade it
202
// out so you can see through it. We can't do this with ":hover" because
203
// we disable cursor events.
204
205
var cursor = JX.$V(e);
206
var margin = 64;
207
208
var near_x = (cursor.x > (p.x - margin));
209
var near_y = (cursor.y > (p.y - margin));
210
var should_dim = (near_x && near_y);
211
212
this._setPreviewDimState(should_dim);
213
},
214
215
_setPreviewDimState: function(is_dim) {
216
if (is_dim === this._previewDimState) {
217
return;
218
}
219
220
this._previewDimState = is_dim;
221
var node = this._getDropPreviewNode();
222
JX.DOM.alterClass(node, 'workboard-drop-preview-fade', is_dim);
223
},
224
225
_showTriggerPreview: function(e) {
226
if (this._disablePreview) {
227
return;
228
}
229
230
var target = e.getTarget();
231
var node = e.getNode('trigger-preview');
232
233
if (target !== node) {
234
return;
235
}
236
237
var phid = JX.Stratcom.getData(node).columnPHID;
238
var column = this._columns[phid];
239
240
// Bail out if we don't know anything about this column.
241
if (!column) {
242
return;
243
}
244
245
if (phid === this._previewPHID) {
246
return;
247
}
248
249
this._previewPHID = phid;
250
251
var effects = column.getDropEffects();
252
253
var triggers = [];
254
for (var ii = 0; ii < effects.length; ii++) {
255
if (effects[ii].getIsTriggerEffect()) {
256
triggers.push(effects[ii]);
257
}
258
}
259
260
if (triggers.length) {
261
var header = column.getTriggerPreviewEffect();
262
triggers = [header].concat(triggers);
263
}
264
265
this._showEffects(triggers);
266
},
267
268
_hideTriggerPreview: function(e) {
269
if (this._disablePreview) {
270
return;
271
}
272
273
var target = e.getTarget();
274
275
if (target !== e.getNode('trigger-preview')) {
276
return;
277
}
278
279
this._removeTriggerPreview();
280
},
281
282
_removeTriggerPreview: function() {
283
this._showEffects([]);
284
this._previewPHID = null;
285
},
286
287
_beginDrag: function() {
288
this._disablePreview = true;
289
this._showEffects([]);
290
},
291
292
_endDrag: function() {
293
this._disablePreview = false;
294
},
295
296
_setupDragHandlers: function() {
297
var columns = this.getColumns();
298
299
var order_template = this.getOrderTemplate(this.getOrder());
300
var has_headers = order_template.getHasHeaders();
301
var can_reorder = order_template.getCanReorder();
302
303
var lists = [];
304
for (var k in columns) {
305
var column = columns[k];
306
307
var list = new JX.DraggableList('draggable-card', column.getRoot())
308
.setOuterContainer(this.getRoot())
309
.setFindItemsHandler(JX.bind(column, column.getDropTargetNodes))
310
.setCanDragX(true)
311
.setHasInfiniteHeight(true)
312
.setIsDropTargetHandler(JX.bind(column, column.setIsDropTarget));
313
314
var default_handler = list.getGhostHandler();
315
list.setGhostHandler(
316
JX.bind(column, column.handleDragGhost, default_handler));
317
318
// The "compare handler" locks cards into a specific position in the
319
// column.
320
list.setCompareHandler(JX.bind(column, column.compareHandler));
321
322
// If the view has group headers, we lock cards into the right position
323
// when moving them between columns, but not within a column.
324
if (has_headers) {
325
list.setCompareOnMove(true);
326
}
327
328
// If we can't reorder cards, we always lock them into their current
329
// position.
330
if (!can_reorder) {
331
list.setCompareOnMove(true);
332
list.setCompareOnReorder(true);
333
}
334
335
list.setTargetChangeHandler(JX.bind(this, this._didChangeDropTarget));
336
337
list.listen('didDrop', JX.bind(this, this._onmovecard, list));
338
339
list.listen('didBeginDrag', JX.bind(this, this._beginDrag));
340
list.listen('didEndDrag', JX.bind(this, this._endDrag));
341
342
lists.push(list);
343
}
344
345
for (var ii = 0; ii < lists.length; ii++) {
346
lists[ii].setGroup(lists);
347
}
348
},
349
350
_didChangeDropTarget: function(src_list, src_node, dst_list, dst_node) {
351
if (!dst_list) {
352
// The card is being dragged into a dead area, like the left menu.
353
this._showEffects([]);
354
return;
355
}
356
357
if (dst_node === false) {
358
// The card is being dragged over itself, so dropping it won't
359
// affect anything.
360
this._showEffects([]);
361
return;
362
}
363
364
var src_phid = JX.Stratcom.getData(src_list.getRootNode()).columnPHID;
365
var dst_phid = JX.Stratcom.getData(dst_list.getRootNode()).columnPHID;
366
367
var src_column = this.getColumn(src_phid);
368
var dst_column = this.getColumn(dst_phid);
369
370
var effects = [];
371
if (src_column !== dst_column) {
372
effects = effects.concat(dst_column.getDropEffects());
373
}
374
375
var context = this._getDropContext(dst_node);
376
if (context.headerKey) {
377
var header = this.getHeaderTemplate(context.headerKey);
378
effects = effects.concat(header.getDropEffects());
379
}
380
381
var card_phid = JX.Stratcom.getData(src_node).objectPHID;
382
var card = src_column.getCard(card_phid);
383
384
var visible = [];
385
for (var ii = 0; ii < effects.length; ii++) {
386
if (effects[ii].isEffectVisibleForCard(card)) {
387
visible.push(effects[ii]);
388
}
389
}
390
effects = visible;
391
392
this._showEffects(effects);
393
},
394
395
_showEffects: function(effects) {
396
var node = this._getDropPreviewNode();
397
398
if (!effects.length) {
399
JX.DOM.remove(node);
400
this._previewPositionVector = null;
401
return;
402
}
403
404
var items = [];
405
for (var ii = 0; ii < effects.length; ii++) {
406
var effect = effects[ii];
407
items.push(effect.newNode());
408
}
409
410
JX.DOM.setContent(this._getDropPreviewListNode(), items);
411
document.body.appendChild(node);
412
413
// Undim the drop preview element if it was previously dimmed.
414
this._setPreviewDimState(false);
415
this._previewPositionVector = JX.$V(node);
416
},
417
418
_getDropPreviewNode: function() {
419
if (!this._dropPreviewNode) {
420
var attributes = {
421
className: 'workboard-drop-preview'
422
};
423
424
var content = [
425
this._getDropPreviewListNode()
426
];
427
428
this._dropPreviewNode = JX.$N('div', attributes, content);
429
}
430
431
return this._dropPreviewNode;
432
},
433
434
_getDropPreviewListNode: function() {
435
if (!this._dropPreviewListNode) {
436
var attributes = {};
437
this._dropPreviewListNode = JX.$N('ul', attributes);
438
}
439
440
return this._dropPreviewListNode;
441
},
442
443
_findCardsInColumn: function(column_node) {
444
return JX.DOM.scry(column_node, 'li', 'project-card');
445
},
446
447
_getDropContext: function(after_node, item) {
448
var header_key;
449
var after_phids = [];
450
var before_phids = [];
451
452
// We're going to send an "afterPHID" and a "beforePHID" if the card
453
// was dropped immediately adjacent to another card. If a card was
454
// dropped before or after a header, we don't send a PHID for the card
455
// on the other side of the header.
456
457
// If the view has headers, we always send the header the card was
458
// dropped under.
459
460
var after_data;
461
var after_card = after_node;
462
while (after_card) {
463
after_data = JX.Stratcom.getData(after_card);
464
465
if (after_data.headerKey) {
466
break;
467
}
468
469
if (after_data.objectPHID) {
470
after_phids.push(after_data.objectPHID);
471
}
472
473
after_card = after_card.previousSibling;
474
}
475
476
if (item) {
477
var before_data;
478
var before_card = item.nextSibling;
479
while (before_card) {
480
before_data = JX.Stratcom.getData(before_card);
481
482
if (before_data.headerKey) {
483
break;
484
}
485
486
if (before_data.objectPHID) {
487
before_phids.push(before_data.objectPHID);
488
}
489
490
before_card = before_card.nextSibling;
491
}
492
}
493
494
var header_data;
495
var header_node = after_node;
496
while (header_node) {
497
header_data = JX.Stratcom.getData(header_node);
498
if (header_data.headerKey) {
499
break;
500
}
501
header_node = header_node.previousSibling;
502
}
503
504
if (header_data) {
505
header_key = header_data.headerKey;
506
}
507
508
return {
509
headerKey: header_key,
510
afterPHIDs: after_phids,
511
beforePHIDs: before_phids
512
};
513
},
514
515
_onmovecard: function(list, item, after_node, src_list) {
516
list.lock();
517
JX.DOM.alterClass(item, 'drag-sending', true);
518
519
var src_phid = JX.Stratcom.getData(src_list.getRootNode()).columnPHID;
520
var dst_phid = JX.Stratcom.getData(list.getRootNode()).columnPHID;
521
522
var item_phid = JX.Stratcom.getData(item).objectPHID;
523
var data = {
524
objectPHID: item_phid,
525
columnPHID: dst_phid,
526
order: this.getOrder()
527
};
528
529
var context = this._getDropContext(after_node, item);
530
data.afterPHIDs = context.afterPHIDs.join(',');
531
data.beforePHIDs = context.beforePHIDs.join(',');
532
533
if (context.headerKey) {
534
var properties = this.getHeaderTemplate(context.headerKey)
535
.getEditProperties();
536
data.header = JX.JSON.stringify(properties);
537
}
538
539
var visible_phids = [];
540
var column = this.getColumn(dst_phid);
541
for (var object_phid in column.getCards()) {
542
visible_phids.push(object_phid);
543
}
544
545
data.visiblePHIDs = visible_phids.join(',');
546
547
// If the user cancels the workflow (for example, by hitting an MFA
548
// prompt that they click "Cancel" on), put the card back where it was
549
// and reset the UI state.
550
var on_revert = JX.bind(
551
this,
552
this._revertCard,
553
list,
554
item,
555
src_phid,
556
dst_phid);
557
558
var after_phid = null;
559
if (data.afterPHIDs.length) {
560
after_phid = data.afterPHIDs[0];
561
}
562
563
var onupdate = JX.bind(
564
this,
565
this._oncardupdate,
566
list,
567
src_phid,
568
dst_phid,
569
after_phid);
570
571
new JX.Workflow(this.getController().getMoveURI(), data)
572
.setHandler(onupdate)
573
.setCloseHandler(on_revert)
574
.start();
575
},
576
577
_revertCard: function(list, item, src_phid, dst_phid) {
578
JX.DOM.alterClass(item, 'drag-sending', false);
579
580
var src_column = this.getColumn(src_phid);
581
var dst_column = this.getColumn(dst_phid);
582
583
src_column.markForRedraw();
584
dst_column.markForRedraw();
585
this._redrawColumns();
586
587
list.unlock();
588
},
589
590
_oncardupdate: function(list, src_phid, dst_phid, after_phid, response) {
591
this.updateCard(response);
592
593
var sounds = response.sounds || [];
594
for (var ii = 0; ii < sounds.length; ii++) {
595
JX.Sound.queue(sounds[ii]);
596
}
597
598
list.unlock();
599
},
600
601
updateCard: function(response) {
602
var columns = this.getColumns();
603
var column_phid;
604
var card_phid;
605
var card_data;
606
607
// The server may send us a full or partial update for a card. If we've
608
// received a full update, we're going to redraw the entire card and may
609
// need to change which columns it appears in.
610
611
// For a partial update, we've just received supplemental sorting or
612
// property information and do not need to perform a full redraw.
613
614
// When we reload card state, edit a card, or move a card, we get a full
615
// update for the card.
616
617
// Ween we move a card in a column, we may get a partial update for other
618
// visible cards in the column.
619
620
621
// Figure out which columns each card now appears in. For cards that
622
// have received a full update, we'll use this map to move them into
623
// the correct columns.
624
var update_map = {};
625
for (column_phid in response.columnMaps) {
626
var target_column = this.getColumn(column_phid);
627
628
if (!target_column) {
629
// If the column isn't visible, don't try to add a card to it.
630
continue;
631
}
632
633
var column_map = response.columnMaps[column_phid];
634
635
for (var ii = 0; ii < column_map.length; ii++) {
636
card_phid = column_map[ii];
637
if (!update_map[card_phid]) {
638
update_map[card_phid] = {};
639
}
640
update_map[card_phid][column_phid] = true;
641
}
642
}
643
644
// Process card removals. These are cases where the client still sees
645
// a particular card on a board but it has been removed on the server.
646
for (card_phid in response.cards) {
647
card_data = response.cards[card_phid];
648
649
if (!card_data.remove) {
650
continue;
651
}
652
653
for (column_phid in columns) {
654
var column = columns[column_phid];
655
656
var card = column.getCard(card_phid);
657
if (card) {
658
column.removeCard(card_phid);
659
column.markForRedraw();
660
}
661
}
662
}
663
664
// Process partial updates for cards. This is supplemental data which
665
// we can just merge in without any special handling.
666
for (card_phid in response.cards) {
667
card_data = response.cards[card_phid];
668
669
if (card_data.remove) {
670
continue;
671
}
672
673
var card_template = this.getCardTemplate(card_phid);
674
675
if (card_data.nodeHTMLTemplate) {
676
card_template.setNodeHTMLTemplate(card_data.nodeHTMLTemplate);
677
}
678
679
var order;
680
for (order in card_data.vectors) {
681
card_template.setSortVector(order, card_data.vectors[order]);
682
}
683
684
for (order in card_data.headers) {
685
card_template.setHeaderKey(order, card_data.headers[order]);
686
}
687
688
for (var key in card_data.properties) {
689
card_template.setObjectProperty(key, card_data.properties[key]);
690
}
691
}
692
693
// Process full updates for cards which we have a full update for. This
694
// may involve moving them between columns.
695
for (card_phid in response.cards) {
696
card_data = response.cards[card_phid];
697
698
if (!card_data.update) {
699
continue;
700
}
701
702
for (column_phid in columns) {
703
var column = columns[column_phid];
704
var card = column.getCard(card_phid);
705
706
if (card) {
707
card.redraw();
708
column.markForRedraw();
709
}
710
711
// Compare the server state to the client state, and add or remove
712
// cards on the client as necessary to synchronize them.
713
714
if (update_map[card_phid] && update_map[card_phid][column_phid]) {
715
if (!card) {
716
column.newCard(card_phid);
717
column.markForRedraw();
718
}
719
} else {
720
if (card) {
721
column.removeCard(card_phid);
722
column.markForRedraw();
723
}
724
}
725
}
726
}
727
728
var column_maps = response.columnMaps;
729
var natural_column;
730
for (var natural_phid in column_maps) {
731
natural_column = this.getColumn(natural_phid);
732
if (!natural_column) {
733
// Our view of the board may be out of date, so we might get back
734
// information about columns that aren't visible. Just ignore the
735
// position information for any columns we aren't displaying on the
736
// client.
737
continue;
738
}
739
740
natural_column.setNaturalOrder(column_maps[natural_phid]);
741
}
742
743
var headers = response.headers;
744
for (var jj = 0; jj < headers.length; jj++) {
745
var header = headers[jj];
746
747
this.getHeaderTemplate(header.key)
748
.setOrder(header.order)
749
.setNodeHTMLTemplate(header.template)
750
.setVector(header.vector)
751
.setEditProperties(header.editProperties);
752
}
753
754
this._redrawColumns();
755
},
756
757
_redrawColumns: function() {
758
var columns = this.getColumns();
759
for (var k in columns) {
760
if (columns[k].isMarkedForRedraw()) {
761
columns[k].redraw();
762
}
763
}
764
},
765
766
_reloadCards: function() {
767
var state = {};
768
769
var columns = this.getColumns();
770
for (var column_phid in columns) {
771
var cards = columns[column_phid].getCards();
772
for (var card_phid in cards) {
773
state[card_phid] = this.getCardTemplate(card_phid).getVersion();
774
}
775
}
776
777
var data = {
778
state: JX.JSON.stringify(state),
779
order: this.getOrder()
780
};
781
782
var on_reload = JX.bind(this, this._onReloadResponse);
783
784
new JX.Request(this.getController().getReloadURI(), on_reload)
785
.setData(data)
786
.send();
787
},
788
789
_onReloadResponse: function(response) {
790
this.updateCard(response);
791
}
792
793
}
794
795
});
796
797