diff --git a/scripts/celerity_mapper.php b/scripts/celerity_mapper.php index f766a669f..2c6c26b49 100755 --- a/scripts/celerity_mapper.php +++ b/scripts/celerity_mapper.php @@ -1,189 +1,191 @@ #!/usr/bin/env php array( 'phabricator-core-css', 'phabricator-core-buttons-css', 'phabricator-standard-page-view', 'aphront-dialog-view-css', 'aphront-form-view-css', 'aphront-panel-view-css', 'aphront-side-nav-view-css', 'aphront-table-view-css', 'aphront-crumbs-view-css', 'aphront-tokenizer-control-css', 'aphront-typeahead-control-css', 'phabricator-directory-css', 'phabricator-remarkup-css', 'syntax-highlighting-css', ), 'differential.pkg.css' => array( 'differential-core-view-css', 'differential-changeset-view-css', 'differential-revision-detail-css', 'differential-revision-history-css', 'differential-table-of-contents-css', 'differential-revision-comment-css', 'differential-revision-add-comment-css', 'differential-revision-comment-list-css', ), 'differential.pkg.js' => array( 'javelin-behavior-differential-feedback-preview', 'javelin-behavior-differential-edit-inline-comments', 'javelin-behavior-differential-populate', 'javelin-behavior-differential-show-more', 'javelin-behavior-differential-diff-radios', ), 'diffusion.pkg.css' => array( 'diffusion-commit-view-css', ), ); require_once dirname(__FILE__).'/__init_script__.php'; if ($argc != 2) { $self = basename($argv[0]); echo "usage: {$self} \n"; exit(1); } phutil_require_module('phutil', 'filesystem'); phutil_require_module('phutil', 'filesystem/filefinder'); phutil_require_module('phutil', 'future/exec'); phutil_require_module('phutil', 'parser/docblock'); $root = Filesystem::resolvePath($argv[1]); echo "Finding static resources...\n"; $files = id(new FileFinder($root)) ->withType('f') ->withSuffix('js') ->withSuffix('css') ->setGenerateChecksums(true) ->find(); echo "Processing ".count($files)." files"; $file_map = array(); foreach ($files as $path => $hash) { echo "."; $name = '/'.Filesystem::readablePath($path, $root); $file_map[$name] = array( 'hash' => $hash, 'disk' => $path, ); } echo "\n"; $runtime_map = array(); $hash_map = array(); $parser = new PhutilDocblockParser(); foreach ($file_map as $path => $info) { $data = Filesystem::readFile($info['disk']); $matches = array(); $ok = preg_match('@/[*][*].*?[*]/@s', $data, $matches); if (!$ok) { throw new Exception( "File {$path} does not have a header doc comment. Encode dependency ". "data in a header docblock."); } list($description, $metadata) = $parser->parse($matches[0]); $provides = preg_split('/\s+/', trim(idx($metadata, 'provides'))); $requires = preg_split('/\s+/', trim(idx($metadata, 'requires'))); $provides = array_filter($provides); $requires = array_filter($requires); + var_dump($requires); + if (count($provides) !== 1) { throw new Exception( "File {$path} must @provide exactly one Celerity target."); } $provides = reset($provides); $type = 'js'; if (preg_match('/\.css$/', $path)) { $type = 'css'; } $uri = '/res/'.substr($info['hash'], 0, 8).$path; $hash_map[$provides] = $info['hash']; $runtime_map[$provides] = array( 'uri' => $uri, 'type' => $type, 'requires' => $requires, 'disk' => $path, ); } $package_map = array(); foreach ($package_spec as $name => $package) { $hashes = array(); $type = null; foreach ($package as $symbol) { if (empty($hash_map[$symbol])) { throw new Exception( "Package specification for '{$name}' includes '{$symbol}', but that ". "symbol is not defined anywhere."); } if ($type === null) { $type = $runtime_map[$symbol]['type']; } else { $ntype = $runtime_map[$symbol]['type']; if ($type !== $ntype) { throw new Exception( "Package specification for '{$name}' mixes resources of type ". "'{$type}' with resources of type '{$ntype}'. Each package may only ". "contain one type of resource."); } } $hashes[] = $symbol.':'.$hash_map[$symbol]; } $key = substr(md5(implode("\n", $hashes)), 0, 8); $package_map['packages'][$key] = array( 'name' => $name, 'symbols' => $package, 'uri' => '/res/pkg/'.$key.'/'.$name, 'type' => $type, ); foreach ($package as $symbol) { $package_map['reverse'][$symbol] = $key; } } $runtime_map = var_export($runtime_map, true); $runtime_map = preg_replace('/\s+$/m', '', $runtime_map); $runtime_map = preg_replace('/array \(/', 'array(', $runtime_map); $package_map = var_export($package_map, true); $pacakge_map = preg_replace('/\s+$/m', '', $package_map); $package_map = preg_replace('/array \(/', 'array(', $package_map); $resource_map = << array( 'uri' => '/res/c666a518/rsrc/css/aphront/crumbs-view.css', 'type' => 'css', 'requires' => array( ), 'disk' => '/rsrc/css/aphront/crumbs-view.css', ), 'aphront-dark-console-css' => array( 'uri' => '/res/056b0c12/rsrc/css/aphront/dark-console.css', 'type' => 'css', 'requires' => array( ), 'disk' => '/rsrc/css/aphront/dark-console.css', ), 'aphront-dialog-view-css' => array( 'uri' => '/res/7101ab69/rsrc/css/aphront/dialog-view.css', 'type' => 'css', 'requires' => array( ), 'disk' => '/rsrc/css/aphront/dialog-view.css', ), 'aphront-error-view-css' => array( 'uri' => '/res/19b27527/rsrc/css/aphront/error-view.css', 'type' => 'css', 'requires' => array( ), 'disk' => '/rsrc/css/aphront/error-view.css', ), 'aphront-form-view-css' => array( 'uri' => '/res/8aaef437/rsrc/css/aphront/form-view.css', 'type' => 'css', 'requires' => array( ), 'disk' => '/rsrc/css/aphront/form-view.css', ), 'aphront-headsup-action-list-view-css' => array( 'uri' => '/res/8fd91c1d/rsrc/css/aphront/headsup-action-list-view.css', 'type' => 'css', 'requires' => array( ), 'disk' => '/rsrc/css/aphront/headsup-action-list-view.css', ), 'aphront-panel-view-css' => array( 'uri' => '/res/63672373/rsrc/css/aphront/panel-view.css', 'type' => 'css', 'requires' => array( ), 'disk' => '/rsrc/css/aphront/panel-view.css', ), 'aphront-request-failure-view-css' => array( 'uri' => '/res/97b8337a/rsrc/css/aphront/request-failure-view.css', 'type' => 'css', 'requires' => array( ), 'disk' => '/rsrc/css/aphront/request-failure-view.css', ), 'aphront-side-nav-view-css' => array( - 'uri' => '/res/09b7eb85/rsrc/css/aphront/side-nav-view.css', + 'uri' => '/res/4f4c5ca8/rsrc/css/aphront/side-nav-view.css', 'type' => 'css', 'requires' => array( ), 'disk' => '/rsrc/css/aphront/side-nav-view.css', ), 'aphront-table-view-css' => array( 'uri' => '/res/7bf17fb8/rsrc/css/aphront/table-view.css', 'type' => 'css', 'requires' => array( ), 'disk' => '/rsrc/css/aphront/table-view.css', ), 'aphront-tokenizer-control-css' => array( 'uri' => '/res/a3d23074/rsrc/css/aphront/tokenizer.css', 'type' => 'css', 'requires' => array( 0 => 'aphront-typeahead-control-css', ), 'disk' => '/rsrc/css/aphront/tokenizer.css', ), 'aphront-typeahead-control-css' => array( 'uri' => '/res/928df9f0/rsrc/css/aphront/typeahead.css', 'type' => 'css', 'requires' => array( ), 'disk' => '/rsrc/css/aphront/typeahead.css', ), 'phabricator-standard-page-view' => array( 'uri' => '/res/0d41ea7c/rsrc/css/application/base/standard-page-view.css', 'type' => 'css', 'requires' => array( ), 'disk' => '/rsrc/css/application/base/standard-page-view.css', ), 'differential-revision-add-comment-css' => array( 'uri' => '/res/aaae14d3/rsrc/css/application/differential/add-comment.css', 'type' => 'css', 'requires' => array( ), 'disk' => '/rsrc/css/application/differential/add-comment.css', ), 'differential-changeset-view-css' => array( 'uri' => '/res/f26ca6f9/rsrc/css/application/differential/changeset-view.css', 'type' => 'css', 'requires' => array( ), 'disk' => '/rsrc/css/application/differential/changeset-view.css', ), 'differential-core-view-css' => array( 'uri' => '/res/525d1a12/rsrc/css/application/differential/core.css', 'type' => 'css', 'requires' => array( ), 'disk' => '/rsrc/css/application/differential/core.css', ), 'differential-revision-comment-list-css' => array( 'uri' => '/res/10b9a829/rsrc/css/application/differential/revision-comment-list.css', 'type' => 'css', 'requires' => array( ), 'disk' => '/rsrc/css/application/differential/revision-comment-list.css', ), 'differential-revision-comment-css' => array( 'uri' => '/res/b271baaf/rsrc/css/application/differential/revision-comment.css', 'type' => 'css', 'requires' => array( ), 'disk' => '/rsrc/css/application/differential/revision-comment.css', ), 'differential-revision-detail-css' => array( 'uri' => '/res/623e3946/rsrc/css/application/differential/revision-detail.css', 'type' => 'css', 'requires' => array( ), 'disk' => '/rsrc/css/application/differential/revision-detail.css', ), 'differential-revision-history-css' => array( 'uri' => '/res/755f3da3/rsrc/css/application/differential/revision-history.css', 'type' => 'css', 'requires' => array( ), 'disk' => '/rsrc/css/application/differential/revision-history.css', ), 'differential-table-of-contents-css' => array( 'uri' => '/res/e68f6f05/rsrc/css/application/differential/table-of-contents.css', 'type' => 'css', 'requires' => array( ), 'disk' => '/rsrc/css/application/differential/table-of-contents.css', ), 'diffusion-commit-view-css' => array( - 'uri' => '/res/4593ecc8/rsrc/css/application/diffusion/commit-view.css', + 'uri' => '/res/8c139192/rsrc/css/application/diffusion/commit-view.css', 'type' => 'css', 'requires' => array( ), 'disk' => '/rsrc/css/application/diffusion/commit-view.css', ), 'phabricator-directory-css' => array( 'uri' => '/res/6a000601/rsrc/css/application/directory/phabricator-directory.css', 'type' => 'css', 'requires' => array( ), 'disk' => '/rsrc/css/application/directory/phabricator-directory.css', ), 'mainphest-task-detail-css' => array( 'uri' => '/res/e5f3beca/rsrc/css/application/maniphest/task-detail.css', 'type' => 'css', 'requires' => array( ), 'disk' => '/rsrc/css/application/maniphest/task-detail.css', ), 'maniphest-task-summary-css' => array( 'uri' => '/res/94d01e6f/rsrc/css/application/maniphest/task-summary.css', 'type' => 'css', 'requires' => array( ), 'disk' => '/rsrc/css/application/maniphest/task-summary.css', ), 'maniphest-transaction-detail-css' => array( 'uri' => '/res/9418efc9/rsrc/css/application/maniphest/transaction-detail.css', 'type' => 'css', 'requires' => array( ), 'disk' => '/rsrc/css/application/maniphest/transaction-detail.css', ), 'phabricator-object-selector-css' => array( 'uri' => '/res/52a7e289/rsrc/css/application/objectselector/object-selector.css', 'type' => 'css', 'requires' => array( 0 => 'aphront-dialog-view-css', ), 'disk' => '/rsrc/css/application/objectselector/object-selector.css', ), 'phabricator-profile-css' => array( 'uri' => '/res/259ad37f/rsrc/css/application/people/profile.css', 'type' => 'css', 'requires' => array( ), 'disk' => '/rsrc/css/application/people/profile.css', ), 'phabricator-core-buttons-css' => array( 'uri' => '/res/53b4f712/rsrc/css/core/buttons.css', 'type' => 'css', 'requires' => array( ), 'disk' => '/rsrc/css/core/buttons.css', ), 'phabricator-core-css' => array( 'uri' => '/res/6eebb99b/rsrc/css/core/core.css', 'type' => 'css', 'requires' => array( ), 'disk' => '/rsrc/css/core/core.css', ), 'phabricator-remarkup-css' => array( 'uri' => '/res/786989c3/rsrc/css/core/remarkup.css', 'type' => 'css', 'requires' => array( ), 'disk' => '/rsrc/css/core/remarkup.css', ), 'syntax-highlighting-css' => array( 'uri' => '/res/fb673ece/rsrc/css/core/syntax.css', 'type' => 'css', 'requires' => array( ), 'disk' => '/rsrc/css/core/syntax.css', ), 'javelin-behavior-dark-console' => array( 'uri' => '/res/020b0265/rsrc/js/application/core/behavior-dark-console.js', 'type' => 'js', 'requires' => array( ), 'disk' => '/rsrc/js/application/core/behavior-dark-console.js', ), 'javelin-behavior-phabricator-object-selector' => array( 'uri' => '/res/4fe735af/rsrc/js/application/core/behavior-object-selector.js', 'type' => 'js', 'requires' => array( 0 => 'javelin-lib-dev', ), 'disk' => '/rsrc/js/application/core/behavior-object-selector.js', ), 'javelin-behavior-aphront-basic-tokenizer' => array( 'uri' => '/res/8317d761/rsrc/js/application/core/behavior-tokenizer.js', 'type' => 'js', 'requires' => array( 0 => 'javelin-lib-dev', ), 'disk' => '/rsrc/js/application/core/behavior-tokenizer.js', ), 'javelin-behavior-workflow' => array( 'uri' => '/res/15446e7e/rsrc/js/application/core/behavior-workflow.js', 'type' => 'js', 'requires' => array( 0 => 'javelin-lib-dev', ), 'disk' => '/rsrc/js/application/core/behavior-workflow.js', ), + 'multirow-row-manager' => + array( + 'uri' => '/res/330d076b/rsrc/js/application/core/MultirowRowManager.js', + 'type' => 'js', + 'requires' => + array( + 0 => 'javelin-lib-dev', + ), + 'disk' => '/rsrc/js/application/core/MultirowRowManager.js', + ), 'javelin-behavior-differential-add-reviewers' => array( 'uri' => '/res/330154e4/rsrc/js/application/differential/behavior-add-reviewers.js', 'type' => 'js', 'requires' => array( 0 => 'javelin-lib-dev', ), 'disk' => '/rsrc/js/application/differential/behavior-add-reviewers.js', ), 'javelin-behavior-differential-feedback-preview' => array( 'uri' => '/res/8695d8b8/rsrc/js/application/differential/behavior-comment-preview.js', 'type' => 'js', 'requires' => array( 0 => 'javelin-lib-dev', ), 'disk' => '/rsrc/js/application/differential/behavior-comment-preview.js', ), 'javelin-behavior-differential-diff-radios' => array( 'uri' => '/res/fdeb3823/rsrc/js/application/differential/behavior-diff-radios.js', 'type' => 'js', 'requires' => array( 0 => 'javelin-lib-dev', ), 'disk' => '/rsrc/js/application/differential/behavior-diff-radios.js', ), 'javelin-behavior-differential-edit-inline-comments' => array( 'uri' => '/res/74747b2e/rsrc/js/application/differential/behavior-edit-inline-comments.js', 'type' => 'js', 'requires' => array( 0 => 'javelin-lib-dev', ), 'disk' => '/rsrc/js/application/differential/behavior-edit-inline-comments.js', ), 'javelin-behavior-differential-populate' => array( 'uri' => '/res/a13dcd7e/rsrc/js/application/differential/behavior-populate.js', 'type' => 'js', 'requires' => array( 0 => 'javelin-lib-dev', ), 'disk' => '/rsrc/js/application/differential/behavior-populate.js', ), 'javelin-behavior-differential-show-all-comments' => array( 'uri' => '/res/2a3592b8/rsrc/js/application/differential/behavior-show-all-comments.js', 'type' => 'js', 'requires' => array( 0 => 'javelin-lib-dev', ), 'disk' => '/rsrc/js/application/differential/behavior-show-all-comments.js', ), 'javelin-behavior-differential-show-more' => array( 'uri' => '/res/ea998002/rsrc/js/application/differential/behavior-show-more.js', 'type' => 'js', 'requires' => array( 0 => 'javelin-lib-dev', ), 'disk' => '/rsrc/js/application/differential/behavior-show-more.js', ), + 'javelin-behavior-herald-rule-editor' => + array( + 'uri' => '/res/f18bcd5e/rsrc/js/application/herald/herald-rule-editor.js', + 'type' => 'js', + 'requires' => + array( + 0 => 'herald-rule-editor', + ), + 'disk' => '/rsrc/js/application/herald/herald-rule-editor.js', + ), + 'herald-rule-editor' => + array( + 'uri' => '/res/e71d1d0e/rsrc/js/application/herald/HeraldRuleEditor.js', + 'type' => 'js', + 'requires' => + array( + 0 => 'multirow-row-manager', + ), + 'disk' => '/rsrc/js/application/herald/HeraldRuleEditor.js', + ), 'javelin-behavior-maniphest-transaction-controls' => array( 'uri' => '/res/fc6a8722/rsrc/js/application/maniphest/behavior-transaction-controls.js', 'type' => 'js', 'requires' => array( 0 => 'javelin-lib-dev', ), 'disk' => '/rsrc/js/application/maniphest/behavior-transaction-controls.js', ), 'javelin-magical-init' => array( 'uri' => '/res/76614f84/rsrc/js/javelin/init.dev.js', 'type' => 'js', 'requires' => array( ), 'disk' => '/rsrc/js/javelin/init.dev.js', ), 'javelin-init-prod' => array( 'uri' => '/res/1267c868/rsrc/js/javelin/init.min.js', 'type' => 'js', 'requires' => array( ), 'disk' => '/rsrc/js/javelin/init.min.js', ), 'javelin-lib-dev' => array( 'uri' => '/res/a0e7a5e9/rsrc/js/javelin/javelin.dev.js', 'type' => 'js', 'requires' => array( ), 'disk' => '/rsrc/js/javelin/javelin.dev.js', ), 'javelin-lib-prod' => array( 'uri' => '/res/2f2b3b2e/rsrc/js/javelin/javelin.min.js', 'type' => 'js', 'requires' => array( ), 'disk' => '/rsrc/js/javelin/javelin.min.js', ), 'javelin-typeahead-dev' => array( 'uri' => '/res/6de6ae59/rsrc/js/javelin/typeahead.dev.js', 'type' => 'js', 'requires' => array( ), 'disk' => '/rsrc/js/javelin/typeahead.dev.js', ), 'javelin-typeahead-prod' => array( 'uri' => '/res/69d5fad1/rsrc/js/javelin/typeahead.min.js', 'type' => 'js', 'requires' => array( ), 'disk' => '/rsrc/js/javelin/typeahead.min.js', ), 'javelin-workflow-dev' => array( 'uri' => '/res/c6b17f93/rsrc/js/javelin/workflow.dev.js', 'type' => 'js', 'requires' => array( ), 'disk' => '/rsrc/js/javelin/workflow.dev.js', ), 'javelin-workflow-prod' => array( 'uri' => '/res/b758e0a0/rsrc/js/javelin/workflow.min.js', 'type' => 'js', 'requires' => array( ), 'disk' => '/rsrc/js/javelin/workflow.min.js', ), ), array ( 'packages' => array ( - 73063447 => + '848f4c9f' => array ( 'name' => 'core.pkg.css', 'symbols' => array ( 0 => 'phabricator-core-css', 1 => 'phabricator-core-buttons-css', 2 => 'phabricator-standard-page-view', 3 => 'aphront-dialog-view-css', 4 => 'aphront-form-view-css', 5 => 'aphront-panel-view-css', 6 => 'aphront-side-nav-view-css', 7 => 'aphront-table-view-css', 8 => 'aphront-crumbs-view-css', 9 => 'aphront-tokenizer-control-css', 10 => 'aphront-typeahead-control-css', 11 => 'phabricator-directory-css', 12 => 'phabricator-remarkup-css', 13 => 'syntax-highlighting-css', ), - 'uri' => '/res/pkg/73063447/core.pkg.css', + 'uri' => '/res/pkg/848f4c9f/core.pkg.css', 'type' => 'css', ), '76f3c1f8' => array ( 'name' => 'differential.pkg.css', 'symbols' => array ( 0 => 'differential-core-view-css', 1 => 'differential-changeset-view-css', 2 => 'differential-revision-detail-css', 3 => 'differential-revision-history-css', 4 => 'differential-table-of-contents-css', 5 => 'differential-revision-comment-css', 6 => 'differential-revision-add-comment-css', 7 => 'differential-revision-comment-list-css', ), 'uri' => '/res/pkg/76f3c1f8/differential.pkg.css', 'type' => 'css', ), '30d594cf' => array ( 'name' => 'differential.pkg.js', 'symbols' => array ( 0 => 'javelin-behavior-differential-feedback-preview', 1 => 'javelin-behavior-differential-edit-inline-comments', 2 => 'javelin-behavior-differential-populate', 3 => 'javelin-behavior-differential-show-more', 4 => 'javelin-behavior-differential-diff-radios', ), 'uri' => '/res/pkg/30d594cf/differential.pkg.js', 'type' => 'js', ), - '2393c3a4' => + 'eadf6ec3' => array ( 'name' => 'diffusion.pkg.css', 'symbols' => array ( 0 => 'diffusion-commit-view-css', ), - 'uri' => '/res/pkg/2393c3a4/diffusion.pkg.css', + 'uri' => '/res/pkg/eadf6ec3/diffusion.pkg.css', 'type' => 'css', ), ), 'reverse' => array ( - 'phabricator-core-css' => '73063447', - 'phabricator-core-buttons-css' => '73063447', - 'phabricator-standard-page-view' => '73063447', - 'aphront-dialog-view-css' => '73063447', - 'aphront-form-view-css' => '73063447', - 'aphront-panel-view-css' => '73063447', - 'aphront-side-nav-view-css' => '73063447', - 'aphront-table-view-css' => '73063447', - 'aphront-crumbs-view-css' => '73063447', - 'aphront-tokenizer-control-css' => '73063447', - 'aphront-typeahead-control-css' => '73063447', - 'phabricator-directory-css' => '73063447', - 'phabricator-remarkup-css' => '73063447', - 'syntax-highlighting-css' => '73063447', + 'phabricator-core-css' => '848f4c9f', + 'phabricator-core-buttons-css' => '848f4c9f', + 'phabricator-standard-page-view' => '848f4c9f', + 'aphront-dialog-view-css' => '848f4c9f', + 'aphront-form-view-css' => '848f4c9f', + 'aphront-panel-view-css' => '848f4c9f', + 'aphront-side-nav-view-css' => '848f4c9f', + 'aphront-table-view-css' => '848f4c9f', + 'aphront-crumbs-view-css' => '848f4c9f', + 'aphront-tokenizer-control-css' => '848f4c9f', + 'aphront-typeahead-control-css' => '848f4c9f', + 'phabricator-directory-css' => '848f4c9f', + 'phabricator-remarkup-css' => '848f4c9f', + 'syntax-highlighting-css' => '848f4c9f', 'differential-core-view-css' => '76f3c1f8', 'differential-changeset-view-css' => '76f3c1f8', 'differential-revision-detail-css' => '76f3c1f8', 'differential-revision-history-css' => '76f3c1f8', 'differential-table-of-contents-css' => '76f3c1f8', 'differential-revision-comment-css' => '76f3c1f8', 'differential-revision-add-comment-css' => '76f3c1f8', 'differential-revision-comment-list-css' => '76f3c1f8', 'javelin-behavior-differential-feedback-preview' => '30d594cf', 'javelin-behavior-differential-edit-inline-comments' => '30d594cf', 'javelin-behavior-differential-populate' => '30d594cf', 'javelin-behavior-differential-show-more' => '30d594cf', 'javelin-behavior-differential-diff-radios' => '30d594cf', - 'diffusion-commit-view-css' => '2393c3a4', + 'diffusion-commit-view-css' => 'eadf6ec3', ), )); diff --git a/src/applications/herald/controller/rule/HeraldRuleController.php b/src/applications/herald/controller/rule/HeraldRuleController.php index 84c0e2b20..0bf62be8d 100644 --- a/src/applications/herald/controller/rule/HeraldRuleController.php +++ b/src/applications/herald/controller/rule/HeraldRuleController.php @@ -1,481 +1,496 @@ id = (int)idx($data, 'id'); } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $content_type_map = HeraldContentTypeConfig::getContentTypeMap(); if ($this->id) { $rule = id(new HeraldRule())->load($this->id); if (!$rule) { return new Aphront404Response(); } if ($rule->getAuthorPHID() != $user->getPHID()) { throw new Exception("You don't own this rule and can't edit it."); } } else { $rule = new HeraldRule(); $rule->setAuthorPHID($user->getPHID()); $rule->setMustMatchAll(true); $type = $request->getStr('type'); if (!isset($content_type_map[$type])) { $type = HeraldContentTypeConfig::CONTENT_TYPE_DIFFERENTIAL; } $rule->setContentType($type); } $local_version = id(new HeraldRule())->getConfigVersion(); if ($rule->getConfigVersion() > $local_version) { throw new Exception( "This rule was created with a newer version of Herald. You can not ". "view or edit it in this older version. Try dev or wait for a push."); } // Upgrade rule version to our version, since we might add newly-defined // conditions, etc. $rule->setConfigVersion($local_version); $rule_conditions = $rule->loadConditions(); $rule_actions = $rule->loadActions(); $rule->attachConditions($rule_conditions); $rule->attachActions($rule_actions); $arr = "\xC2\xAB"; $e_name = true; $errors = array(); if ($request->isFormPost() && $request->getStr('save')) { $rule->setName($request->getStr('name')); $rule->setMustMatchAll(($request->getStr('must_match') == 'all')); if (!strlen($rule->getName())) { $e_name = "{$arr} Required"; $errors[] = "Rule must have a name."; } $data = json_decode($request->getStr('rule'), true); if (!is_array($data) || !$data['conditions'] || !$data['actions']) { throw new Exception("Failed to decode rule data."); } $conditions = array(); foreach ($data['conditions'] as $condition) { $obj = new HeraldCondition(); $obj->setFieldName($condition[0]); $obj->setCondition($condition[1]); if (is_array($condition[2])) { $obj->setValue(array_keys($condition[2])); } else { $obj->setValue($condition[2]); } $cond_type = $obj->getCondition(); if ($cond_type == HeraldConditionConfig::CONDITION_REGEXP) { if (@preg_match($obj->getValue(), '') === false) { $errors[] = 'The regular expression "'.$obj->getValue().'" is not valid. '. 'Regular expressions must have enclosing characters (e.g. '. '"@/path/to/file@", not "/path/to/file") and be syntactically '. 'correct.'; } } if ($cond_type == HeraldConditionConfig::CONDITION_REGEXP_PAIR) { $json = json_decode($obj->getValue(), true); if (!is_array($json)) { $errors[] = 'The regular expression pair "'.$obj->getValue().'" is not '. 'valid JSON. Enter a valid JSON array with two elements.'; } else { if (count($json) != 2) { $errors[] = 'The regular expression pair "'.$obj->getValue().'" must have '. 'exactly two elements.'; } else { $key_regexp = array_shift($json); $val_regexp = array_shift($json); if (@preg_match($key_regexp, '') === false) { $errors[] = 'The first regexp, "'.$key_regexp.'" in the regexp pair '. 'is not a valid regexp.'; } if (@preg_match($val_regexp, '') === false) { $errors[] = 'The second regexp, "'.$val_regexp.'" in the regexp pair '. 'is not a valid regexp.'; } } } } $conditions[] = $obj; } $actions = array(); foreach ($data['actions'] as $action) { $obj = new HeraldAction(); $obj->setAction($action[0]); if (!isset($action[1])) { // Legitimate for any action which doesn't need a target, like // "Do nothing". $action[1] = null; } if (is_array($action[1])) { $obj->setTarget(array_keys($action[1])); } else { $obj->setTarget($action[1]); } $actions[] = $obj; } $rule->attachConditions($conditions); $rule->attachActions($actions); if (!$errors) { try { $rule->openTransaction(); $rule->save(); $rule->saveConditions($conditions); $rule->saveActions($actions); $rule->saveTransaction(); $uri = '/herald/view/'.$rule->getContentType().'/'; return id(new AphrontRedirectResponse()) ->setURI($uri); } catch (QueryDuplicateKeyException $ex) { $e_name = "{$arr} Not Unique"; $errors[] = "Rule name is not unique. Choose a unique name."; } } } $phids = array(); $phids[] = $rule->getAuthorPHID(); foreach ($rule->getActions() as $action) { foreach ($action->getTarget() as $target) { $target = (array)$target; foreach ($target as $phid) { $phids[] = $phid; } } } foreach ($rule->getConditions() as $condition) { $value = $condition->getValue(); if (is_array($value)) { foreach ($value as $phid) { $phids[] = $phid; } } } $handles = id(new PhabricatorObjectHandleData($phids)) ->loadHandles(); if ($errors) { $errors = '!!';//; } // require_static('herald-css'); $options = array( 'all' => 'all of', 'any' => 'any of', ); $selected = $rule->getMustMatchAll() ? 'all' : 'any'; $must_match = array(); foreach ($options as $key => $option) { $must_match[] = phutil_render_tag( 'option', array( 'selected' => ($selected == $key) ? 'selected' : null, ), phutil_escape_html($option)); } $must_match = ''; if ($rule->getID()) { $action = '/herald/rule/'.$rule->getID().'/'; } else { $action = '/herald/rule/'.$rule->getID().'/'; } $type_name = $content_type_map[$rule->getContentType()]; $form = id(new AphrontFormView()) ->setUser($user) + ->setID('herald-rule-edit-form') ->addHiddenInput('type', $rule->getContentType()) ->addHiddenInput('save', true) ->addHiddenInput('rule', '') ->appendChild( id(new AphrontFormTextControl()) ->setLabel('Rule Name') ->setError($e_name) ->setValue($rule->getName())) ->appendChild( id(new AphrontFormStaticControl()) ->setLabel('Author') ->setValue($handles[$rule->getAuthorPHID()]->getName())) ->appendChild( id(new AphrontFormMarkupControl()) ->setValue( "This rule triggers for {$type_name}.")) ->appendChild( '

Conditions

'. '
'. - 'Create New Condition'. + javelin_render_tag( + 'a', + array( + 'href' => '#', + 'class' => 'button green', + 'sigil' => 'create-action', + ), + 'Create New Condition'). '

When '.$must_match.' these conditions are met:

'. - '
'. + javelin_render_tag( + 'table', + array( + 'sigil' => 'rule-conditions', + ), + ''). '
') ->appendChild( '

Action

'. '
'. 'Create New Action'. '

Take these actions:

'. - '
'. + javelin_render_tag( + 'table', + array( + 'sigil' => 'rule-actions', + ), + ''). '
') ->appendChild( id(new AphrontFormSubmitControl()) ->setValue('Save') ->addCancelButton('/herald/view/'.$rule->getContentType().'/')); /* $form =
getStr('type')} /> {$errors}

Edit Rule

getName()} /> {$handles[$rule->getAuthorPHID()]->getName()} This rule triggers for {$content_type_map[$rule->getContentType()]}.

Conditions

When {$must_match} these conditions are met:

Actions

Take these actions:
Cancel
; */ $serial_conditions = array( array('default', 'default', ''), ); if ($rule->getConditions()) { $serial_conditions = array(); foreach ($rule->getConditions() as $condition) { $value = $condition->getValue(); if (is_array($value)) { $value_map = array(); foreach ($value as $k => $fbid) { $value_map[$fbid] = $handles[$fbid]->getExtendedDisplayName(); } $value = $value_map; } $serial_conditions[] = array( $condition->getFieldName(), $condition->getCondition(), $value, ); } } $serial_actions = array( array('default', ''), ); if ($rule->getActions()) { $serial_actions = array(); foreach ($rule->getActions() as $action) { $target_map = array(); foreach ((array)$action->getTarget() as $fbid) { $target_map[$fbid] = $handles[$fbid]->getExtendedDisplayName(); } $serial_actions[] = array( $action->getAction(), $target_map, ); } } $all_rules = id(new HeraldRule())->loadAllWhere( 'authorPHID = %d AND contentType = %s', $rule->getAuthorPHID(), $rule->getContentType()); $all_rules = mpull($all_rules, 'getName', 'getID'); asort($all_rules); unset($all_rules[$rule->getID()]); $config_info = array(); $config_info['fields'] = HeraldFieldConfig::getFieldMapForContentType($rule->getContentType()); $config_info['conditions'] = HeraldConditionConfig::getConditionMap(); foreach ($config_info['fields'] as $field => $name) { $config_info['conditionMap'][$field] = array_keys( HeraldConditionConfig::getConditionMapForField($field)); } foreach ($config_info['fields'] as $field => $fname) { foreach ($config_info['conditions'] as $condition => $cname) { $config_info['values'][$field][$condition] = HeraldValueTypeConfig::getValueTypeForFieldAndCondition( $field, $condition); } } $config_info['actions'] = HeraldActionConfig::getActionMapForContentType($rule->getContentType()); foreach ($config_info['actions'] as $action => $name) { $config_info['targets'][$action] = HeraldValueTypeConfig::getValueTypeForAction($action); } -/* Javelin::initBehavior( 'herald-rule-editor', array( - 'root' => 'qq',//$form->requireUniqueId(), + 'root' => 'herald-rule-edit-form', 'conditions' => (object) $serial_conditions, 'actions' => (object) $serial_actions, 'template' => $this->buildTokenizerTemplates() + array( 'rules' => $all_rules, ), 'info' => $config_info, )); -*/ - $panel = new AphrontPanelView(); $panel->setHeader('Edit Herald Rule'); $panel->setWidth(AphrontPanelView::WIDTH_WIDE); $panel->appendChild($form); return $this->buildStandardPageResponse( $panel, array( 'title' => 'Edit Rule', )); } protected function buildTokenizerTemplates() { return array( 'source' => array( 'email' => '/datasource/mailable/', 'user' => '/datasource/user/', 'repository' => '/datasource/repository/', 'tag' => '/datasource/tag/', 'package' => '/datasource/package/', ), 'markup' => 'derp derp',//id()->toString(), ); } } diff --git a/src/view/form/base/AphrontFormView.php b/src/view/form/base/AphrontFormView.php index 4722a32f7..710b22363 100755 --- a/src/view/form/base/AphrontFormView.php +++ b/src/view/form/base/AphrontFormView.php @@ -1,99 +1,106 @@ id = $id; + return $this; + } public function setUser(PhabricatorUser $user) { $this->user = $user; return $this; } public function setAction($action) { $this->action = $action; return $this; } public function setMethod($method) { $this->method = $method; return $this; } public function setEncType($enc_type) { $this->encType = $enc_type; return $this; } public function addHiddenInput($key, $value) { $this->data[$key] = $value; return $this; } public function setWorkflow($workflow) { $this->workflow = $workflow; return $this; } public function render() { require_celerity_resource('aphront-form-view-css'); return javelin_render_tag( 'form', array( 'action' => $this->action, 'method' => $this->method, 'class' => 'aphront-form-view', 'enctype' => $this->encType, 'sigil' => $this->workflow ? 'workflow' : null, + 'id' => $this->id, ), $this->renderDataInputs(). $this->renderChildren()); } private function renderDataInputs() { if (!$this->user) { throw new Exception('You must pass the user to AphrontFormView.'); } $data = $this->data + array( '__form__' => 1, '__csrf__' => $this->user->getCSRFToken(), ); $inputs = array(); foreach ($data as $key => $value) { if ($value === null) { continue; } $inputs[] = phutil_render_tag( 'input', array( 'type' => 'hidden', 'name' => $key, 'value' => $value, )); } return implode("\n", $inputs); } } diff --git a/webroot/rsrc/js/application/core/MultirowRowManager.js b/webroot/rsrc/js/application/core/MultirowRowManager.js new file mode 100644 index 000000000..45d1bf368 --- /dev/null +++ b/webroot/rsrc/js/application/core/MultirowRowManager.js @@ -0,0 +1,143 @@ +/** + * @requires javelin-lib-dev + * @provides multirow-row-manager + * @javelin + */ + + +/** + * Give a MultirowRowManager a table DOM elem to manage. + * You can add rows, and provide a given ID if you like. + * You can update rows by ID. + * Rows are automatically equipped with a removal button. + * You can listen to the 'row-removed' event on the Manager to get + * notifications of these row removals, with the DOM id of the removed + * row as event data. + */ +JX.install('MultirowRowManager', { + /** + * @param DOM element
root Container for rows + */ + construct : function(root, minRows) { + this._root = root; + this._rows = []; + + if (typeof minRows !== "undefined") { + this._minRows = minRows; + } else { + this._minRows = 1; + } + + JX.DOM.listen( + this._root, + 'click', + JX.MultirowRowManager._removeSigil, + JX.bind(this, this._onrowremoved)); + }, + + members : { + _count : 0, + _nextID : 0, + _root : null, + _rows : null, + + _generateRowID : function() { + return "" + this._nextID++; + }, + + _wrapRowContents : function(row_id, row_contents) { + var row = JX.$N('tr', + { sigil : JX.MultirowRowManager.getRowSigil(), + meta : { multirow_row_manager_row_id : row_id } + }, + row_contents); + + var removeButton = JX.$N( + 'td', + {}, + JX.$N( + 'a', + { className: "button", + sigil: JX.MultirowRowManager._removeSigil + }, + '-')); + + JX.DOM.appendContent(row, removeButton); + return row; + }, + + getRowID : function(row) { + return JX.Stratcom.getData(row).multirow_row_manager_row_id; + }, + /** + * @param row_contents [DOM elements] New contents of row + * @param row_id row ID to update, will throw if this row has been removed + */ + updateRow : function(row_id, row_contents) { + if (__DEV__) { + if (typeof this._rows[row_id] === "undefined") { + throw new Error("JX.MultirowRowManager.updateRow(row_id, " + + "row_contents): provided row id does not exist." + + " Use addRow to create a new row and make sure " + + "not to update rows that have been deleted."); + } + } + var old_row = this._rows[row_id]; + var new_row = this._wrapRowContents(row_id, row_contents); + JX.copy(JX.Stratcom.getData(new_row), JX.Stratcom.getData(old_row)); + + JX.DOM.replace(old_row, new_row); + this._rows[row_id] = new_row; + + this._oncountchanged(); // Fix the new button. + return new_row; + }, + + addRow : function(row_contents) { + var row_id = this._generateRowID(); + var row = this._wrapRowContents(row_id, row_contents); + JX.DOM.appendContent(this._root, row); + + this._count++; + this._oncountchanged(); + + this._rows[row_id] = row; + return row; + }, + _onrowremoved : function(e) { + if (!JX.Stratcom.getData(e.getTarget()).enabled) { + return; + } + var row = e.getNode(JX.MultirowRowManager.getRowSigil()); + var row_id = this.getRowID(row); + delete this._rows[row_id]; + JX.DOM.remove(row); + + this._count--; + this._oncountchanged(); + this.invoke('row-removed', row_id); + }, + + _oncountchanged : function(e) { + var buttons = JX.DOM.scry( + this._root, + 'a', + JX.MultirowRowManager._removeSigil); + + var disable = (this._minRows >= 0 && this._count <= this._minRows); + for (var i = 0; i < buttons.length; i++) { + var button = buttons[i]; + JX.DOM.alterClass(button, 'disabled', disable); + JX.Stratcom.getData(button).enabled = !disable; + } + } + }, + events : ['row-removed'], + statics : { + getRowSigil : function() { + return "tools-multirow-row-manager-row"; + }, + _removeSigil : "tools-multirow-row-manager-row-remove" + } +}); + diff --git a/webroot/rsrc/js/application/herald/HeraldRuleEditor.js b/webroot/rsrc/js/application/herald/HeraldRuleEditor.js new file mode 100644 index 000000000..a6bce77dd --- /dev/null +++ b/webroot/rsrc/js/application/herald/HeraldRuleEditor.js @@ -0,0 +1,363 @@ +/** + * @requires multirow-row-manager + * javelin-lib-dev + * javelin-typeahead-dev + * @provides herald-rule-editor + * @javelin + */ + +JX.install('HeraldRuleEditor', { + construct : function(config) { + var root = JX.$(config.root); + this._root = root; + + JX.DOM.listen( + root, + 'click', + 'create-condition', + JX.bind(this, this._onnewcondition)); + + JX.DOM.listen( + root, + 'click', + 'create-action', + JX.bind(this, this._onnewaction)); + + JX.DOM.listen(root, 'change', null, JX.bind(this, this._onchange)); + JX.DOM.listen(root, 'submit', null, JX.bind(this, this._onsubmit)); + + var conditionsTable = JX.DOM.find(root, 'table', 'rule-conditions'); + var actionsTable = JX.DOM.find(root, 'table', 'rule-actions'); + + this._conditionsRowManager = new JX.MultirowRowManager(conditionsTable); + this._conditionsRowManager.listen( + 'row-removed', + JX.bind(this, function(row_id) { + delete this._config.conditions[row_id]; + })); + + this._actionsRowManager = new JX.MultirowRowManager(actionsTable); + this._actionsRowManager.listen( + 'row-removed', + JX.bind(this, function(row_id) { + delete this._config.actions[row_id]; + })); + + this._conditionGetters = {}; + this._conditionTypes = {}; + this._actionGetters = {}; + this._actionTypes = {}; + + this._config = config; + + var conditions = this._config.conditions; + this._config.conditions = []; + + var actions = this._config.actions; + this._config.actions = []; + + this._renderConditions(conditions); + this._renderActions(actions); + }, + + members : { + _config : null, + _root : null, + _conditionGetters : null, + _conditionTypes : null, + _actionGetters : null, + _actionTypes : null, + _conditionsRowManager : null, + _actionsRowManager : null, + + _onnewcondition : function(e) { + this._newCondition(); + e.kill(); + }, + _onnewaction : function(e) { + this._newAction(); + e.kill(); + }, + _onchange : function(e) { + var target = e.getTarget(); + + var row = e.getNode(JX.MultirowRowManager.getRowSigil()); + if (!row) { + // Changing the "when all of / any of these..." dropdown. + return; + } + + if (JX.Stratcom.hasSigil(target, 'field-select')) { + this._onfieldchange(row); + } else if (JX.Stratcom.hasSigil(target, 'condition-select')) { + this._onconditionchange(row); + } else if (JX.Stratcom.hasSigil(target, 'action-select')) { + this._onactionchange(row); + } + }, + _onsubmit : function(e) { + var rule = JX.DOM.find(this._root, 'input', 'rule'); + + var k; + + for (k in this._config.conditions) { + this._config.conditions[k][2] = this._getConditionValue(k); + } + + var acts = this._config.actions; + for (k in this._config.actions) { + this._config.actions[k][1] = this._getActionTarget(k); + } + + rule.value = JX.JSON.serialize({ + conditions: this._config.conditions, + actions: this._config.actions + }); + }, + + _getConditionValue : function(id) { + if (this._conditionGetters[id]) { + return this._conditionGetters[id](); + } + return this._config.conditions[id][2]; + }, + + _getActionTarget : function(id) { + if (this._actionGetters[id]) { + return this._actionGetters[id](); + } + return this._config.actions[id][1]; + }, + + _onactionchange : function(r) { + var target = JX.DOM.find(r, 'select', 'action-select'); + var row_id = this._actionsRowManager.getRowID(r); + + this._config.actions[row_id][0] = target.value; + + var target_cell = JX.DOM.find(r, 'td', 'target-cell'); + var target_input = this._renderTargetInputForRow(row_id); + + JX.DOM.setContent(target_cell, target_input); + }, + _onfieldchange : function(r) { + var target = JX.DOM.find(r, 'select', 'field-select'); + var row_id = this._actionsRowManager.getRowID(r); + + this._config.conditions[row_id][0] = target.value; + + var condition_cell = JX.DOM.find(r, 'td', 'condition-cell'); + var condition_select = this._renderSelect( + this._selectKeys( + this._config.info.conditions, + this._config.info.conditionMap[target.value]), + this._config.conditions[row_id][1], + 'condition-select'); + + JX.DOM.setContent(condition_cell, condition_select); + + this._onconditionchange(r); + }, + _onconditionchange : function(r) { + var target = JX.DOM.find(r, 'select', 'condition-select'); + var row_id = this._conditionsRowManager.getRowID(r); + + this._config.conditions[row_id][1] = target.value; + + var value_cell = JX.DOM.find(r, 'td', 'value-cell'); + var value_input = this._renderValueInputForRow(row_id); + JX.DOM.setContent(value_cell, value_input); + }, + + _renderTargetInputForRow : function(row_id) { + var action = this._config.actions[row_id]; + var type = this._config.info.targets[action[0]]; + + var input = this._buildInput(type); + var node = input[0]; + var get_fn = input[1]; + var set_fn = input[2]; + + if (node) { + JX.Stratcom.addSigil(node, 'action-target'); + } + + + var old_type = this._actionTypes[row_id]; + if (old_type == type || !old_type) { + set_fn(this._getActionTarget(row_id)); + } + + this._actionTypes[row_id] = type; + this._actionGetters[row_id] = get_fn; + + return node; + }, + + _buildInput : function(type) { + var input; + var get_fn; + var set_fn; + switch (type) { + case 'rule': + input = this._renderSelect(this._config.template.rules); + get_fn = function() { return input.value; }; + set_fn = function(v) { input.value = v; }; + break; + case 'email': + case 'employee': + case 'repository': + case 'tag': + case 'package': + var tokenizer = this._newTokenizer(type); + input = tokenizer[0]; + get_fn = tokenizer[1]; + set_fn = tokenizer[2]; + break; + case 'none': + input = ''; + get_fn = JX.bag; + set_fn = JX.bag; + break; + default: + input = JX.$N('input'); + get_fn = function() { return input.value; }; + set_fn = function(v) { input.value = v; }; + break; + } + + return [input, get_fn, set_fn]; + }, + + _renderValueInputForRow : function(row_id) { + var cond = this._config.conditions[row_id]; + var type = this._config.info.values[cond[0]][cond[1]]; + + var input = this._buildInput(type); + var node = input[0]; + var get_fn = input[1]; + var set_fn = input[2]; + + if (node) { + JX.Stratcom.addSigil(node, 'condition-value'); + } + + var old_type = this._conditionTypes[row_id]; + if (old_type == type || !old_type) { + set_fn(this._getConditionValue(row_id)); + } + + this._conditionTypes[row_id] = type; + this._conditionGetters[row_id] = get_fn; + + return node; + }, + + _newTokenizer : function(type) { + var template = JX.$N( + 'div', + new JX.HTML(this._config.template.markup)); + template = template.firstChild; + template.id = ''; + + var datasource = new JX.TypeaheadPreloadedSource( + this._config.template.source[type]); + + var typeahead = new JX.Typeahead(template); + typeahead.setDatasource(datasource); + + var tokenizer = new JX.Tokenizer(template); + tokenizer.setTypeahead(typeahead); + tokenizer.start(); + + return [ + template, + function() { + return tokenizer.getTokens(); + }, + function(map) { + for (var k in map) { + tokenizer.addToken(k, map[k]); + } + }]; + }, + _selectKeys : function(map, keys) { + var r = {}; + for (var ii = 0; ii < keys.length; ii++) { + r[keys[ii]] = map[keys[ii]]; + } + return r; + }, + _renderConditions : function(conditions) { + for (var k in conditions) { + this._newCondition(conditions[k]); + } + }, + _newCondition : function(data) { + var row = this._conditionsRowManager.addRow([]); + var row_id = this._conditionsRowManager.getRowID(row); + this._config.conditions[row_id] = data || [null, null, '']; + var r = this._conditionsRowManager.updateRow( + row_id, + this._renderCondition(row_id)); + + this._onfieldchange(r); + }, + _renderCondition : function(row_id) { + var field_select = this._renderSelect( + this._config.info.fields, + this._config.conditions[row_id][0], + 'field-select'); + var field_cell = JX.$N('td', {sigil: 'field-cell'}, field_select); + + var condition_cell = JX.$N('td', {sigil: 'condition-cell'}); + var value_cell = JX.$N('td', {className : 'value', sigil: 'value-cell'}); + + return [field_cell, condition_cell, value_cell]; + }, + _renderActions : function(actions) { + for (var k in actions) { + this._newAction(actions[k]); + delete actions[k]; + } + }, + _newAction : function(data) { + data = data || []; + var temprow = this._actionsRowManager.addRow([]); + var row_id = this._actionsRowManager.getRowID(temprow); + this._config.actions[row_id] = data; + var r = this._actionsRowManager.updateRow(row_id, + this._renderAction(data)); + this._onactionchange(r); + }, + _renderAction : function(action) { + var action_select = this._renderSelect( + this._config.info.actions, + action[0], + 'action-select'); + var action_cell = JX.$N('td', {sigil: 'action-cell'}, action_select); + + var target_cell = JX.$N( + 'td', + {className : 'target', sigil : 'target-cell'}); + + return [action_cell, target_cell]; + }, + _renderSelect : function(map, selected, sigil) { + var select = JX.$N( + 'select', + { + style : {width: '250px', margin: '0 .5em 0 0'}, + sigil : sigil + }); + for (var k in map) { + select.options[select.options.length] = new Option(map[k], k); + if (k == selected) { + select.value = k; + } + } + select.value = select.value || JX.keys(map)[0]; + return select; + } + } +}); \ No newline at end of file diff --git a/webroot/rsrc/js/application/herald/herald-rule-editor.js b/webroot/rsrc/js/application/herald/herald-rule-editor.js new file mode 100644 index 000000000..ff4f17ecc --- /dev/null +++ b/webroot/rsrc/js/application/herald/herald-rule-editor.js @@ -0,0 +1,10 @@ +/** + * @requires herald-rule-editor + * javelin-behavior + * @provides javelin-behavior-herald-rule-editor + * @javelin + */ + +JX.behavior('herald-rule-editor', function(config) { + new JX.HeraldRuleEditor(config); +});