"Merge Duplicates" in Maniphest
Summary: Allow duplicate tasks to be selected and merged in Maniphest. I didn't create a separate transaction type for this because that implies a bunch of really complicated rules which I don't want to sort out right now (e.g., do we need to do cycle detection for merges? If so, what do we do when we detect a cycle?) since I think it's unnecessary to get right for the initial implementation (my Tasks merge implementation was similar to this and worked quite well) and if/when we eventually need the metadata to be available in a computer-readable form that need should inform the implementation. Plenty of room for improvement here, of course. Test Plan: Merged duplicate tasks, tried to perform invalid merge operations (e.g., merge a task into itself). Tested existing attach workflows (task -> revision, revision -> task). Reviewed By: aran Reviewers: tuomaspelkonen, jungejason, aran CC: anjali, aran Differential Revision: 459
This commit is contained in:
parent
0a749ad51b
commit
b49c5e9762
|
@ -63,7 +63,7 @@ celerity_register_resource_map(array(
|
||||||
),
|
),
|
||||||
'aphront-headsup-action-list-view-css' =>
|
'aphront-headsup-action-list-view-css' =>
|
||||||
array(
|
array(
|
||||||
'uri' => '/res/71783633/rsrc/css/aphront/headsup-action-list-view.css',
|
'uri' => '/res/b7c6bbc2/rsrc/css/aphront/headsup-action-list-view.css',
|
||||||
'type' => 'css',
|
'type' => 'css',
|
||||||
'requires' =>
|
'requires' =>
|
||||||
array(
|
array(
|
||||||
|
@ -626,7 +626,7 @@ celerity_register_resource_map(array(
|
||||||
),
|
),
|
||||||
'javelin-behavior-phabricator-object-selector' =>
|
'javelin-behavior-phabricator-object-selector' =>
|
||||||
array(
|
array(
|
||||||
'uri' => '/res/12d4d90d/rsrc/js/application/core/behavior-object-selector.js',
|
'uri' => '/res/34f9a11e/rsrc/js/application/core/behavior-object-selector.js',
|
||||||
'type' => 'js',
|
'type' => 'js',
|
||||||
'requires' =>
|
'requires' =>
|
||||||
array(
|
array(
|
||||||
|
@ -902,7 +902,7 @@ celerity_register_resource_map(array(
|
||||||
),
|
),
|
||||||
'maniphest-transaction-detail-css' =>
|
'maniphest-transaction-detail-css' =>
|
||||||
array(
|
array(
|
||||||
'uri' => '/res/7ee02b5e/rsrc/css/application/maniphest/transaction-detail.css',
|
'uri' => '/res/149fccab/rsrc/css/application/maniphest/transaction-detail.css',
|
||||||
'type' => 'css',
|
'type' => 'css',
|
||||||
'requires' =>
|
'requires' =>
|
||||||
array(
|
array(
|
||||||
|
@ -1038,7 +1038,7 @@ celerity_register_resource_map(array(
|
||||||
),
|
),
|
||||||
'phabricator-object-selector-css' =>
|
'phabricator-object-selector-css' =>
|
||||||
array(
|
array(
|
||||||
'uri' => '/res/ced4098a/rsrc/css/application/objectselector/object-selector.css',
|
'uri' => '/res/62a8fd92/rsrc/css/application/objectselector/object-selector.css',
|
||||||
'type' => 'css',
|
'type' => 'css',
|
||||||
'requires' =>
|
'requires' =>
|
||||||
array(
|
array(
|
||||||
|
|
|
@ -188,7 +188,7 @@ class AphrontDefaultApplicationConfiguration
|
||||||
'/search/' => array(
|
'/search/' => array(
|
||||||
'$' => 'PhabricatorSearchController',
|
'$' => 'PhabricatorSearchController',
|
||||||
'(?P<id>\d+)/$' => 'PhabricatorSearchController',
|
'(?P<id>\d+)/$' => 'PhabricatorSearchController',
|
||||||
'attach/(?P<phid>[^/]+)/(?P<type>\w+)/$'
|
'attach/(?P<phid>[^/]+)/(?P<type>\w+)/(?:(?P<action>\w+)/)?$'
|
||||||
=> 'PhabricatorSearchAttachController',
|
=> 'PhabricatorSearchAttachController',
|
||||||
'select/(?P<type>\w+)/$'
|
'select/(?P<type>\w+)/$'
|
||||||
=> 'PhabricatorSearchSelectController',
|
=> 'PhabricatorSearchSelectController',
|
||||||
|
|
|
@ -174,6 +174,13 @@ class ManiphestTaskDetailController extends ManiphestController {
|
||||||
require_celerity_resource('phabricator-object-selector-css');
|
require_celerity_resource('phabricator-object-selector-css');
|
||||||
require_celerity_resource('javelin-behavior-phabricator-object-selector');
|
require_celerity_resource('javelin-behavior-phabricator-object-selector');
|
||||||
|
|
||||||
|
$action = new AphrontHeadsupActionView();
|
||||||
|
$action->setName('Merge Duplicates');
|
||||||
|
$action->setURI('/search/attach/'.$task->getPHID().'/TASK/merge/');
|
||||||
|
$action->setWorkflow(true);
|
||||||
|
$action->setClass('action-merge');
|
||||||
|
$actions[] = $action;
|
||||||
|
|
||||||
$action = new AphrontHeadsupActionView();
|
$action = new AphrontHeadsupActionView();
|
||||||
$action->setName('Edit Differential Revisions');
|
$action->setName('Edit Differential Revisions');
|
||||||
$action->setURI('/search/attach/'.$task->getPHID().'/DREV/');
|
$action->setURI('/search/attach/'.$task->getPHID().'/DREV/');
|
||||||
|
|
|
@ -396,6 +396,10 @@ class ManiphestTransactionDetailView extends AphrontView {
|
||||||
$verb = 'Spited';
|
$verb = 'Spited';
|
||||||
$desc = 'closed this task out of spite';
|
$desc = 'closed this task out of spite';
|
||||||
$classes[] = 'spited';
|
$classes[] = 'spited';
|
||||||
|
} else if ($new == ManiphestTaskStatus::STATUS_CLOSED_DUPLICATE) {
|
||||||
|
$verb = 'Merged';
|
||||||
|
$desc = 'closed this task as a duplicate';
|
||||||
|
$classes[] = 'duplicate';
|
||||||
} else {
|
} else {
|
||||||
$verb = 'Closed';
|
$verb = 'Closed';
|
||||||
$full = idx(ManiphestTaskStatus::getTaskStatusMap(), $new, '???');
|
$full = idx(ManiphestTaskStatus::getTaskStatusMap(), $new, '???');
|
||||||
|
|
|
@ -20,10 +20,15 @@ class PhabricatorSearchAttachController extends PhabricatorSearchController {
|
||||||
|
|
||||||
private $phid;
|
private $phid;
|
||||||
private $type;
|
private $type;
|
||||||
|
private $action;
|
||||||
|
|
||||||
|
const ACTION_ATTACH = 'attach';
|
||||||
|
const ACTION_MERGE = 'merge';
|
||||||
|
|
||||||
public function willProcessRequest(array $data) {
|
public function willProcessRequest(array $data) {
|
||||||
$this->phid = $data['phid'];
|
$this->phid = $data['phid'];
|
||||||
$this->type = $data['type'];
|
$this->type = $data['type'];
|
||||||
|
$this->action = idx($data, 'action', self::ACTION_ATTACH);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function processRequest() {
|
public function processRequest() {
|
||||||
|
@ -39,7 +44,6 @@ class PhabricatorSearchAttachController extends PhabricatorSearchController {
|
||||||
$object_type = $handle->getType();
|
$object_type = $handle->getType();
|
||||||
$attach_type = $this->type;
|
$attach_type = $this->type;
|
||||||
|
|
||||||
|
|
||||||
// Load the object we're going to attach/detach stuff from. This is the
|
// Load the object we're going to attach/detach stuff from. This is the
|
||||||
// object that triggered the action, e.g. the revision you clicked
|
// object that triggered the action, e.g. the revision you clicked
|
||||||
// "Edit Maniphest Tasks" on.
|
// "Edit Maniphest Tasks" on.
|
||||||
|
@ -65,6 +69,17 @@ class PhabricatorSearchAttachController extends PhabricatorSearchController {
|
||||||
$phids = explode(';', $request->getStr('phids'));
|
$phids = explode(';', $request->getStr('phids'));
|
||||||
$phids = array_filter($phids);
|
$phids = array_filter($phids);
|
||||||
$phids = array_values($phids);
|
$phids = array_values($phids);
|
||||||
|
|
||||||
|
switch ($this->action) {
|
||||||
|
case self::ACTION_MERGE:
|
||||||
|
return $this->performMerge($object, $handle, $phids);
|
||||||
|
case self::ACTION_ATTACH:
|
||||||
|
// Fall through to the workflow below.
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Exception("Unsupported attach action.");
|
||||||
|
}
|
||||||
|
|
||||||
// sort() so that removing [X, Y] and then adding [Y, X] is correctly
|
// sort() so that removing [X, Y] and then adding [Y, X] is correctly
|
||||||
// detected as a no-op.
|
// detected as a no-op.
|
||||||
sort($phids);
|
sort($phids);
|
||||||
|
@ -139,10 +154,17 @@ class PhabricatorSearchAttachController extends PhabricatorSearchController {
|
||||||
|
|
||||||
return id(new AphrontReloadResponse())->setURI($handle->getURI());
|
return id(new AphrontReloadResponse())->setURI($handle->getURI());
|
||||||
} else {
|
} else {
|
||||||
$phids = $object->getAttachedPHIDs($attach_type);
|
switch ($this->action) {
|
||||||
|
case self::ACTION_ATTACH:
|
||||||
|
$phids = $object->getAttachedPHIDs($attach_type);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
$phids = array();
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch ($attach_type) {
|
switch ($this->type) {
|
||||||
case PhabricatorPHIDConstants::PHID_TYPE_DREV:
|
case PhabricatorPHIDConstants::PHID_TYPE_DREV:
|
||||||
$noun = 'Revisions';
|
$noun = 'Revisions';
|
||||||
$selected = 'created';
|
$selected = 'created';
|
||||||
|
@ -153,6 +175,24 @@ class PhabricatorSearchAttachController extends PhabricatorSearchController {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
switch ($this->action) {
|
||||||
|
case self::ACTION_ATTACH:
|
||||||
|
$dialog_title = "Manage Attached {$noun}";
|
||||||
|
$header_text = "Currently Attached {$noun}";
|
||||||
|
$button_text = "Save {$noun}";
|
||||||
|
$instructions = null;
|
||||||
|
break;
|
||||||
|
case self::ACTION_MERGE:
|
||||||
|
$dialog_title = "Merge Duplicate Tasks";
|
||||||
|
$header_text = "Tasks To Merge";
|
||||||
|
$button_text = "Merge {$noun}";
|
||||||
|
$instructions =
|
||||||
|
"These tasks will be merged into the current task and then closed. ".
|
||||||
|
"The current task (\"".phutil_escape_html($handle->getName())."\") ".
|
||||||
|
"will grow stronger.";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
$handles = id(new PhabricatorObjectHandleData($phids))
|
$handles = id(new PhabricatorObjectHandleData($phids))
|
||||||
->loadHandles();
|
->loadHandles();
|
||||||
|
|
||||||
|
@ -169,7 +209,10 @@ class PhabricatorSearchAttachController extends PhabricatorSearchController {
|
||||||
->setSelectedFilter($selected)
|
->setSelectedFilter($selected)
|
||||||
->setCancelURI($handle->getURI())
|
->setCancelURI($handle->getURI())
|
||||||
->setSearchURI('/search/select/'.$attach_type.'/')
|
->setSearchURI('/search/select/'.$attach_type.'/')
|
||||||
->setNoun($noun);
|
->setTitle($dialog_title)
|
||||||
|
->setHeader($header_text)
|
||||||
|
->setButtonText($button_text)
|
||||||
|
->setInstructions($instructions);
|
||||||
|
|
||||||
$dialog = $obj_dialog->buildDialog();
|
$dialog = $obj_dialog->buildDialog();
|
||||||
|
|
||||||
|
@ -196,4 +239,67 @@ class PhabricatorSearchAttachController extends PhabricatorSearchController {
|
||||||
$transaction->setNewValue($new);
|
$transaction->setNewValue($new);
|
||||||
$editor->applyTransactions($task, array($transaction));
|
$editor->applyTransactions($task, array($transaction));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function performMerge(
|
||||||
|
ManiphestTask $task,
|
||||||
|
PhabricatorObjectHandle $handle,
|
||||||
|
array $phids) {
|
||||||
|
|
||||||
|
$user = $this->getRequest()->getUser();
|
||||||
|
$response = id(new AphrontReloadResponse())->setURI($handle->getURI());
|
||||||
|
|
||||||
|
$phids = array_fill_keys($phids, true);
|
||||||
|
unset($phids[$task->getPHID()]); // Prevent merging a task into itself.
|
||||||
|
|
||||||
|
if (!$phids) {
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
$targets = id(new ManiphestTask())->loadAllWhere(
|
||||||
|
'phid in (%Ls) ORDER BY id ASC',
|
||||||
|
array_keys($phids));
|
||||||
|
|
||||||
|
if (empty($targets)) {
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
$editor = new ManiphestTransactionEditor();
|
||||||
|
|
||||||
|
$task_names = array();
|
||||||
|
|
||||||
|
$merge_into_name = 'T'.$task->getID();
|
||||||
|
|
||||||
|
$cc_vector = array();
|
||||||
|
$cc_vector[] = $task->getCCPHIDs();
|
||||||
|
foreach ($targets as $target) {
|
||||||
|
$cc_vector[] = $target->getCCPHIDs();
|
||||||
|
$cc_vector[] = array(
|
||||||
|
$target->getAuthorPHID(),
|
||||||
|
$target->getOwnerPHID());
|
||||||
|
|
||||||
|
$close_task = id(new ManiphestTransaction())
|
||||||
|
->setAuthorPHID($user->getPHID())
|
||||||
|
->setTransactionType(ManiphestTransactionType::TYPE_STATUS)
|
||||||
|
->setNewValue(ManiphestTaskStatus::STATUS_CLOSED_DUPLICATE)
|
||||||
|
->setComments("\xE2\x9C\x98 Merged into {$merge_into_name}.");
|
||||||
|
|
||||||
|
$editor->applyTransactions($target, array($close_task));
|
||||||
|
|
||||||
|
$task_names[] = 'T'.$target->getID();
|
||||||
|
}
|
||||||
|
$all_ccs = array_mergev($cc_vector);
|
||||||
|
$all_ccs = array_filter($all_ccs);
|
||||||
|
$all_ccs = array_unique($all_ccs);
|
||||||
|
|
||||||
|
$task_names = implode(', ', $task_names);
|
||||||
|
|
||||||
|
$add_ccs = id(new ManiphestTransaction())
|
||||||
|
->setAuthorPHID($user->getPHID())
|
||||||
|
->setTransactionType(ManiphestTransactionType::TYPE_CCS)
|
||||||
|
->setNewValue($all_ccs)
|
||||||
|
->setComments("\xE2\x97\x80 Merged tasks: {$task_names}.");
|
||||||
|
$editor->applyTransactions($task, array($add_ccs));
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ phutil_require_module('phabricator', 'aphront/response/404');
|
||||||
phutil_require_module('phabricator', 'aphront/response/dialog');
|
phutil_require_module('phabricator', 'aphront/response/dialog');
|
||||||
phutil_require_module('phabricator', 'aphront/response/reload');
|
phutil_require_module('phabricator', 'aphront/response/reload');
|
||||||
phutil_require_module('phabricator', 'applications/differential/storage/revision');
|
phutil_require_module('phabricator', 'applications/differential/storage/revision');
|
||||||
|
phutil_require_module('phabricator', 'applications/maniphest/constants/status');
|
||||||
phutil_require_module('phabricator', 'applications/maniphest/constants/transactiontype');
|
phutil_require_module('phabricator', 'applications/maniphest/constants/transactiontype');
|
||||||
phutil_require_module('phabricator', 'applications/maniphest/editor/transaction');
|
phutil_require_module('phabricator', 'applications/maniphest/editor/transaction');
|
||||||
phutil_require_module('phabricator', 'applications/maniphest/storage/task');
|
phutil_require_module('phabricator', 'applications/maniphest/storage/task');
|
||||||
|
@ -19,6 +20,7 @@ phutil_require_module('phabricator', 'applications/phid/handle/data');
|
||||||
phutil_require_module('phabricator', 'applications/search/controller/search');
|
phutil_require_module('phabricator', 'applications/search/controller/search');
|
||||||
phutil_require_module('phabricator', 'view/control/objectselector');
|
phutil_require_module('phabricator', 'view/control/objectselector');
|
||||||
|
|
||||||
|
phutil_require_module('phutil', 'markup');
|
||||||
phutil_require_module('phutil', 'utils');
|
phutil_require_module('phutil', 'utils');
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -23,10 +23,14 @@ class PhabricatorObjectSelectorDialog {
|
||||||
private $handles = array();
|
private $handles = array();
|
||||||
private $cancelURI;
|
private $cancelURI;
|
||||||
private $submitURI;
|
private $submitURI;
|
||||||
private $noun;
|
|
||||||
private $searchURI;
|
private $searchURI;
|
||||||
private $selectedFilter;
|
private $selectedFilter;
|
||||||
|
|
||||||
|
private $title;
|
||||||
|
private $header;
|
||||||
|
private $buttonText;
|
||||||
|
private $instructions;
|
||||||
|
|
||||||
public function setUser($user) {
|
public function setUser($user) {
|
||||||
$this->user = $user;
|
$this->user = $user;
|
||||||
return $this;
|
return $this;
|
||||||
|
@ -62,8 +66,23 @@ class PhabricatorObjectSelectorDialog {
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setNoun($noun) {
|
public function setTitle($title) {
|
||||||
$this->noun = $noun;
|
$this->title = $title;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setHeader($header) {
|
||||||
|
$this->header = $header;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setButtonText($button_text) {
|
||||||
|
$this->buttonText = $button_text;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setInstructions($instructions) {
|
||||||
|
$this->instructions = $instructions;
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,6 +112,14 @@ class PhabricatorObjectSelectorDialog {
|
||||||
}
|
}
|
||||||
$options = implode("\n", $options);
|
$options = implode("\n", $options);
|
||||||
|
|
||||||
|
$instructions = null;
|
||||||
|
if ($this->instructions) {
|
||||||
|
$instructions =
|
||||||
|
'<p class="phabricator-object-selector-instructions">'.
|
||||||
|
$this->instructions.
|
||||||
|
'</p>';
|
||||||
|
}
|
||||||
|
|
||||||
$search_box = phabricator_render_form(
|
$search_box = phabricator_render_form(
|
||||||
$user,
|
$user,
|
||||||
array(
|
array(
|
||||||
|
@ -119,10 +146,11 @@ class PhabricatorObjectSelectorDialog {
|
||||||
'<div class="phabricator-object-selector-current">'.
|
'<div class="phabricator-object-selector-current">'.
|
||||||
'<div class="phabricator-object-selector-currently-attached">'.
|
'<div class="phabricator-object-selector-currently-attached">'.
|
||||||
'<div class="phabricator-object-selector-header">'.
|
'<div class="phabricator-object-selector-header">'.
|
||||||
'Currently Attached '.$this->noun.
|
phutil_escape_html($this->header).
|
||||||
'</div>'.
|
'</div>'.
|
||||||
'<div id="'.$current_id.'">'.
|
'<div id="'.$current_id.'">'.
|
||||||
'</div>'.
|
'</div>'.
|
||||||
|
$instructions.
|
||||||
'</div>'.
|
'</div>'.
|
||||||
'</div>';
|
'</div>';
|
||||||
|
|
||||||
|
@ -130,14 +158,14 @@ class PhabricatorObjectSelectorDialog {
|
||||||
$dialog = new AphrontDialogView();
|
$dialog = new AphrontDialogView();
|
||||||
$dialog
|
$dialog
|
||||||
->setUser($this->user)
|
->setUser($this->user)
|
||||||
->setTitle('Manage Attached '.$this->noun)
|
->setTitle($this->title)
|
||||||
->setClass('phabricator-object-selector-dialog')
|
->setClass('phabricator-object-selector-dialog')
|
||||||
->appendChild($search_box)
|
->appendChild($search_box)
|
||||||
->appendChild($result_box)
|
->appendChild($result_box)
|
||||||
->appendChild($attached_box)
|
->appendChild($attached_box)
|
||||||
->setRenderDialogAsDiv()
|
->setRenderDialogAsDiv()
|
||||||
->setFormID($form_id)
|
->setFormID($form_id)
|
||||||
->addSubmitButton('Save '.$this->noun);
|
->addSubmitButton($this->buttonText);
|
||||||
|
|
||||||
if ($this->cancelURI) {
|
if ($this->cancelURI) {
|
||||||
$dialog->addCancelButton($this->cancelURI);
|
$dialog->addCancelButton($this->cancelURI);
|
||||||
|
|
|
@ -58,3 +58,6 @@
|
||||||
background-image: url(/rsrc/image/icon/tango/log.png);
|
background-image: url(/rsrc/image/icon/tango/log.png);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.aphront-headsup-action-list .action-merge {
|
||||||
|
background-image: url(/rsrc/image/icon/fatcow/arrow_merge.png);
|
||||||
|
}
|
||||||
|
|
|
@ -55,6 +55,10 @@
|
||||||
border-color: #aa0000;
|
border-color: #aa0000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.maniphest-transaction-detail-container .duplicate {
|
||||||
|
border-color: #333333;
|
||||||
|
}
|
||||||
|
|
||||||
.maniphest-transaction-header {
|
.maniphest-transaction-header {
|
||||||
background: #f3f3f3;
|
background: #f3f3f3;
|
||||||
padding: 4px 1em;
|
padding: 4px 1em;
|
||||||
|
|
|
@ -86,3 +86,9 @@ td.phabricator-object-selector-search-text {
|
||||||
color: #888888;
|
color: #888888;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.phabricator-object-selector-instructions {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #666666;
|
||||||
|
margin-top: 1.25em;
|
||||||
|
}
|
||||||
|
|
|
@ -8,4 +8,5 @@ They are available under the Creative Commons Attribution 3.0 License:
|
||||||
|
|
||||||
Some icons have been adapted from the FatCow set for use in Phabricator:
|
Some icons have been adapted from the FatCow set for use in Phabricator:
|
||||||
|
|
||||||
key_question.png
|
key_question.png
|
||||||
|
|
||||||
|
|
BIN
webroot/rsrc/image/icon/fatcow/arrow_merge.png
Normal file
BIN
webroot/rsrc/image/icon/fatcow/arrow_merge.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 508 B |
|
@ -80,7 +80,7 @@ JX.behavior('phabricator-object-selector', function(config) {
|
||||||
var btn = JX.$N(
|
var btn = JX.$N(
|
||||||
'a',
|
'a',
|
||||||
{className: 'button small grey'},
|
{className: 'button small grey'},
|
||||||
attach ? 'Attach' : 'Remove');
|
attach ? 'Select' : 'Remove');
|
||||||
|
|
||||||
JX.Stratcom.addSigil(btn, 'object-attach-button');
|
JX.Stratcom.addSigil(btn, 'object-attach-button');
|
||||||
JX.Stratcom.addData(btn, {handle : h, table : table});
|
JX.Stratcom.addData(btn, {handle : h, table : table});
|
||||||
|
|
Loading…
Reference in a new issue