diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..176a458f94 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto diff --git a/.gitignore b/.gitignore index b7f6818b8f..6dbcffe859 100644 --- a/.gitignore +++ b/.gitignore @@ -31,9 +31,6 @@ # Users can link binaries here /support/bin/* -# User extensions -/src/extensions/* - # NPM local packages /support/aphlict/server/node_modules/ /support/aphlict/server/package.json diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000..b48929bb8b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,134 @@ +##### Start Phabricator +FROM php:8.2-apache +##### End Phabricator + +LABEL org.opencontainers.image.source https://github.com/moonmana-games/phabricator + +# Required Components +# @see https://secure.phabricator.com/book/phabricator/article/installation_guide/#installing-required-comp +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + ca-certificates \ + git \ + imagemagick \ + libzip4 \ + mariadb-client \ + mercurial \ + # @see https://secure.phabricator.com/w/guides/dependencies/ + # provides ssh-keygen and ssh, these are needed to sync ssh repositories + openssh-client \ + procps \ + # @see https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=944908 + python3-pkg-resources \ + python3-pygments \ + subversion \ + && rm -rf /var/lib/apt/lists/* + +# install the PHP extensions we need +RUN set -ex; \ + \ + if command -v a2enmod; then \ + # Phabricator needs mod_rewrite for rewritting to index.php + a2enmod rewrite; \ + fi; \ + \ + savedAptMark="$(apt-mark showmanual)"; \ + \ + apt-get update; \ + apt-get install -y --no-install-recommends \ + libcurl4-gnutls-dev \ + libfreetype6-dev \ + libjpeg62-turbo-dev \ + libonig-dev \ + libpng-dev \ + libzip-dev \ + libzip4 \ + ; \ + \ + docker-php-ext-configure gd \ + --with-jpeg=/usr \ + --with-freetype=/usr \ + ; \ + \ + docker-php-ext-install -j "$(nproc)" \ + gd \ + opcache \ + mbstring \ + iconv \ + mysqli \ + curl \ + pcntl \ + zip \ + ; \ + \ + # reset apt-mark's "manual" list so that "purge --auto-remove" will remove all build dependencies + apt-mark auto '.*' > /dev/null; \ + apt-mark manual ${savedAptMark}; \ + ldd "$(php -r 'echo ini_get("extension_dir");')"/*.so \ + | awk '/=>/ { print $3 }' \ + | sort -u \ + | xargs -r dpkg-query -S \ + | cut -d: -f1 \ + | sort -u \ + | xargs -rt apt-mark manual; \ + \ + apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false; \ + rm -rf /var/lib/apt/lists/* + +RUN pecl channel-update pecl.php.net \ + && pecl install apcu \ + && docker-php-ext-enable apcu + +# set recommended PHP.ini settings +# see https://secure.php.net/manual/en/opcache.installation.php +RUN { \ + echo 'opcache.memory_consumption=128'; \ + echo 'opcache.interned_strings_buffer=8'; \ + echo 'opcache.max_accelerated_files=4000'; \ + echo 'opcache.revalidate_freq=60'; \ + echo 'opcache.fast_shutdown=1'; \ + # From Phabricator + echo 'opcache.validate_timestamps=0'; \ + } > /usr/local/etc/php/conf.d/opcache-recommended.ini + +# Set the default timezone. +RUN { \ + echo 'date.timezone="UTC"'; \ + } > /usr/local/etc/php/conf.d/timezone.ini + +# File Uploads +RUN { \ + echo 'post_max_size=32M'; \ + echo 'upload_max_filesize=32M'; \ + } > /usr/local/etc/php/conf.d/uploads.ini + +# Repository Folder. +RUN mkdir /var/repo \ + && chown www-data:www-data /var/repo + +# Data Folder. +RUN mkdir /var/data \ + && chown www-data:www-data /var/data + +##### Start Phabricator +RUN { \ + echo ''; \ + echo ' RewriteEngine on'; \ + echo ' RewriteRule ^(.*)$ /index.php?__path__=$1 [B,L,QSA]'; \ + echo ' LimitRequestBody 33554432'; \ + echo ''; \ + } > /etc/apache2/sites-available/000-default.conf +##### End Phabricator + +# Clone phabricator +COPY ./ /opt/phabricator +RUN git clone https://github.com/phacility/arcanist /opt/arcanist + +WORKDIR /opt/phabricator + +##### Start Phabricator +RUN rmdir /var/www/html; \ + ln -sf /opt/phabricator/webroot /var/www/html; +##### End Phabricator + +ENV PATH "$PATH:/opt/phabricator/bin" diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000000..98c1f5d26e --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,22 @@ +services: + phabricator: + build: ./ + image: phabricator/phabricator + volumes: + - config:/opt/phabricator/conf/local + - repo:/var/repo + ports: + - 8888:80 + links: + - database + database: + image: mariadb:10.5 + volumes: + - db-data:/var/lib/mysql + - ./mysql.cnf:/etc/mysql/conf.d/mysql.cnf:ro + environment: + MYSQL_ROOT_PASSWORD: CHANGEME +volumes: + config: + repo: + db-data: diff --git a/externals/phpmailer/class.smtp.php b/externals/phpmailer/class.smtp.php index c2ca1cb3b8..91050e13cf 100644 --- a/externals/phpmailer/class.smtp.php +++ b/externals/phpmailer/class.smtp.php @@ -385,7 +385,7 @@ public function Data($msg_data) { $max_line_length = 998; // used below; set here for ease in change - while(list(,$line) = @each($lines)) { + foreach ($lines as $line) { $lines_out = null; if($line == "" && $in_headers) { $in_headers = false; @@ -414,12 +414,9 @@ public function Data($msg_data) { $lines_out[] = $line; // send the lines to the server - while(list(,$line_out) = @each($lines_out)) { - if(strlen($line_out) > 0) - { - if(substr($line_out, 0, 1) == ".") { - $line_out = "." . $line_out; - } + foreach ($lines_out as $line_out) { + if(strlen($line_out) > 0 && substr($line_out, 0, 1) == ".") { + $line_out = "." . $line_out; } fputs($this->smtp_conn,$line_out . $this->CRLF); } diff --git a/mysql.cnf b/mysql.cnf new file mode 100644 index 0000000000..d9d4abe742 --- /dev/null +++ b/mysql.cnf @@ -0,0 +1,5 @@ +[mysqld] +sql_mode=STRICT_ALL_TABLES +max_allowed_packet=33554432 +innodb_buffer_pool_size=1600M +local_infile=0 \ No newline at end of file diff --git a/resources/builtin/roles/fa-android.png b/resources/builtin/roles/fa-android.png new file mode 100644 index 0000000000..db56162683 Binary files /dev/null and b/resources/builtin/roles/fa-android.png differ diff --git a/resources/builtin/roles/fa-apple.png b/resources/builtin/roles/fa-apple.png new file mode 100644 index 0000000000..7e289818d1 Binary files /dev/null and b/resources/builtin/roles/fa-apple.png differ diff --git a/resources/builtin/roles/fa-beer.png b/resources/builtin/roles/fa-beer.png new file mode 100644 index 0000000000..ff5fd0646f Binary files /dev/null and b/resources/builtin/roles/fa-beer.png differ diff --git a/resources/builtin/roles/fa-bomb.png b/resources/builtin/roles/fa-bomb.png new file mode 100644 index 0000000000..bfeacfa84c Binary files /dev/null and b/resources/builtin/roles/fa-bomb.png differ diff --git a/resources/builtin/roles/fa-book.png b/resources/builtin/roles/fa-book.png new file mode 100644 index 0000000000..f6bda5d61c Binary files /dev/null and b/resources/builtin/roles/fa-book.png differ diff --git a/resources/builtin/roles/fa-briefcase.png b/resources/builtin/roles/fa-briefcase.png new file mode 100644 index 0000000000..afa70e2a3a Binary files /dev/null and b/resources/builtin/roles/fa-briefcase.png differ diff --git a/resources/builtin/roles/fa-bug.png b/resources/builtin/roles/fa-bug.png new file mode 100644 index 0000000000..83c538f885 Binary files /dev/null and b/resources/builtin/roles/fa-bug.png differ diff --git a/resources/builtin/roles/fa-building.png b/resources/builtin/roles/fa-building.png new file mode 100644 index 0000000000..d033366cf7 Binary files /dev/null and b/resources/builtin/roles/fa-building.png differ diff --git a/resources/builtin/roles/fa-calendar.png b/resources/builtin/roles/fa-calendar.png new file mode 100644 index 0000000000..80cfc3e11b Binary files /dev/null and b/resources/builtin/roles/fa-calendar.png differ diff --git a/resources/builtin/roles/fa-camera-retro.png b/resources/builtin/roles/fa-camera-retro.png new file mode 100644 index 0000000000..c1ac04d27b Binary files /dev/null and b/resources/builtin/roles/fa-camera-retro.png differ diff --git a/resources/builtin/roles/fa-chrome.png b/resources/builtin/roles/fa-chrome.png new file mode 100644 index 0000000000..177c2fe4f3 Binary files /dev/null and b/resources/builtin/roles/fa-chrome.png differ diff --git a/resources/builtin/roles/fa-cloud.png b/resources/builtin/roles/fa-cloud.png new file mode 100644 index 0000000000..ae1b855f94 Binary files /dev/null and b/resources/builtin/roles/fa-cloud.png differ diff --git a/resources/builtin/roles/fa-coffee.png b/resources/builtin/roles/fa-coffee.png new file mode 100644 index 0000000000..8d0850f0ee Binary files /dev/null and b/resources/builtin/roles/fa-coffee.png differ diff --git a/resources/builtin/roles/fa-comments.png b/resources/builtin/roles/fa-comments.png new file mode 100644 index 0000000000..927fb91bb2 Binary files /dev/null and b/resources/builtin/roles/fa-comments.png differ diff --git a/resources/builtin/roles/fa-credit-card.png b/resources/builtin/roles/fa-credit-card.png new file mode 100644 index 0000000000..c159d2b673 Binary files /dev/null and b/resources/builtin/roles/fa-credit-card.png differ diff --git a/resources/builtin/roles/fa-database.png b/resources/builtin/roles/fa-database.png new file mode 100644 index 0000000000..bafcd1e338 Binary files /dev/null and b/resources/builtin/roles/fa-database.png differ diff --git a/resources/builtin/roles/fa-desktop.png b/resources/builtin/roles/fa-desktop.png new file mode 100644 index 0000000000..f463db8e1b Binary files /dev/null and b/resources/builtin/roles/fa-desktop.png differ diff --git a/resources/builtin/roles/fa-diamond.png b/resources/builtin/roles/fa-diamond.png new file mode 100644 index 0000000000..34a68c3284 Binary files /dev/null and b/resources/builtin/roles/fa-diamond.png differ diff --git a/resources/builtin/roles/fa-empire.png b/resources/builtin/roles/fa-empire.png new file mode 100644 index 0000000000..2d9dc3a8df Binary files /dev/null and b/resources/builtin/roles/fa-empire.png differ diff --git a/resources/builtin/roles/fa-envelope.png b/resources/builtin/roles/fa-envelope.png new file mode 100644 index 0000000000..934f1587bb Binary files /dev/null and b/resources/builtin/roles/fa-envelope.png differ diff --git a/resources/builtin/roles/fa-facebook.png b/resources/builtin/roles/fa-facebook.png new file mode 100644 index 0000000000..3d5a5f1f9a Binary files /dev/null and b/resources/builtin/roles/fa-facebook.png differ diff --git a/resources/builtin/roles/fa-fax.png b/resources/builtin/roles/fa-fax.png new file mode 100644 index 0000000000..af00c31269 Binary files /dev/null and b/resources/builtin/roles/fa-fax.png differ diff --git a/resources/builtin/roles/fa-film.png b/resources/builtin/roles/fa-film.png new file mode 100644 index 0000000000..2c30ffe477 Binary files /dev/null and b/resources/builtin/roles/fa-film.png differ diff --git a/resources/builtin/roles/fa-firefox.png b/resources/builtin/roles/fa-firefox.png new file mode 100644 index 0000000000..25fd9fc739 Binary files /dev/null and b/resources/builtin/roles/fa-firefox.png differ diff --git a/resources/builtin/roles/fa-flag-checkered.png b/resources/builtin/roles/fa-flag-checkered.png new file mode 100644 index 0000000000..e8e2519b4a Binary files /dev/null and b/resources/builtin/roles/fa-flag-checkered.png differ diff --git a/resources/builtin/roles/fa-flask.png b/resources/builtin/roles/fa-flask.png new file mode 100644 index 0000000000..ce978ae7d3 Binary files /dev/null and b/resources/builtin/roles/fa-flask.png differ diff --git a/resources/builtin/roles/fa-folder.png b/resources/builtin/roles/fa-folder.png new file mode 100644 index 0000000000..f6003fcd91 Binary files /dev/null and b/resources/builtin/roles/fa-folder.png differ diff --git a/resources/builtin/roles/fa-gamepad.png b/resources/builtin/roles/fa-gamepad.png new file mode 100644 index 0000000000..db3e7df75e Binary files /dev/null and b/resources/builtin/roles/fa-gamepad.png differ diff --git a/resources/builtin/roles/fa-gears.png b/resources/builtin/roles/fa-gears.png new file mode 100644 index 0000000000..8f60e32f84 Binary files /dev/null and b/resources/builtin/roles/fa-gears.png differ diff --git a/resources/builtin/roles/fa-google.png b/resources/builtin/roles/fa-google.png new file mode 100644 index 0000000000..cd1ad99b1b Binary files /dev/null and b/resources/builtin/roles/fa-google.png differ diff --git a/resources/builtin/roles/fa-hand-peace-o.png b/resources/builtin/roles/fa-hand-peace-o.png new file mode 100644 index 0000000000..20a9181f5c Binary files /dev/null and b/resources/builtin/roles/fa-hand-peace-o.png differ diff --git a/resources/builtin/roles/fa-hashtag.png b/resources/builtin/roles/fa-hashtag.png new file mode 100644 index 0000000000..3324b19a05 Binary files /dev/null and b/resources/builtin/roles/fa-hashtag.png differ diff --git a/resources/builtin/roles/fa-heart.png b/resources/builtin/roles/fa-heart.png new file mode 100644 index 0000000000..35733cacca Binary files /dev/null and b/resources/builtin/roles/fa-heart.png differ diff --git a/resources/builtin/roles/fa-internet-explorer.png b/resources/builtin/roles/fa-internet-explorer.png new file mode 100644 index 0000000000..34d30aabd9 Binary files /dev/null and b/resources/builtin/roles/fa-internet-explorer.png differ diff --git a/resources/builtin/roles/fa-key.png b/resources/builtin/roles/fa-key.png new file mode 100644 index 0000000000..8b7f810fe5 Binary files /dev/null and b/resources/builtin/roles/fa-key.png differ diff --git a/resources/builtin/roles/fa-legal.png b/resources/builtin/roles/fa-legal.png new file mode 100644 index 0000000000..52d217f6ba Binary files /dev/null and b/resources/builtin/roles/fa-legal.png differ diff --git a/resources/builtin/roles/fa-linux.png b/resources/builtin/roles/fa-linux.png new file mode 100644 index 0000000000..e0defe6671 Binary files /dev/null and b/resources/builtin/roles/fa-linux.png differ diff --git a/resources/builtin/roles/fa-lock.png b/resources/builtin/roles/fa-lock.png new file mode 100644 index 0000000000..4c7f552c65 Binary files /dev/null and b/resources/builtin/roles/fa-lock.png differ diff --git a/resources/builtin/roles/fa-map-marker.png b/resources/builtin/roles/fa-map-marker.png new file mode 100644 index 0000000000..865175bd9a Binary files /dev/null and b/resources/builtin/roles/fa-map-marker.png differ diff --git a/resources/builtin/roles/fa-microphone.png b/resources/builtin/roles/fa-microphone.png new file mode 100644 index 0000000000..d279fe6934 Binary files /dev/null and b/resources/builtin/roles/fa-microphone.png differ diff --git a/resources/builtin/roles/fa-mobile.png b/resources/builtin/roles/fa-mobile.png new file mode 100644 index 0000000000..3147473cb7 Binary files /dev/null and b/resources/builtin/roles/fa-mobile.png differ diff --git a/resources/builtin/roles/fa-money.png b/resources/builtin/roles/fa-money.png new file mode 100644 index 0000000000..e0823759e2 Binary files /dev/null and b/resources/builtin/roles/fa-money.png differ diff --git a/resources/builtin/roles/fa-phone.png b/resources/builtin/roles/fa-phone.png new file mode 100644 index 0000000000..ad5f77913f Binary files /dev/null and b/resources/builtin/roles/fa-phone.png differ diff --git a/resources/builtin/roles/fa-pie-chart.png b/resources/builtin/roles/fa-pie-chart.png new file mode 100644 index 0000000000..23fefc60c3 Binary files /dev/null and b/resources/builtin/roles/fa-pie-chart.png differ diff --git a/resources/builtin/roles/fa-rebel.png b/resources/builtin/roles/fa-rebel.png new file mode 100644 index 0000000000..1ada36553b Binary files /dev/null and b/resources/builtin/roles/fa-rebel.png differ diff --git a/resources/builtin/roles/fa-reddit-alien.png b/resources/builtin/roles/fa-reddit-alien.png new file mode 100644 index 0000000000..897eca4f39 Binary files /dev/null and b/resources/builtin/roles/fa-reddit-alien.png differ diff --git a/resources/builtin/roles/fa-safari.png b/resources/builtin/roles/fa-safari.png new file mode 100644 index 0000000000..fc056a7388 Binary files /dev/null and b/resources/builtin/roles/fa-safari.png differ diff --git a/resources/builtin/roles/fa-search.png b/resources/builtin/roles/fa-search.png new file mode 100644 index 0000000000..e4135af53a Binary files /dev/null and b/resources/builtin/roles/fa-search.png differ diff --git a/resources/builtin/roles/fa-server.png b/resources/builtin/roles/fa-server.png new file mode 100644 index 0000000000..3369e094b7 Binary files /dev/null and b/resources/builtin/roles/fa-server.png differ diff --git a/resources/builtin/roles/fa-shopping-cart.png b/resources/builtin/roles/fa-shopping-cart.png new file mode 100644 index 0000000000..5a623e9b62 Binary files /dev/null and b/resources/builtin/roles/fa-shopping-cart.png differ diff --git a/resources/builtin/roles/fa-sitemap.png b/resources/builtin/roles/fa-sitemap.png new file mode 100644 index 0000000000..81cbe5406d Binary files /dev/null and b/resources/builtin/roles/fa-sitemap.png differ diff --git a/resources/builtin/roles/fa-star.png b/resources/builtin/roles/fa-star.png new file mode 100644 index 0000000000..02b66e7ef2 Binary files /dev/null and b/resources/builtin/roles/fa-star.png differ diff --git a/resources/builtin/roles/fa-tablet.png b/resources/builtin/roles/fa-tablet.png new file mode 100644 index 0000000000..263e50d733 Binary files /dev/null and b/resources/builtin/roles/fa-tablet.png differ diff --git a/resources/builtin/roles/fa-tag.png b/resources/builtin/roles/fa-tag.png new file mode 100644 index 0000000000..558fbc40fb Binary files /dev/null and b/resources/builtin/roles/fa-tag.png differ diff --git a/resources/builtin/roles/fa-tags.png b/resources/builtin/roles/fa-tags.png new file mode 100644 index 0000000000..a491bdefbe Binary files /dev/null and b/resources/builtin/roles/fa-tags.png differ diff --git a/resources/builtin/roles/fa-trash-o.png b/resources/builtin/roles/fa-trash-o.png new file mode 100644 index 0000000000..03c0f0362a Binary files /dev/null and b/resources/builtin/roles/fa-trash-o.png differ diff --git a/resources/builtin/roles/fa-truck.png b/resources/builtin/roles/fa-truck.png new file mode 100644 index 0000000000..bb67c3157a Binary files /dev/null and b/resources/builtin/roles/fa-truck.png differ diff --git a/resources/builtin/roles/fa-twitter.png b/resources/builtin/roles/fa-twitter.png new file mode 100644 index 0000000000..5dd349420c Binary files /dev/null and b/resources/builtin/roles/fa-twitter.png differ diff --git a/resources/builtin/roles/fa-umbrella.png b/resources/builtin/roles/fa-umbrella.png new file mode 100644 index 0000000000..5369f45840 Binary files /dev/null and b/resources/builtin/roles/fa-umbrella.png differ diff --git a/resources/builtin/roles/fa-university.png b/resources/builtin/roles/fa-university.png new file mode 100644 index 0000000000..5d523182e7 Binary files /dev/null and b/resources/builtin/roles/fa-university.png differ diff --git a/resources/builtin/roles/fa-user-secret.png b/resources/builtin/roles/fa-user-secret.png new file mode 100644 index 0000000000..c84267103c Binary files /dev/null and b/resources/builtin/roles/fa-user-secret.png differ diff --git a/resources/builtin/roles/fa-user.png b/resources/builtin/roles/fa-user.png new file mode 100644 index 0000000000..62c77a6729 Binary files /dev/null and b/resources/builtin/roles/fa-user.png differ diff --git a/resources/builtin/roles/fa-users.png b/resources/builtin/roles/fa-users.png new file mode 100644 index 0000000000..93d0a2f8bc Binary files /dev/null and b/resources/builtin/roles/fa-users.png differ diff --git a/resources/builtin/roles/fa-warning.png b/resources/builtin/roles/fa-warning.png new file mode 100644 index 0000000000..a950cb7e8e Binary files /dev/null and b/resources/builtin/roles/fa-warning.png differ diff --git a/resources/builtin/roles/fa-wheelchair.png b/resources/builtin/roles/fa-wheelchair.png new file mode 100644 index 0000000000..602f744a46 Binary files /dev/null and b/resources/builtin/roles/fa-wheelchair.png differ diff --git a/resources/builtin/roles/fa-windows.png b/resources/builtin/roles/fa-windows.png new file mode 100644 index 0000000000..5e7c6e6acf Binary files /dev/null and b/resources/builtin/roles/fa-windows.png differ diff --git a/resources/builtin/roles/v3/archive.png b/resources/builtin/roles/v3/archive.png new file mode 100644 index 0000000000..77ff79e555 Binary files /dev/null and b/resources/builtin/roles/v3/archive.png differ diff --git a/resources/builtin/roles/v3/art.png b/resources/builtin/roles/v3/art.png new file mode 100644 index 0000000000..9c684469cb Binary files /dev/null and b/resources/builtin/roles/v3/art.png differ diff --git a/resources/builtin/roles/v3/basic-book.png b/resources/builtin/roles/v3/basic-book.png new file mode 100644 index 0000000000..b2a6d99415 Binary files /dev/null and b/resources/builtin/roles/v3/basic-book.png differ diff --git a/resources/builtin/roles/v3/book.png b/resources/builtin/roles/v3/book.png new file mode 100644 index 0000000000..eceb0bfb4d Binary files /dev/null and b/resources/builtin/roles/v3/book.png differ diff --git a/resources/builtin/roles/v3/briefcase.png b/resources/builtin/roles/v3/briefcase.png new file mode 100644 index 0000000000..c6abf5ed42 Binary files /dev/null and b/resources/builtin/roles/v3/briefcase.png differ diff --git a/resources/builtin/roles/v3/bug.png b/resources/builtin/roles/v3/bug.png new file mode 100644 index 0000000000..bb2948a93a Binary files /dev/null and b/resources/builtin/roles/v3/bug.png differ diff --git a/resources/builtin/roles/v3/calendar.png b/resources/builtin/roles/v3/calendar.png new file mode 100644 index 0000000000..6ebdc2e08c Binary files /dev/null and b/resources/builtin/roles/v3/calendar.png differ diff --git a/resources/builtin/roles/v3/clipboard.png b/resources/builtin/roles/v3/clipboard.png new file mode 100644 index 0000000000..60e2acd4f4 Binary files /dev/null and b/resources/builtin/roles/v3/clipboard.png differ diff --git a/resources/builtin/roles/v3/cloud.png b/resources/builtin/roles/v3/cloud.png new file mode 100644 index 0000000000..efb644001f Binary files /dev/null and b/resources/builtin/roles/v3/cloud.png differ diff --git a/resources/builtin/roles/v3/code.png b/resources/builtin/roles/v3/code.png new file mode 100644 index 0000000000..b7af59cfbb Binary files /dev/null and b/resources/builtin/roles/v3/code.png differ diff --git a/resources/builtin/roles/v3/contact.png b/resources/builtin/roles/v3/contact.png new file mode 100644 index 0000000000..6b3095dc3d Binary files /dev/null and b/resources/builtin/roles/v3/contact.png differ diff --git a/resources/builtin/roles/v3/creditcard.png b/resources/builtin/roles/v3/creditcard.png new file mode 100644 index 0000000000..d231c9437d Binary files /dev/null and b/resources/builtin/roles/v3/creditcard.png differ diff --git a/resources/builtin/roles/v3/database.png b/resources/builtin/roles/v3/database.png new file mode 100644 index 0000000000..9e44c0ec58 Binary files /dev/null and b/resources/builtin/roles/v3/database.png differ diff --git a/resources/builtin/roles/v3/desktop.png b/resources/builtin/roles/v3/desktop.png new file mode 100644 index 0000000000..cf6f80eeaf Binary files /dev/null and b/resources/builtin/roles/v3/desktop.png differ diff --git a/resources/builtin/roles/v3/discussion.png b/resources/builtin/roles/v3/discussion.png new file mode 100644 index 0000000000..e4519d664f Binary files /dev/null and b/resources/builtin/roles/v3/discussion.png differ diff --git a/resources/builtin/roles/v3/download.png b/resources/builtin/roles/v3/download.png new file mode 100644 index 0000000000..f086222212 Binary files /dev/null and b/resources/builtin/roles/v3/download.png differ diff --git a/resources/builtin/roles/v3/experimental.png b/resources/builtin/roles/v3/experimental.png new file mode 100644 index 0000000000..5bb05ac100 Binary files /dev/null and b/resources/builtin/roles/v3/experimental.png differ diff --git a/resources/builtin/roles/v3/flag.png b/resources/builtin/roles/v3/flag.png new file mode 100644 index 0000000000..c7d2563115 Binary files /dev/null and b/resources/builtin/roles/v3/flag.png differ diff --git a/resources/builtin/roles/v3/folder.png b/resources/builtin/roles/v3/folder.png new file mode 100644 index 0000000000..cac0f9fbfa Binary files /dev/null and b/resources/builtin/roles/v3/folder.png differ diff --git a/resources/builtin/roles/v3/gears.png b/resources/builtin/roles/v3/gears.png new file mode 100644 index 0000000000..ecbef1a9cf Binary files /dev/null and b/resources/builtin/roles/v3/gears.png differ diff --git a/resources/builtin/roles/v3/gold.png b/resources/builtin/roles/v3/gold.png new file mode 100644 index 0000000000..630f45bc61 Binary files /dev/null and b/resources/builtin/roles/v3/gold.png differ diff --git a/resources/builtin/roles/v3/home.png b/resources/builtin/roles/v3/home.png new file mode 100644 index 0000000000..808d639f0d Binary files /dev/null and b/resources/builtin/roles/v3/home.png differ diff --git a/resources/builtin/roles/v3/library.png b/resources/builtin/roles/v3/library.png new file mode 100644 index 0000000000..1f9ea1a64a Binary files /dev/null and b/resources/builtin/roles/v3/library.png differ diff --git a/resources/builtin/roles/v3/lightbulb.png b/resources/builtin/roles/v3/lightbulb.png new file mode 100644 index 0000000000..1aba0d32f3 Binary files /dev/null and b/resources/builtin/roles/v3/lightbulb.png differ diff --git a/resources/builtin/roles/v3/lock.png b/resources/builtin/roles/v3/lock.png new file mode 100644 index 0000000000..839cb1e5bf Binary files /dev/null and b/resources/builtin/roles/v3/lock.png differ diff --git a/resources/builtin/roles/v3/mail.png b/resources/builtin/roles/v3/mail.png new file mode 100644 index 0000000000..17f91ea881 Binary files /dev/null and b/resources/builtin/roles/v3/mail.png differ diff --git a/resources/builtin/roles/v3/manage.png b/resources/builtin/roles/v3/manage.png new file mode 100644 index 0000000000..7804360a29 Binary files /dev/null and b/resources/builtin/roles/v3/manage.png differ diff --git a/resources/builtin/roles/v3/marker.png b/resources/builtin/roles/v3/marker.png new file mode 100644 index 0000000000..c2c753a006 Binary files /dev/null and b/resources/builtin/roles/v3/marker.png differ diff --git a/resources/builtin/roles/v3/mobile.png b/resources/builtin/roles/v3/mobile.png new file mode 100644 index 0000000000..fbb1985015 Binary files /dev/null and b/resources/builtin/roles/v3/mobile.png differ diff --git a/resources/builtin/roles/v3/one-server.png b/resources/builtin/roles/v3/one-server.png new file mode 100644 index 0000000000..d8fbff8a17 Binary files /dev/null and b/resources/builtin/roles/v3/one-server.png differ diff --git a/resources/builtin/roles/v3/organization.png b/resources/builtin/roles/v3/organization.png new file mode 100644 index 0000000000..1957dd8e68 Binary files /dev/null and b/resources/builtin/roles/v3/organization.png differ diff --git a/resources/builtin/roles/v3/people.png b/resources/builtin/roles/v3/people.png new file mode 100644 index 0000000000..5bb42656df Binary files /dev/null and b/resources/builtin/roles/v3/people.png differ diff --git a/resources/builtin/roles/v3/piechart.png b/resources/builtin/roles/v3/piechart.png new file mode 100644 index 0000000000..4c707ed6ff Binary files /dev/null and b/resources/builtin/roles/v3/piechart.png differ diff --git a/resources/builtin/roles/v3/police-badge.png b/resources/builtin/roles/v3/police-badge.png new file mode 100644 index 0000000000..8b729bc35a Binary files /dev/null and b/resources/builtin/roles/v3/police-badge.png differ diff --git a/resources/builtin/roles/v3/purchase-order.png b/resources/builtin/roles/v3/purchase-order.png new file mode 100644 index 0000000000..906d72a529 Binary files /dev/null and b/resources/builtin/roles/v3/purchase-order.png differ diff --git a/resources/builtin/roles/v3/robot.png b/resources/builtin/roles/v3/robot.png new file mode 100644 index 0000000000..317544f2dd Binary files /dev/null and b/resources/builtin/roles/v3/robot.png differ diff --git a/resources/builtin/roles/v3/rocket.png b/resources/builtin/roles/v3/rocket.png new file mode 100644 index 0000000000..f5a758ff81 Binary files /dev/null and b/resources/builtin/roles/v3/rocket.png differ diff --git a/resources/builtin/roles/v3/server-documentation.png b/resources/builtin/roles/v3/server-documentation.png new file mode 100644 index 0000000000..d71dd53b72 Binary files /dev/null and b/resources/builtin/roles/v3/server-documentation.png differ diff --git a/resources/builtin/roles/v3/servers.png b/resources/builtin/roles/v3/servers.png new file mode 100644 index 0000000000..df20dd6492 Binary files /dev/null and b/resources/builtin/roles/v3/servers.png differ diff --git a/resources/builtin/roles/v3/shield.png b/resources/builtin/roles/v3/shield.png new file mode 100644 index 0000000000..fd938307fd Binary files /dev/null and b/resources/builtin/roles/v3/shield.png differ diff --git a/resources/builtin/roles/v3/silver.png b/resources/builtin/roles/v3/silver.png new file mode 100644 index 0000000000..afe0e84b35 Binary files /dev/null and b/resources/builtin/roles/v3/silver.png differ diff --git a/resources/builtin/roles/v3/sitemap.png b/resources/builtin/roles/v3/sitemap.png new file mode 100644 index 0000000000..8ee6e232b4 Binary files /dev/null and b/resources/builtin/roles/v3/sitemap.png differ diff --git a/resources/builtin/roles/v3/support.png b/resources/builtin/roles/v3/support.png new file mode 100644 index 0000000000..88a9746708 Binary files /dev/null and b/resources/builtin/roles/v3/support.png differ diff --git a/resources/builtin/roles/v3/sword.png b/resources/builtin/roles/v3/sword.png new file mode 100644 index 0000000000..30040633fd Binary files /dev/null and b/resources/builtin/roles/v3/sword.png differ diff --git a/resources/builtin/roles/v3/tag.png b/resources/builtin/roles/v3/tag.png new file mode 100644 index 0000000000..c91bac473a Binary files /dev/null and b/resources/builtin/roles/v3/tag.png differ diff --git a/resources/builtin/roles/v3/three-servers.png b/resources/builtin/roles/v3/three-servers.png new file mode 100644 index 0000000000..3361d8967b Binary files /dev/null and b/resources/builtin/roles/v3/three-servers.png differ diff --git a/resources/builtin/roles/v3/trash.png b/resources/builtin/roles/v3/trash.png new file mode 100644 index 0000000000..ce3f557d44 Binary files /dev/null and b/resources/builtin/roles/v3/trash.png differ diff --git a/resources/builtin/roles/v3/truck.png b/resources/builtin/roles/v3/truck.png new file mode 100644 index 0000000000..3c903ea58b Binary files /dev/null and b/resources/builtin/roles/v3/truck.png differ diff --git a/resources/builtin/roles/v3/two-servers.png b/resources/builtin/roles/v3/two-servers.png new file mode 100644 index 0000000000..d5d408d9ad Binary files /dev/null and b/resources/builtin/roles/v3/two-servers.png differ diff --git a/resources/builtin/roles/v3/umbrella.png b/resources/builtin/roles/v3/umbrella.png new file mode 100644 index 0000000000..98c7c12365 Binary files /dev/null and b/resources/builtin/roles/v3/umbrella.png differ diff --git a/resources/builtin/roles/v3/upload.png b/resources/builtin/roles/v3/upload.png new file mode 100644 index 0000000000..13e33507bb Binary files /dev/null and b/resources/builtin/roles/v3/upload.png differ diff --git a/resources/builtin/roles/v3/wand.png b/resources/builtin/roles/v3/wand.png new file mode 100644 index 0000000000..6de1cb55ab Binary files /dev/null and b/resources/builtin/roles/v3/wand.png differ diff --git a/resources/celerity/map.php b/resources/celerity/map.php index c29df70b1e..97b73351ff 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -7,273 +7,280 @@ */ return array( 'names' => array( - 'conpherence.pkg.css' => '0e3cf785', - 'conpherence.pkg.js' => '020aebcf', - 'core.pkg.css' => 'b816811e', - 'core.pkg.js' => 'd2de90d9', - 'dark-console.pkg.js' => '187792c2', - 'differential.pkg.css' => 'ffb69e3d', - 'differential.pkg.js' => 'c60bec1b', - 'diffusion.pkg.css' => '42c75c37', - 'diffusion.pkg.js' => '78c9885d', - 'maniphest.pkg.css' => '35995d6d', - 'maniphest.pkg.js' => 'c9308721', + 'conpherence.pkg.css' => '4417220b', + 'conpherence.pkg.js' => '3072815e', + 'core.pkg.css' => '56179aa6', + 'core.pkg.js' => 'e41d1fc7', + 'dark-console.pkg.js' => 'f835403b', + 'differential.pkg.css' => 'd66e894a', + 'differential.pkg.js' => '89ff42ec', + 'diffusion.pkg.css' => '1c5970b2', + 'diffusion.pkg.js' => '83b9d576', + 'maniphest.pkg.css' => '543cc763', + 'maniphest.pkg.js' => 'c724b3d4', 'rsrc/audio/basic/alert.mp3' => '17889334', 'rsrc/audio/basic/bing.mp3' => 'a817a0c3', 'rsrc/audio/basic/pock.mp3' => '0fa843d0', 'rsrc/audio/basic/tap.mp3' => '02d16994', 'rsrc/audio/basic/ting.mp3' => 'a6b6540e', - 'rsrc/css/aphront/aphront-bars.css' => '4a327b4a', - 'rsrc/css/aphront/dark-console.css' => '7f06cda2', - 'rsrc/css/aphront/dialog-view.css' => '6f4ea703', - 'rsrc/css/aphront/list-filter-view.css' => 'feb64255', - 'rsrc/css/aphront/multi-column.css' => 'fbc00ba3', - 'rsrc/css/aphront/notification.css' => '30240bd2', - 'rsrc/css/aphront/panel-view.css' => '46923d46', - 'rsrc/css/aphront/phabricator-nav-view.css' => '423f92cc', - 'rsrc/css/aphront/table-view.css' => '0bb61df1', - 'rsrc/css/aphront/tokenizer.css' => '34e2a838', - 'rsrc/css/aphront/tooltip.css' => 'e3f2412f', - 'rsrc/css/aphront/typeahead-browse.css' => 'b7ed02d2', - 'rsrc/css/aphront/typeahead.css' => '8779483d', - 'rsrc/css/application/almanac/almanac.css' => '2e050f4f', - 'rsrc/css/application/auth/auth.css' => 'c2f23d74', - 'rsrc/css/application/base/main-menu-view.css' => 'bcec20f0', - 'rsrc/css/application/base/notification-menu.css' => '4df1ee30', - 'rsrc/css/application/base/phui-theme.css' => '35883b37', - 'rsrc/css/application/base/standard-page-view.css' => 'a374f94c', - 'rsrc/css/application/chatlog/chatlog.css' => 'abdc76ee', - 'rsrc/css/application/conduit/conduit-api.css' => 'ce2cfc41', - 'rsrc/css/application/config/config-options.css' => '16c920ae', - 'rsrc/css/application/config/config-template.css' => '20babf50', - 'rsrc/css/application/config/setup-issue.css' => '5eed85b2', - 'rsrc/css/application/config/unhandled-exception.css' => '9ecfc00d', - 'rsrc/css/application/conpherence/color.css' => 'b17746b0', - 'rsrc/css/application/conpherence/durable-column.css' => '2d57072b', - 'rsrc/css/application/conpherence/header-pane.css' => 'c9a3db8e', - 'rsrc/css/application/conpherence/menu.css' => '67f4680d', - 'rsrc/css/application/conpherence/message-pane.css' => 'd244db1e', - 'rsrc/css/application/conpherence/notification.css' => '6a3d4e58', - 'rsrc/css/application/conpherence/participant-pane.css' => '69e0058a', - 'rsrc/css/application/conpherence/transaction.css' => '3a3f5e7e', - 'rsrc/css/application/contentsource/content-source-view.css' => 'cdf0d579', - 'rsrc/css/application/countdown/timer.css' => 'bff8012f', - 'rsrc/css/application/daemon/bulk-job.css' => '73af99f5', - 'rsrc/css/application/dashboard/dashboard.css' => '5a205b9d', - 'rsrc/css/application/diff/diff-tree-view.css' => 'e2d3e222', - 'rsrc/css/application/diff/inline-comment-summary.css' => '81eb368d', - 'rsrc/css/application/differential/add-comment.css' => '7e5900d9', - 'rsrc/css/application/differential/changeset-view.css' => '60c3d405', - 'rsrc/css/application/differential/core.css' => '7300a73e', - 'rsrc/css/application/differential/phui-inline-comment.css' => '9863a85e', - 'rsrc/css/application/differential/revision-comment.css' => '7dbc8d1d', - 'rsrc/css/application/differential/revision-history.css' => '237a2979', - 'rsrc/css/application/differential/revision-list.css' => '93d2df7d', - 'rsrc/css/application/differential/table-of-contents.css' => 'bba788b9', - 'rsrc/css/application/diffusion/diffusion-icons.css' => '23b31a1b', - 'rsrc/css/application/diffusion/diffusion-readme.css' => 'b68a76e4', - 'rsrc/css/application/diffusion/diffusion-repository.css' => 'b89e8c6c', - 'rsrc/css/application/diffusion/diffusion.css' => 'e46232d6', - 'rsrc/css/application/feed/feed.css' => 'd8b6e3f8', - 'rsrc/css/application/files/global-drag-and-drop.css' => '1d2713a4', - 'rsrc/css/application/flag/flag.css' => '2b77be8d', - 'rsrc/css/application/harbormaster/harbormaster.css' => '8dfe16b2', - 'rsrc/css/application/herald/herald-test.css' => '7e7bbdae', - 'rsrc/css/application/herald/herald.css' => '648d39e2', - 'rsrc/css/application/maniphest/report.css' => '3d53188b', - 'rsrc/css/application/maniphest/task-edit.css' => '272daa84', - 'rsrc/css/application/maniphest/task-summary.css' => '61d1667e', - 'rsrc/css/application/objectselector/object-selector.css' => 'ee77366f', - 'rsrc/css/application/owners/owners-path-editor.css' => 'fa7c13ef', - 'rsrc/css/application/paste/paste.css' => 'b37bcd38', - 'rsrc/css/application/people/people-picture-menu-item.css' => 'fe8e07cf', - 'rsrc/css/application/people/people-profile.css' => '2ea2daa1', - 'rsrc/css/application/phame/phame.css' => 'bb442327', - 'rsrc/css/application/pholio/pholio-edit.css' => '4df55b3b', - 'rsrc/css/application/pholio/pholio-inline-comments.css' => '722b48c2', - 'rsrc/css/application/pholio/pholio.css' => '88ef5ef1', - 'rsrc/css/application/phortune/phortune-credit-card-form.css' => '3b9868a8', - 'rsrc/css/application/phortune/phortune-invoice.css' => '4436b241', - 'rsrc/css/application/phortune/phortune.css' => '508a1a5e', - 'rsrc/css/application/phrequent/phrequent.css' => 'bd79cc67', - 'rsrc/css/application/phriction/phriction-document-css.css' => '03380da0', - 'rsrc/css/application/policy/policy-edit.css' => '8794e2ed', - 'rsrc/css/application/policy/policy-transaction-detail.css' => 'c02b8384', - 'rsrc/css/application/policy/policy.css' => 'ceb56a08', - 'rsrc/css/application/ponder/ponder-view.css' => '05a09d0a', - 'rsrc/css/application/project/project-card-view.css' => 'a9f2c2dd', - 'rsrc/css/application/project/project-triggers.css' => 'cd9c8bb9', - 'rsrc/css/application/project/project-view.css' => '567858b3', - 'rsrc/css/application/search/application-search-view.css' => '0f7c06d8', - 'rsrc/css/application/search/search-results.css' => '9ea70ace', - 'rsrc/css/application/slowvote/slowvote.css' => '1694baed', - 'rsrc/css/application/tokens/tokens.css' => 'ce5a50bd', - 'rsrc/css/application/uiexample/example.css' => 'b4795059', - 'rsrc/css/core/core.css' => 'b3ebd90d', - 'rsrc/css/core/remarkup.css' => '5baa3bd9', - 'rsrc/css/core/syntax.css' => '548567f6', - 'rsrc/css/core/z-index.css' => 'ac3bfcd4', - 'rsrc/css/diviner/diviner-shared.css' => '4bd263b0', - 'rsrc/css/font/font-awesome.css' => '3883938a', - 'rsrc/css/font/font-lato.css' => '23631304', - 'rsrc/css/font/phui-font-icon-base.css' => '303c9b87', - 'rsrc/css/fuel/fuel-grid.css' => '66697240', - 'rsrc/css/fuel/fuel-handle-list.css' => '2c4cbeca', - 'rsrc/css/fuel/fuel-map.css' => 'd6e31510', - 'rsrc/css/fuel/fuel-menu.css' => '21f5d199', - 'rsrc/css/layout/phabricator-source-code-view.css' => '03d7ac28', - 'rsrc/css/phui/button/phui-button-bar.css' => 'a4aa75c4', - 'rsrc/css/phui/button/phui-button-simple.css' => '1ff278aa', - 'rsrc/css/phui/button/phui-button.css' => 'ea704902', - 'rsrc/css/phui/calendar/phui-calendar-day.css' => '9597d706', - 'rsrc/css/phui/calendar/phui-calendar-list.css' => 'ccd7e4e2', - 'rsrc/css/phui/calendar/phui-calendar-month.css' => 'cb758c42', - 'rsrc/css/phui/calendar/phui-calendar.css' => 'f11073aa', - 'rsrc/css/phui/object-item/phui-oi-big-ui.css' => 'fa74cc35', - 'rsrc/css/phui/object-item/phui-oi-color.css' => 'b517bfa0', - 'rsrc/css/phui/object-item/phui-oi-drag-ui.css' => 'da15d3dc', - 'rsrc/css/phui/object-item/phui-oi-flush-ui.css' => '490e2e2e', - 'rsrc/css/phui/object-item/phui-oi-list-view.css' => 'af98a277', - 'rsrc/css/phui/object-item/phui-oi-simple-ui.css' => '6a30fa46', - 'rsrc/css/phui/phui-action-list.css' => '1b0085b2', - 'rsrc/css/phui/phui-action-panel.css' => '6c386cbf', - 'rsrc/css/phui/phui-badge.css' => '666e25ad', - 'rsrc/css/phui/phui-basic-nav-view.css' => '56ebd66d', - 'rsrc/css/phui/phui-big-info-view.css' => '362ad37b', - 'rsrc/css/phui/phui-box.css' => '5ed3b8cb', - 'rsrc/css/phui/phui-bulk-editor.css' => '374d5e30', - 'rsrc/css/phui/phui-chart.css' => '14df9ae3', - 'rsrc/css/phui/phui-cms.css' => '8c05c41e', - 'rsrc/css/phui/phui-comment-form.css' => '68a2d99a', - 'rsrc/css/phui/phui-comment-panel.css' => 'ec4e31c0', - 'rsrc/css/phui/phui-crumbs-view.css' => '614f43cf', - 'rsrc/css/phui/phui-curtain-object-ref-view.css' => '51d93266', - 'rsrc/css/phui/phui-curtain-view.css' => '68c5efb6', - 'rsrc/css/phui/phui-document-pro.css' => 'b9613a10', - 'rsrc/css/phui/phui-document-summary.css' => 'b068eed1', - 'rsrc/css/phui/phui-document.css' => '52b748a5', - 'rsrc/css/phui/phui-feed-story.css' => 'a0c05029', - 'rsrc/css/phui/phui-fontkit.css' => '1ec937e5', - 'rsrc/css/phui/phui-form-view.css' => '01b796c0', - 'rsrc/css/phui/phui-form.css' => '1f177cb7', - 'rsrc/css/phui/phui-formation-view.css' => 'd2dec8ed', - 'rsrc/css/phui/phui-head-thing.css' => 'd7f293df', - 'rsrc/css/phui/phui-header-view.css' => '36c86a58', - 'rsrc/css/phui/phui-hovercard.css' => '6ca90fa0', - 'rsrc/css/phui/phui-icon-set-selector.css' => '7aa5f3ec', - 'rsrc/css/phui/phui-icon.css' => '4cbc684a', - 'rsrc/css/phui/phui-image-mask.css' => '62c7f4d2', - 'rsrc/css/phui/phui-info-view.css' => 'a10a909b', - 'rsrc/css/phui/phui-invisible-character-view.css' => 'c694c4a4', - 'rsrc/css/phui/phui-left-right.css' => '68513c34', - 'rsrc/css/phui/phui-lightbox.css' => '4ebf22da', - 'rsrc/css/phui/phui-list.css' => '0c04affd', - 'rsrc/css/phui/phui-object-box.css' => 'b8d7eea0', - 'rsrc/css/phui/phui-pager.css' => 'd022c7ad', - 'rsrc/css/phui/phui-pinboard-view.css' => '1f08f5d8', - 'rsrc/css/phui/phui-policy-section-view.css' => '139fdc64', - 'rsrc/css/phui/phui-property-list-view.css' => '5adf7078', - 'rsrc/css/phui/phui-remarkup-preview.css' => '91767007', - 'rsrc/css/phui/phui-segment-bar-view.css' => '5166b370', - 'rsrc/css/phui/phui-spacing.css' => 'b05cadc3', - 'rsrc/css/phui/phui-status.css' => '293b5dad', - 'rsrc/css/phui/phui-tag-view.css' => 'fb811341', - 'rsrc/css/phui/phui-timeline-view.css' => '2d32d7a9', - 'rsrc/css/phui/phui-two-column-view.css' => 'f96d319f', - 'rsrc/css/phui/workboards/phui-workboard-color.css' => 'e86de308', - 'rsrc/css/phui/workboards/phui-workboard.css' => '74fc9d98', - 'rsrc/css/phui/workboards/phui-workcard.css' => '913441b6', - 'rsrc/css/phui/workboards/phui-workpanel.css' => '3ae89b20', - 'rsrc/css/sprite-login.css' => '18b368a6', - 'rsrc/css/sprite-tokens.css' => 'f1896dc5', - 'rsrc/css/syntax/syntax-default.css' => '055fc231', - 'rsrc/externals/d3/d3.min.js' => '9d068042', + 'rsrc/css/aphront/aphront-bars.css' => '00ff5fe9', + 'rsrc/css/aphront/dark-console.css' => '38e4ea36', + 'rsrc/css/aphront/dialog-view.css' => 'a084e330', + 'rsrc/css/aphront/list-filter-view.css' => '3d222b09', + 'rsrc/css/aphront/multi-column.css' => 'b9f74f2a', + 'rsrc/css/aphront/notification.css' => '2093d822', + 'rsrc/css/aphront/panel-view.css' => '9e6903c5', + 'rsrc/css/aphront/phabricator-nav-view.css' => '763d9410', + 'rsrc/css/aphront/table-view.css' => '5a4d3a38', + 'rsrc/css/aphront/tokenizer.css' => '8775367f', + 'rsrc/css/aphront/tooltip.css' => 'ad116c35', + 'rsrc/css/aphront/typeahead-browse.css' => '1b90f79f', + 'rsrc/css/aphront/typeahead.css' => '421db43d', + 'rsrc/css/application/almanac/almanac.css' => 'cef2e49b', + 'rsrc/css/application/auth/auth.css' => '2b77b6e3', + 'rsrc/css/application/base/main-menu-view.css' => '3b66b652', + 'rsrc/css/application/base/notification-menu.css' => '02fb87e2', + 'rsrc/css/application/base/phui-theme.css' => '108e564a', + 'rsrc/css/application/base/standard-page-view.css' => '9feb4eae', + 'rsrc/css/application/chatlog/chatlog.css' => 'f0969b46', + 'rsrc/css/application/conduit/conduit-api.css' => '66faab49', + 'rsrc/css/application/config/config-options.css' => '76627af5', + 'rsrc/css/application/config/config-template.css' => '41c22f41', + 'rsrc/css/application/config/setup-issue.css' => '539e7d71', + 'rsrc/css/application/config/unhandled-exception.css' => '237a4011', + 'rsrc/css/application/conpherence/color.css' => '180cfb5f', + 'rsrc/css/application/conpherence/durable-column.css' => '310813dd', + 'rsrc/css/application/conpherence/header-pane.css' => 'fda7e0bb', + 'rsrc/css/application/conpherence/menu.css' => '0291ad8b', + 'rsrc/css/application/conpherence/message-pane.css' => '57759b60', + 'rsrc/css/application/conpherence/notification.css' => '5d16c9b4', + 'rsrc/css/application/conpherence/participant-pane.css' => 'fd95bab6', + 'rsrc/css/application/conpherence/transaction.css' => '68ccd991', + 'rsrc/css/application/contentsource/content-source-view.css' => '2b7b096d', + 'rsrc/css/application/countdown/timer.css' => 'b81584b0', + 'rsrc/css/application/daemon/bulk-job.css' => 'e82e5d63', + 'rsrc/css/application/dashboard/dashboard.css' => '0c1f90e6', + 'rsrc/css/application/diff/diff-tree-view.css' => '4463923c', + 'rsrc/css/application/diff/inline-comment-summary.css' => '232a5fae', + 'rsrc/css/application/differential/add-comment.css' => 'ddeac4fb', + 'rsrc/css/application/differential/changeset-view.css' => '9d1e8849', + 'rsrc/css/application/differential/core.css' => '948b6550', + 'rsrc/css/application/differential/phui-inline-comment.css' => '717e3483', + 'rsrc/css/application/differential/revision-comment.css' => '09d505d1', + 'rsrc/css/application/differential/revision-history.css' => '5db10da7', + 'rsrc/css/application/differential/revision-list.css' => 'a4b47824', + 'rsrc/css/application/differential/table-of-contents.css' => '29c16bdc', + 'rsrc/css/application/diffusion/diffusion-icons.css' => '77dd2265', + 'rsrc/css/application/diffusion/diffusion-readme.css' => 'd48f0924', + 'rsrc/css/application/diffusion/diffusion-repository.css' => '7b9e177a', + 'rsrc/css/application/diffusion/diffusion.css' => '4bb55f03', + 'rsrc/css/application/feed/feed.css' => 'a1631e46', + 'rsrc/css/application/files/global-drag-and-drop.css' => '013aa9f1', + 'rsrc/css/application/flag/flag.css' => '4f448b39', + 'rsrc/css/application/harbormaster/harbormaster.css' => 'c4d2c934', + 'rsrc/css/application/herald/herald-test.css' => '63d07577', + 'rsrc/css/application/herald/herald.css' => '97d92574', + 'rsrc/css/application/maniphest/report.css' => 'fee2dc7f', + 'rsrc/css/application/maniphest/task-edit.css' => '17ca7755', + 'rsrc/css/application/maniphest/task-summary.css' => '801801c3', + 'rsrc/css/application/objectselector/object-selector.css' => 'aedefb77', + 'rsrc/css/application/owners/owners-path-editor.css' => '8d54df52', + 'rsrc/css/application/paste/paste.css' => '6c541d60', + 'rsrc/css/application/people/people-picture-menu-item.css' => '0ad886aa', + 'rsrc/css/application/people/people-profile.css' => '516e6a12', + 'rsrc/css/application/phame/phame.css' => '81285d4e', + 'rsrc/css/application/pholio/pholio-edit.css' => '1fb1c5ce', + 'rsrc/css/application/pholio/pholio-inline-comments.css' => '57bfe2c5', + 'rsrc/css/application/pholio/pholio.css' => '1792b28d', + 'rsrc/css/application/phortune/phortune-credit-card-form.css' => '170d1116', + 'rsrc/css/application/phortune/phortune-invoice.css' => 'a6fe8f13', + 'rsrc/css/application/phortune/phortune.css' => '2dae92d7', + 'rsrc/css/application/phrequent/phrequent.css' => '7d5af048', + 'rsrc/css/application/phriction/phriction-document-css.css' => '176dee98', + 'rsrc/css/application/policy/policy-edit.css' => '609fdd48', + 'rsrc/css/application/policy/policy-transaction-detail.css' => '3d0b461d', + 'rsrc/css/application/policy/policy.css' => 'aee81621', + 'rsrc/css/application/ponder/ponder-view.css' => 'b51b545f', + 'rsrc/css/application/project/project-card-view.css' => '52c616b0', + 'rsrc/css/application/project/project-triggers.css' => '565be023', + 'rsrc/css/application/project/project-view.css' => '87ba8a77', + 'rsrc/css/application/search/application-search-view.css' => '1ce21548', + 'rsrc/css/application/search/search-results.css' => 'a501eb57', + 'rsrc/css/application/slowvote/slowvote.css' => 'a95abbfe', + 'rsrc/css/application/timetracker/images/ui-icons_444444_256x240.png' => '8e52e7b0', + 'rsrc/css/application/timetracker/images/ui-icons_555555_256x240.png' => '00746f17', + 'rsrc/css/application/timetracker/images/ui-icons_777620_256x240.png' => '867f3d0f', + 'rsrc/css/application/timetracker/images/ui-icons_777777_256x240.png' => '8278fc72', + 'rsrc/css/application/timetracker/images/ui-icons_cc0000_256x240.png' => 'a6070abe', + 'rsrc/css/application/timetracker/images/ui-icons_ffffff_256x240.png' => '386ec878', + 'rsrc/css/application/timetracker/jquery-ui.css' => 'c686b2c7', + 'rsrc/css/application/tokens/tokens.css' => 'e60c1cd7', + 'rsrc/css/application/uiexample/example.css' => '3bb522bb', + 'rsrc/css/core/core.css' => 'bc564b67', + 'rsrc/css/core/remarkup.css' => '86c7abd9', + 'rsrc/css/core/syntax.css' => '66087f6a', + 'rsrc/css/core/z-index.css' => 'f78d61f3', + 'rsrc/css/diviner/diviner-shared.css' => '6d533b5e', + 'rsrc/css/font/font-awesome.css' => '067fc518', + 'rsrc/css/font/font-lato.css' => 'ae392e67', + 'rsrc/css/font/phui-font-icon-base.css' => 'f1a6c60c', + 'rsrc/css/fuel/fuel-grid.css' => 'a82e3be1', + 'rsrc/css/fuel/fuel-handle-list.css' => '7284f3f8', + 'rsrc/css/fuel/fuel-map.css' => 'e08255b0', + 'rsrc/css/fuel/fuel-menu.css' => '8c4660dd', + 'rsrc/css/layout/phabricator-source-code-view.css' => 'ea851bff', + 'rsrc/css/phui/button/phui-button-bar.css' => '103bddc5', + 'rsrc/css/phui/button/phui-button-simple.css' => 'bf9b167e', + 'rsrc/css/phui/button/phui-button.css' => 'dca27a4d', + 'rsrc/css/phui/calendar/phui-calendar-day.css' => 'd17a124a', + 'rsrc/css/phui/calendar/phui-calendar-list.css' => '70e6796e', + 'rsrc/css/phui/calendar/phui-calendar-month.css' => 'e85ac216', + 'rsrc/css/phui/calendar/phui-calendar.css' => '7990005c', + 'rsrc/css/phui/object-item/phui-oi-big-ui.css' => 'bc69e075', + 'rsrc/css/phui/object-item/phui-oi-color.css' => '993aa3c0', + 'rsrc/css/phui/object-item/phui-oi-drag-ui.css' => '272870cf', + 'rsrc/css/phui/object-item/phui-oi-flush-ui.css' => '64b01337', + 'rsrc/css/phui/object-item/phui-oi-list-view.css' => 'dc4d3fe5', + 'rsrc/css/phui/object-item/phui-oi-simple-ui.css' => '17656c73', + 'rsrc/css/phui/phui-action-list.css' => '712619b3', + 'rsrc/css/phui/phui-action-panel.css' => 'a6750bff', + 'rsrc/css/phui/phui-badge.css' => '65615ada', + 'rsrc/css/phui/phui-basic-nav-view.css' => 'fb533658', + 'rsrc/css/phui/phui-big-info-view.css' => '6d8006d9', + 'rsrc/css/phui/phui-box.css' => '34a585e9', + 'rsrc/css/phui/phui-bulk-editor.css' => 'd4a48524', + 'rsrc/css/phui/phui-chart.css' => '0d9514c3', + 'rsrc/css/phui/phui-cms.css' => '6982140b', + 'rsrc/css/phui/phui-comment-form.css' => '1a60a95e', + 'rsrc/css/phui/phui-comment-panel.css' => 'af171e27', + 'rsrc/css/phui/phui-crumbs-view.css' => '824b38f8', + 'rsrc/css/phui/phui-curtain-object-ref-view.css' => 'c5cc2a97', + 'rsrc/css/phui/phui-curtain-view.css' => '82abe39e', + 'rsrc/css/phui/phui-document-pro.css' => '8b6d139b', + 'rsrc/css/phui/phui-document-summary.css' => '4281c81c', + 'rsrc/css/phui/phui-document.css' => 'd4bf2736', + 'rsrc/css/phui/phui-feed-story.css' => '1c892ee8', + 'rsrc/css/phui/phui-fontkit.css' => 'cbc011de', + 'rsrc/css/phui/phui-form-view.css' => '8c117351', + 'rsrc/css/phui/phui-form.css' => '95fe0ad3', + 'rsrc/css/phui/phui-formation-view.css' => 'a1c04196', + 'rsrc/css/phui/phui-head-thing.css' => '8cbc3e5a', + 'rsrc/css/phui/phui-header-view.css' => 'eed2b7dc', + 'rsrc/css/phui/phui-hovercard.css' => '9e537a9d', + 'rsrc/css/phui/phui-icon-set-selector.css' => '1f214f73', + 'rsrc/css/phui/phui-icon.css' => '84e6d519', + 'rsrc/css/phui/phui-image-mask.css' => '58416b6b', + 'rsrc/css/phui/phui-info-view.css' => '9cf36210', + 'rsrc/css/phui/phui-invisible-character-view.css' => '67fac570', + 'rsrc/css/phui/phui-left-right.css' => 'bc561f76', + 'rsrc/css/phui/phui-lightbox.css' => 'a60db19d', + 'rsrc/css/phui/phui-list.css' => '56c65dfe', + 'rsrc/css/phui/phui-object-box.css' => 'da097b4d', + 'rsrc/css/phui/phui-pager.css' => '67f21790', + 'rsrc/css/phui/phui-pinboard-view.css' => '9da87545', + 'rsrc/css/phui/phui-policy-section-view.css' => '724c4b80', + 'rsrc/css/phui/phui-property-list-view.css' => 'eaadaa72', + 'rsrc/css/phui/phui-remarkup-preview.css' => '4716e81e', + 'rsrc/css/phui/phui-segment-bar-view.css' => 'ed889b90', + 'rsrc/css/phui/phui-spacing.css' => '660205d0', + 'rsrc/css/phui/phui-status.css' => '63a83125', + 'rsrc/css/phui/phui-tag-view.css' => '0773e57c', + 'rsrc/css/phui/phui-timeline-view.css' => '6f1d1ae7', + 'rsrc/css/phui/phui-two-column-view.css' => 'c5ff8d17', + 'rsrc/css/phui/workboards/phui-workboard-color.css' => 'dcea01dd', + 'rsrc/css/phui/workboards/phui-workboard.css' => '5179d214', + 'rsrc/css/phui/workboards/phui-workcard.css' => 'e01ca2e0', + 'rsrc/css/phui/workboards/phui-workpanel.css' => 'f6b9fd3a', + 'rsrc/css/sprite-login.css' => '511a7d30', + 'rsrc/css/sprite-tokens.css' => '8fd81966', + 'rsrc/css/syntax/syntax-default.css' => 'f1d67c7e', + 'rsrc/externals/d3/d3.min.js' => '091f6716', 'rsrc/externals/font/fontawesome/fontawesome-webfont.eot' => '23f8c698', 'rsrc/externals/font/fontawesome/fontawesome-webfont.ttf' => '70983df0', 'rsrc/externals/font/fontawesome/fontawesome-webfont.woff' => 'cd02f93b', 'rsrc/externals/font/fontawesome/fontawesome-webfont.woff2' => '351fd46a', 'rsrc/externals/font/lato/lato-bold.eot' => '7367aa5e', - 'rsrc/externals/font/lato/lato-bold.svg' => '681aa4f5', + 'rsrc/externals/font/lato/lato-bold.svg' => '8e2b6b44', 'rsrc/externals/font/lato/lato-bold.ttf' => '66d3c296', 'rsrc/externals/font/lato/lato-bold.woff' => '89d9fba7', 'rsrc/externals/font/lato/lato-bold.woff2' => '389fcdb1', 'rsrc/externals/font/lato/lato-bolditalic.eot' => '03eeb4da', - 'rsrc/externals/font/lato/lato-bolditalic.svg' => 'f56fa11c', + 'rsrc/externals/font/lato/lato-bolditalic.svg' => '3f6051f5', 'rsrc/externals/font/lato/lato-bolditalic.ttf' => '9c3aec21', 'rsrc/externals/font/lato/lato-bolditalic.woff' => 'bfbd0616', 'rsrc/externals/font/lato/lato-bolditalic.woff2' => 'bc7d1274', 'rsrc/externals/font/lato/lato-italic.eot' => '7db5b247', - 'rsrc/externals/font/lato/lato-italic.svg' => 'b1ae496f', + 'rsrc/externals/font/lato/lato-italic.svg' => '4d4b1da0', 'rsrc/externals/font/lato/lato-italic.ttf' => '43eed813', 'rsrc/externals/font/lato/lato-italic.woff' => 'c28975e1', 'rsrc/externals/font/lato/lato-italic.woff2' => 'fffc0d8c', 'rsrc/externals/font/lato/lato-regular.eot' => '06e0c291', - 'rsrc/externals/font/lato/lato-regular.svg' => '3ad95f53', + 'rsrc/externals/font/lato/lato-regular.svg' => '45227798', 'rsrc/externals/font/lato/lato-regular.ttf' => 'e2e9c398', 'rsrc/externals/font/lato/lato-regular.woff' => '0b13d332', 'rsrc/externals/font/lato/lato-regular.woff2' => '8f846797', - 'rsrc/externals/javelin/core/Event.js' => 'c03f2fb4', - 'rsrc/externals/javelin/core/Stratcom.js' => '0889b835', - 'rsrc/externals/javelin/core/__tests__/event-stop-and-kill.js' => '048472d2', - 'rsrc/externals/javelin/core/__tests__/install.js' => '14a7e671', - 'rsrc/externals/javelin/core/__tests__/stratcom.js' => 'a28464bb', - 'rsrc/externals/javelin/core/__tests__/util.js' => 'e29a4354', - 'rsrc/externals/javelin/core/init.js' => '98e6504a', - 'rsrc/externals/javelin/core/init_node.js' => '16961339', - 'rsrc/externals/javelin/core/install.js' => '5902260c', - 'rsrc/externals/javelin/core/util.js' => 'edb4d8c9', - 'rsrc/externals/javelin/docs/Base.js' => '5a401d7d', - 'rsrc/externals/javelin/docs/onload.js' => 'ee58fb62', - 'rsrc/externals/javelin/ext/fx/Color.js' => '78f811c9', - 'rsrc/externals/javelin/ext/fx/FX.js' => '34450586', - 'rsrc/externals/javelin/ext/reactor/core/DynVal.js' => '202a2e85', - 'rsrc/externals/javelin/ext/reactor/core/Reactor.js' => '1c850a26', - 'rsrc/externals/javelin/ext/reactor/core/ReactorNode.js' => '72960bc1', - 'rsrc/externals/javelin/ext/reactor/core/ReactorNodeCalmer.js' => '225bbb98', - 'rsrc/externals/javelin/ext/reactor/dom/RDOM.js' => '6cfa0008', - 'rsrc/externals/javelin/ext/view/HTMLView.js' => 'f8c4e135', - 'rsrc/externals/javelin/ext/view/View.js' => '289bf236', - 'rsrc/externals/javelin/ext/view/ViewInterpreter.js' => '876506b6', - 'rsrc/externals/javelin/ext/view/ViewPlaceholder.js' => 'a9942052', - 'rsrc/externals/javelin/ext/view/ViewRenderer.js' => '9aae2b66', - 'rsrc/externals/javelin/ext/view/ViewVisitor.js' => '308f9fe4', - 'rsrc/externals/javelin/ext/view/__tests__/HTMLView.js' => '6e50a13f', - 'rsrc/externals/javelin/ext/view/__tests__/View.js' => 'd284be5d', - 'rsrc/externals/javelin/ext/view/__tests__/ViewInterpreter.js' => 'a9f35511', - 'rsrc/externals/javelin/ext/view/__tests__/ViewRenderer.js' => '3a1b81f6', - 'rsrc/externals/javelin/lib/Cookie.js' => '05d290ef', - 'rsrc/externals/javelin/lib/DOM.js' => 'e4c7622a', - 'rsrc/externals/javelin/lib/History.js' => '030b4f7a', - 'rsrc/externals/javelin/lib/JSON.js' => '541f81c3', - 'rsrc/externals/javelin/lib/Leader.js' => '0d2490ce', - 'rsrc/externals/javelin/lib/Mask.js' => '7c4d8998', - 'rsrc/externals/javelin/lib/Quicksand.js' => 'd3799cb4', - 'rsrc/externals/javelin/lib/Request.js' => '84e6891f', - 'rsrc/externals/javelin/lib/Resource.js' => '20514cc2', - 'rsrc/externals/javelin/lib/Routable.js' => '6a18c42e', - 'rsrc/externals/javelin/lib/Router.js' => '32755edb', - 'rsrc/externals/javelin/lib/Scrollbar.js' => 'a43ae2ae', - 'rsrc/externals/javelin/lib/Sound.js' => 'd4cc2d2a', - 'rsrc/externals/javelin/lib/URI.js' => '2e255291', - 'rsrc/externals/javelin/lib/Vector.js' => 'e9c80beb', - 'rsrc/externals/javelin/lib/WebSocket.js' => 'fdc13e4e', - 'rsrc/externals/javelin/lib/Workflow.js' => '945ff654', - 'rsrc/externals/javelin/lib/__tests__/Cookie.js' => 'ca686f71', - 'rsrc/externals/javelin/lib/__tests__/DOM.js' => '4566e249', - 'rsrc/externals/javelin/lib/__tests__/JSON.js' => '710377ae', - 'rsrc/externals/javelin/lib/__tests__/URI.js' => '6fff0c2b', - 'rsrc/externals/javelin/lib/__tests__/behavior.js' => '8426ebeb', - 'rsrc/externals/javelin/lib/behavior.js' => '1b6acc2a', - 'rsrc/externals/javelin/lib/control/tokenizer/Tokenizer.js' => '89a1ae3a', - 'rsrc/externals/javelin/lib/control/typeahead/Typeahead.js' => 'a4356cde', - 'rsrc/externals/javelin/lib/control/typeahead/normalizer/TypeaheadNormalizer.js' => 'a241536a', - 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadCompositeSource.js' => '22ee68a5', - 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadOnDemandSource.js' => '23387297', - 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadPreloadedSource.js' => '5a79f6c3', - 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadSource.js' => '8badee71', - 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadStaticSource.js' => '80bff3af', + 'rsrc/externals/javelin/core/Event.js' => 'ce95eb52', + 'rsrc/externals/javelin/core/Stratcom.js' => '3e63c3da', + 'rsrc/externals/javelin/core/__tests__/event-stop-and-kill.js' => '65d00189', + 'rsrc/externals/javelin/core/__tests__/install.js' => 'e93a26e0', + 'rsrc/externals/javelin/core/__tests__/stratcom.js' => 'a1486c2c', + 'rsrc/externals/javelin/core/__tests__/util.js' => '51b598b2', + 'rsrc/externals/javelin/core/init.js' => '723d06f9', + 'rsrc/externals/javelin/core/init_node.js' => 'fa41ef33', + 'rsrc/externals/javelin/core/install.js' => 'dde695a1', + 'rsrc/externals/javelin/core/util.js' => '4ff621eb', + 'rsrc/externals/javelin/docs/Base.js' => 'd89a851e', + 'rsrc/externals/javelin/docs/onload.js' => 'ce0ade42', + 'rsrc/externals/javelin/ext/fx/Color.js' => '54514c9a', + 'rsrc/externals/javelin/ext/fx/FX.js' => '7dbf81e3', + 'rsrc/externals/javelin/ext/reactor/core/DynVal.js' => 'cda4ac41', + 'rsrc/externals/javelin/ext/reactor/core/Reactor.js' => '50c03f43', + 'rsrc/externals/javelin/ext/reactor/core/ReactorNode.js' => '27e4ebfa', + 'rsrc/externals/javelin/ext/reactor/core/ReactorNodeCalmer.js' => '1a7c0789', + 'rsrc/externals/javelin/ext/reactor/dom/RDOM.js' => 'acd57fb7', + 'rsrc/externals/javelin/ext/view/HTMLView.js' => '395db5df', + 'rsrc/externals/javelin/ext/view/View.js' => '1c4dfd8f', + 'rsrc/externals/javelin/ext/view/ViewInterpreter.js' => 'fb72774b', + 'rsrc/externals/javelin/ext/view/ViewPlaceholder.js' => '71e022dc', + 'rsrc/externals/javelin/ext/view/ViewRenderer.js' => '7b5bd01c', + 'rsrc/externals/javelin/ext/view/ViewVisitor.js' => 'b6b17353', + 'rsrc/externals/javelin/ext/view/__tests__/HTMLView.js' => 'bd300581', + 'rsrc/externals/javelin/ext/view/__tests__/View.js' => '5f792e9d', + 'rsrc/externals/javelin/ext/view/__tests__/ViewInterpreter.js' => '72a8d679', + 'rsrc/externals/javelin/ext/view/__tests__/ViewRenderer.js' => '261e180c', + 'rsrc/externals/javelin/lib/Cookie.js' => '0a842c5a', + 'rsrc/externals/javelin/lib/DOM.js' => '4c7e6cfb', + 'rsrc/externals/javelin/lib/History.js' => '09ffdb7a', + 'rsrc/externals/javelin/lib/JSON.js' => '3074b5a2', + 'rsrc/externals/javelin/lib/Leader.js' => 'b10edcf4', + 'rsrc/externals/javelin/lib/Mask.js' => '03bfa877', + 'rsrc/externals/javelin/lib/Quicksand.js' => '0ad7e653', + 'rsrc/externals/javelin/lib/Request.js' => 'c34271ef', + 'rsrc/externals/javelin/lib/Resource.js' => '290e1d73', + 'rsrc/externals/javelin/lib/Routable.js' => 'ee039e34', + 'rsrc/externals/javelin/lib/Router.js' => 'a713fb57', + 'rsrc/externals/javelin/lib/Scrollbar.js' => '554854d3', + 'rsrc/externals/javelin/lib/Sound.js' => 'aff6fa58', + 'rsrc/externals/javelin/lib/URI.js' => '4b0b997a', + 'rsrc/externals/javelin/lib/Vector.js' => '0a734ab1', + 'rsrc/externals/javelin/lib/WebSocket.js' => 'eb6c1fee', + 'rsrc/externals/javelin/lib/Workflow.js' => 'e754ba8c', + 'rsrc/externals/javelin/lib/__tests__/Cookie.js' => '1628bb1a', + 'rsrc/externals/javelin/lib/__tests__/DOM.js' => '36688adb', + 'rsrc/externals/javelin/lib/__tests__/JSON.js' => 'e1d3542b', + 'rsrc/externals/javelin/lib/__tests__/URI.js' => '6f3f5668', + 'rsrc/externals/javelin/lib/__tests__/behavior.js' => 'a2dff9b9', + 'rsrc/externals/javelin/lib/behavior.js' => '823998c6', + 'rsrc/externals/javelin/lib/control/tokenizer/Tokenizer.js' => '37381107', + 'rsrc/externals/javelin/lib/control/typeahead/Typeahead.js' => '3b4a8853', + 'rsrc/externals/javelin/lib/control/typeahead/normalizer/TypeaheadNormalizer.js' => '0314da2a', + 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadCompositeSource.js' => '8f32ee03', + 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadOnDemandSource.js' => 'ea897c68', + 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadPreloadedSource.js' => '471d9cb0', + 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadSource.js' => 'a8149272', + 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadStaticSource.js' => '2f8a42fc', 'rsrc/favicons/favicon-16x16.png' => '4c51a03a', - 'rsrc/favicons/mask-icon.svg' => 'db699fe1', + 'rsrc/favicons/mask-icon.svg' => 'fa58b93a', 'rsrc/image/BFCFDA.png' => '74b5c88b', 'rsrc/image/actions/edit.png' => 'fd987dff', 'rsrc/image/avatar.png' => '0d17c6c4', @@ -356,1035 +363,992 @@ 'rsrc/image/texture/table_header.png' => '7652d1ad', 'rsrc/image/texture/table_header_hover.png' => '12ea5236', 'rsrc/image/texture/table_header_tall.png' => '5cc420c4', - 'rsrc/js/application/aphlict/Aphlict.js' => '022516b4', - 'rsrc/js/application/aphlict/behavior-aphlict-dropdown.js' => 'e9a2940f', - 'rsrc/js/application/aphlict/behavior-aphlict-listen.js' => '4e61fa88', - 'rsrc/js/application/aphlict/behavior-aphlict-status.js' => 'c3703a16', - 'rsrc/js/application/aphlict/behavior-desktop-notifications-control.js' => '070679fe', - 'rsrc/js/application/calendar/behavior-day-view.js' => '727a5a61', - 'rsrc/js/application/calendar/behavior-event-all-day.js' => '0b1bc990', - 'rsrc/js/application/calendar/behavior-month-view.js' => '158c64e0', - 'rsrc/js/application/config/behavior-reorder-fields.js' => '2539f834', - 'rsrc/js/application/conpherence/ConpherenceThreadManager.js' => 'aec8e38c', - 'rsrc/js/application/conpherence/behavior-conpherence-search.js' => '91befbcc', - 'rsrc/js/application/conpherence/behavior-durable-column.js' => 'fa6f30b2', - 'rsrc/js/application/conpherence/behavior-menu.js' => '8c2ed2bf', - 'rsrc/js/application/conpherence/behavior-participant-pane.js' => '43ba89a2', - 'rsrc/js/application/conpherence/behavior-pontificate.js' => '4ae58b5a', - 'rsrc/js/application/conpherence/behavior-quicksand-blacklist.js' => '5a6f6a06', - 'rsrc/js/application/conpherence/behavior-toggle-widget.js' => '8f959ad0', - 'rsrc/js/application/countdown/timer.js' => '6a162524', - 'rsrc/js/application/daemon/behavior-bulk-job-reload.js' => '3829a3cf', - 'rsrc/js/application/dashboard/behavior-dashboard-async-panel.js' => '9c01e364', - 'rsrc/js/application/dashboard/behavior-dashboard-move-panels.js' => 'a2ab19be', - 'rsrc/js/application/dashboard/behavior-dashboard-query-panel-select.js' => '1e413dc9', - 'rsrc/js/application/dashboard/behavior-dashboard-tab-panel.js' => '0116d3e8', - 'rsrc/js/application/diff/DiffChangeset.js' => 'd7d3ba75', - 'rsrc/js/application/diff/DiffChangesetList.js' => 'cc2c5de5', - 'rsrc/js/application/diff/DiffInline.js' => '9c775532', - 'rsrc/js/application/diff/DiffInlineContentState.js' => 'aa51efb4', - 'rsrc/js/application/diff/DiffPathView.js' => '8207abf9', - 'rsrc/js/application/diff/DiffTreeView.js' => '5d83623b', - 'rsrc/js/application/differential/behavior-diff-radios.js' => '925fe8cd', - 'rsrc/js/application/differential/behavior-populate.js' => 'b86ef6c2', - 'rsrc/js/application/diffusion/DiffusionLocateFileSource.js' => '94243d89', - 'rsrc/js/application/diffusion/ExternalEditorLinkEngine.js' => '48a8641f', - 'rsrc/js/application/diffusion/behavior-audit-preview.js' => 'b7b73831', - 'rsrc/js/application/diffusion/behavior-commit-branches.js' => '4b671572', - 'rsrc/js/application/diffusion/behavior-commit-graph.js' => 'ac10c917', - 'rsrc/js/application/diffusion/behavior-locate-file.js' => '87428eb2', - 'rsrc/js/application/diffusion/behavior-pull-lastmodified.js' => 'c715c123', - 'rsrc/js/application/doorkeeper/behavior-doorkeeper-tag.js' => '6a85bc5a', - 'rsrc/js/application/drydock/drydock-live-operation-status.js' => '47a0728b', - 'rsrc/js/application/fact/Chart.js' => '52e3ff03', - 'rsrc/js/application/fact/ChartCurtainView.js' => '86954222', - 'rsrc/js/application/fact/ChartFunctionLabel.js' => '81de1dab', - 'rsrc/js/application/files/behavior-document-engine.js' => '243d6c22', - 'rsrc/js/application/files/behavior-icon-composer.js' => '38a6cedb', - 'rsrc/js/application/files/behavior-launch-icon-composer.js' => 'a17b84f1', - 'rsrc/js/application/harbormaster/behavior-harbormaster-log.js' => 'b347a301', - 'rsrc/js/application/herald/HeraldRuleEditor.js' => '2633bef7', - 'rsrc/js/application/herald/PathTypeahead.js' => 'ad486db3', - 'rsrc/js/application/herald/herald-rule-editor.js' => '0922e81d', - 'rsrc/js/application/maniphest/behavior-batch-selector.js' => '139ef688', - 'rsrc/js/application/maniphest/behavior-line-chart.js' => 'ad258e28', - 'rsrc/js/application/maniphest/behavior-list-edit.js' => 'c687e867', - 'rsrc/js/application/owners/OwnersPathEditor.js' => '2a8b62d9', - 'rsrc/js/application/owners/owners-path-editor.js' => 'ff688a7a', - 'rsrc/js/application/passphrase/passphrase-credential-control.js' => '48fe33d0', - 'rsrc/js/application/pholio/behavior-pholio-mock-edit.js' => '3eed1f2b', - 'rsrc/js/application/pholio/behavior-pholio-mock-view.js' => '5aa1544e', - 'rsrc/js/application/phortune/behavior-stripe-payment-form.js' => '02cb4398', - 'rsrc/js/application/phortune/behavior-test-payment-form.js' => '4a7fb02b', - 'rsrc/js/application/phortune/phortune-credit-card-form.js' => 'd12d214f', - 'rsrc/js/application/policy/behavior-policy-control.js' => '0eaa33a9', - 'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '9347f172', - 'rsrc/js/application/projects/WorkboardBoard.js' => 'b46d88c5', - 'rsrc/js/application/projects/WorkboardCard.js' => '0392a5d8', - 'rsrc/js/application/projects/WorkboardCardTemplate.js' => '84f82dad', - 'rsrc/js/application/projects/WorkboardColumn.js' => 'c3d24e63', - 'rsrc/js/application/projects/WorkboardController.js' => 'b9d0c2f3', - 'rsrc/js/application/projects/WorkboardDropEffect.js' => '8e0aa661', - 'rsrc/js/application/projects/WorkboardHeader.js' => '111bfd2d', - 'rsrc/js/application/projects/WorkboardHeaderTemplate.js' => 'ebe83a6b', - 'rsrc/js/application/projects/WorkboardOrderTemplate.js' => '03e8891f', - 'rsrc/js/application/projects/behavior-project-boards.js' => '58cb6a88', - 'rsrc/js/application/projects/behavior-project-create.js' => '34c53422', - 'rsrc/js/application/projects/behavior-reorder-columns.js' => '8ac32fd9', - 'rsrc/js/application/repository/repository-crossreference.js' => '44d48cd1', - 'rsrc/js/application/search/behavior-reorder-profile-menu-items.js' => 'e5bdb730', - 'rsrc/js/application/search/behavior-reorder-queries.js' => 'b86f297f', - 'rsrc/js/application/transactions/behavior-comment-actions.js' => '4dffaeb2', - 'rsrc/js/application/transactions/behavior-reorder-configs.js' => '4842f137', - 'rsrc/js/application/transactions/behavior-reorder-fields.js' => '0ad8d31f', - 'rsrc/js/application/transactions/behavior-show-older-transactions.js' => '8b5c7d65', - 'rsrc/js/application/transactions/behavior-transaction-comment-form.js' => '2bdadf1a', - 'rsrc/js/application/transactions/behavior-transaction-list.js' => '9cec214e', - 'rsrc/js/application/trigger/TriggerRule.js' => '41b7b4f6', - 'rsrc/js/application/trigger/TriggerRuleControl.js' => '5faf27b9', - 'rsrc/js/application/trigger/TriggerRuleEditor.js' => 'b49fd60c', - 'rsrc/js/application/trigger/TriggerRuleType.js' => '4feea7d3', - 'rsrc/js/application/trigger/trigger-rule-editor.js' => '398fdf13', - 'rsrc/js/application/typeahead/behavior-typeahead-browse.js' => '70245195', - 'rsrc/js/application/typeahead/behavior-typeahead-search.js' => '7b139193', - 'rsrc/js/application/uiexample/gesture-example.js' => '242dedd0', - 'rsrc/js/application/uiexample/notification-example.js' => '29819b75', - 'rsrc/js/core/Busy.js' => '5202e831', - 'rsrc/js/core/DragAndDropFileUpload.js' => '4370900d', - 'rsrc/js/core/DraggableList.js' => '0169e425', - 'rsrc/js/core/Favicon.js' => '7930776a', - 'rsrc/js/core/FileUpload.js' => 'ab85e184', - 'rsrc/js/core/Hovercard.js' => '6199f752', - 'rsrc/js/core/HovercardList.js' => 'de4b4919', - 'rsrc/js/core/KeyboardShortcut.js' => '1a844c06', - 'rsrc/js/core/KeyboardShortcutManager.js' => '81debc48', - 'rsrc/js/core/MultirowRowManager.js' => '5b54c823', - 'rsrc/js/core/Notification.js' => 'a9b91e3f', - 'rsrc/js/core/Prefab.js' => '5793d835', - 'rsrc/js/core/ShapedRequest.js' => '995f5102', - 'rsrc/js/core/TextAreaUtils.js' => 'f340a484', - 'rsrc/js/core/Title.js' => '43bc9360', - 'rsrc/js/core/ToolTip.js' => '83754533', - 'rsrc/js/core/behavior-audio-source.js' => '3dc5ad43', - 'rsrc/js/core/behavior-autofocus.js' => '65bb0011', - 'rsrc/js/core/behavior-badge-view.js' => '92cdd7b6', - 'rsrc/js/core/behavior-bulk-editor.js' => 'aa6d2308', - 'rsrc/js/core/behavior-choose-control.js' => '04f8a1e3', - 'rsrc/js/core/behavior-copy.js' => 'cf32921f', - 'rsrc/js/core/behavior-detect-timezone.js' => '78bc5d94', - 'rsrc/js/core/behavior-device.js' => 'ac2b1e01', - 'rsrc/js/core/behavior-drag-and-drop-textarea.js' => '3277c62d', - 'rsrc/js/core/behavior-fancy-datepicker.js' => '36821f8d', - 'rsrc/js/core/behavior-form.js' => '55d7b788', - 'rsrc/js/core/behavior-gesture.js' => 'b58d1a2a', - 'rsrc/js/core/behavior-global-drag-and-drop.js' => '1cab0e9a', - 'rsrc/js/core/behavior-high-security-warning.js' => 'dae2d55b', - 'rsrc/js/core/behavior-history-install.js' => '6a1583a8', - 'rsrc/js/core/behavior-hovercard.js' => '183738e6', - 'rsrc/js/core/behavior-keyboard-pager.js' => '1325b731', - 'rsrc/js/core/behavior-keyboard-shortcuts.js' => '42c44e8b', - 'rsrc/js/core/behavior-lightbox-attachments.js' => '14c7ab36', - 'rsrc/js/core/behavior-line-linker.js' => '0d915ff5', - 'rsrc/js/core/behavior-linked-container.js' => '74446546', - 'rsrc/js/core/behavior-more.js' => '506aa3f4', - 'rsrc/js/core/behavior-object-selector.js' => '98ef467f', - 'rsrc/js/core/behavior-oncopy.js' => 'da8f5259', - 'rsrc/js/core/behavior-phabricator-remarkup-assist.js' => '54262396', - 'rsrc/js/core/behavior-read-only-warning.js' => 'b9109f8f', - 'rsrc/js/core/behavior-redirect.js' => '407ee861', - 'rsrc/js/core/behavior-refresh-csrf.js' => '46116c01', - 'rsrc/js/core/behavior-remarkup-load-image.js' => '202bfa3f', - 'rsrc/js/core/behavior-remarkup-preview.js' => 'd8a86cfb', - 'rsrc/js/core/behavior-reorder-applications.js' => 'aa371860', - 'rsrc/js/core/behavior-reveal-content.js' => 'b105a3a6', - 'rsrc/js/core/behavior-scrollbar.js' => '92388bae', - 'rsrc/js/core/behavior-search-typeahead.js' => '1cb7d027', - 'rsrc/js/core/behavior-select-content.js' => 'e8240b50', - 'rsrc/js/core/behavior-select-on-click.js' => '66365ee2', - 'rsrc/js/core/behavior-setup-check-https.js' => '01384686', - 'rsrc/js/core/behavior-time-typeahead.js' => '5803b9e7', - 'rsrc/js/core/behavior-toggle-class.js' => '32db8374', - 'rsrc/js/core/behavior-tokenizer.js' => '3b4899b0', - 'rsrc/js/core/behavior-tooltip.js' => '73ecc1f8', - 'rsrc/js/core/behavior-user-menu.js' => '60cd9241', - 'rsrc/js/core/behavior-watch-anchor.js' => 'a77e2cbd', - 'rsrc/js/core/behavior-workflow.js' => '9623adc1', - 'rsrc/js/core/darkconsole/DarkLog.js' => '3b869402', - 'rsrc/js/core/darkconsole/DarkMessage.js' => '26cd4b73', - 'rsrc/js/core/darkconsole/behavior-dark-console.js' => '457f4d16', - 'rsrc/js/core/phtize.js' => '2f1db1ed', - 'rsrc/js/phui/behavior-phui-dropdown-menu.js' => '5cf0501a', - 'rsrc/js/phui/behavior-phui-file-upload.js' => 'e150bd50', - 'rsrc/js/phui/behavior-phui-selectable-list.js' => 'b26a41e4', - 'rsrc/js/phui/behavior-phui-submenu.js' => 'b5e9bff9', - 'rsrc/js/phui/behavior-phui-tab-group.js' => '242aa08b', - 'rsrc/js/phui/behavior-phui-timer-control.js' => 'f84bcbf4', - 'rsrc/js/phuix/PHUIXActionListView.js' => 'c68f183f', - 'rsrc/js/phuix/PHUIXActionView.js' => 'a8f573a9', - 'rsrc/js/phuix/PHUIXAutocomplete.js' => '2fbe234d', - 'rsrc/js/phuix/PHUIXButtonView.js' => '55a24e84', - 'rsrc/js/phuix/PHUIXDropdownMenu.js' => 'b557770a', - 'rsrc/js/phuix/PHUIXExample.js' => 'c2c500a7', - 'rsrc/js/phuix/PHUIXFormControl.js' => '38c1f3fb', - 'rsrc/js/phuix/PHUIXFormationColumnView.js' => '4bcc1f78', - 'rsrc/js/phuix/PHUIXFormationFlankView.js' => '6648270a', - 'rsrc/js/phuix/PHUIXFormationView.js' => 'cef53b3e', - 'rsrc/js/phuix/PHUIXIconView.js' => 'a5257c4e', + 'rsrc/js/application/aphlict/Aphlict.js' => '6778cd25', + 'rsrc/js/application/aphlict/behavior-aphlict-dropdown.js' => 'da025579', + 'rsrc/js/application/aphlict/behavior-aphlict-listen.js' => '5b4d8666', + 'rsrc/js/application/aphlict/behavior-aphlict-status.js' => 'a4e03d76', + 'rsrc/js/application/aphlict/behavior-desktop-notifications-control.js' => 'e4366d54', + 'rsrc/js/application/calendar/behavior-day-view.js' => '7a61e28c', + 'rsrc/js/application/calendar/behavior-event-all-day.js' => 'e7cb91b5', + 'rsrc/js/application/calendar/behavior-month-view.js' => '610587d5', + 'rsrc/js/application/config/behavior-reorder-fields.js' => 'bdac8af0', + 'rsrc/js/application/conpherence/ConpherenceThreadManager.js' => '7fa6d478', + 'rsrc/js/application/conpherence/behavior-conpherence-search.js' => '763dd0b2', + 'rsrc/js/application/conpherence/behavior-durable-column.js' => 'e57f4bdd', + 'rsrc/js/application/conpherence/behavior-menu.js' => '8312ac01', + 'rsrc/js/application/conpherence/behavior-participant-pane.js' => '27e29e59', + 'rsrc/js/application/conpherence/behavior-pontificate.js' => 'b48f5e2c', + 'rsrc/js/application/conpherence/behavior-quicksand-blacklist.js' => 'd52a680e', + 'rsrc/js/application/conpherence/behavior-toggle-widget.js' => 'fd92bf06', + 'rsrc/js/application/countdown/timer.js' => '8c5ff0ff', + 'rsrc/js/application/daemon/behavior-bulk-job-reload.js' => 'fa7092c6', + 'rsrc/js/application/dashboard/behavior-dashboard-async-panel.js' => '87b119e1', + 'rsrc/js/application/dashboard/behavior-dashboard-move-panels.js' => '31a1c4a1', + 'rsrc/js/application/dashboard/behavior-dashboard-query-panel-select.js' => 'bae4d536', + 'rsrc/js/application/dashboard/behavior-dashboard-tab-panel.js' => 'cb1ed345', + 'rsrc/js/application/diff/DiffChangeset.js' => 'd33517b0', + 'rsrc/js/application/diff/DiffChangesetList.js' => '1683d069', + 'rsrc/js/application/diff/DiffInline.js' => '225287db', + 'rsrc/js/application/diff/DiffInlineContentState.js' => '269f2ce5', + 'rsrc/js/application/diff/DiffPathView.js' => 'a985aa58', + 'rsrc/js/application/diff/DiffTreeView.js' => '28e74c8a', + 'rsrc/js/application/differential/behavior-diff-radios.js' => '27187c26', + 'rsrc/js/application/differential/behavior-populate.js' => 'cb8ced97', + 'rsrc/js/application/diffusion/DiffusionLocateFileSource.js' => '31ebed0b', + 'rsrc/js/application/diffusion/ExternalEditorLinkEngine.js' => 'e9da5466', + 'rsrc/js/application/diffusion/behavior-audit-preview.js' => '95a14f58', + 'rsrc/js/application/diffusion/behavior-commit-branches.js' => '63b4e112', + 'rsrc/js/application/diffusion/behavior-commit-graph.js' => '8cc0bee0', + 'rsrc/js/application/diffusion/behavior-locate-file.js' => 'd9158762', + 'rsrc/js/application/diffusion/behavior-pull-lastmodified.js' => '662be695', + 'rsrc/js/application/doorkeeper/behavior-doorkeeper-tag.js' => '66673225', + 'rsrc/js/application/drydock/drydock-live-operation-status.js' => 'bcb1469f', + 'rsrc/js/application/fact/Chart.js' => '39bf659d', + 'rsrc/js/application/fact/ChartCurtainView.js' => '959b7022', + 'rsrc/js/application/fact/ChartFunctionLabel.js' => '9c100943', + 'rsrc/js/application/files/behavior-document-engine.js' => '79b71246', + 'rsrc/js/application/files/behavior-icon-composer.js' => '9151195b', + 'rsrc/js/application/files/behavior-launch-icon-composer.js' => '0bc272f6', + 'rsrc/js/application/harbormaster/behavior-harbormaster-log.js' => '8233f53b', + 'rsrc/js/application/herald/HeraldRuleEditor.js' => 'b377c6c1', + 'rsrc/js/application/herald/PathTypeahead.js' => 'ae7d493d', + 'rsrc/js/application/herald/herald-rule-editor.js' => 'c76477e6', + 'rsrc/js/application/maniphest/behavior-batch-selector.js' => '44b74260', + 'rsrc/js/application/maniphest/behavior-line-chart.js' => '479d39e8', + 'rsrc/js/application/maniphest/behavior-list-edit.js' => '3dfbd876', + 'rsrc/js/application/owners/OwnersPathEditor.js' => 'c1810d04', + 'rsrc/js/application/owners/owners-path-editor.js' => 'f1c80c78', + 'rsrc/js/application/passphrase/passphrase-credential-control.js' => 'dd66ccfa', + 'rsrc/js/application/pholio/behavior-pholio-mock-edit.js' => '6e85bec4', + 'rsrc/js/application/pholio/behavior-pholio-mock-view.js' => 'edd6d697', + 'rsrc/js/application/phortune/behavior-stripe-payment-form.js' => '9d56ea6d', + 'rsrc/js/application/phortune/behavior-test-payment-form.js' => '39c77ea0', + 'rsrc/js/application/phortune/phortune-credit-card-form.js' => '685f95ed', + 'rsrc/js/application/policy/behavior-policy-control.js' => 'd88500e0', + 'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '515a18f6', + 'rsrc/js/application/projects/WorkboardBoard.js' => '556e7da0', + 'rsrc/js/application/projects/WorkboardCard.js' => '0b61ca0a', + 'rsrc/js/application/projects/WorkboardCardTemplate.js' => '0e6cd2c5', + 'rsrc/js/application/projects/WorkboardColumn.js' => '86c9bec8', + 'rsrc/js/application/projects/WorkboardController.js' => '6a85a739', + 'rsrc/js/application/projects/WorkboardDropEffect.js' => '6803f974', + 'rsrc/js/application/projects/WorkboardHeader.js' => '868e951d', + 'rsrc/js/application/projects/WorkboardHeaderTemplate.js' => '4bead335', + 'rsrc/js/application/projects/WorkboardOrderTemplate.js' => 'bb4c7566', + 'rsrc/js/application/projects/behavior-project-boards.js' => '40174fe1', + 'rsrc/js/application/projects/behavior-project-create.js' => 'b2d6b2d6', + 'rsrc/js/application/projects/behavior-reorder-columns.js' => 'c829538d', + 'rsrc/js/application/repository/repository-crossreference.js' => '0a9e2f6d', + 'rsrc/js/application/search/behavior-reorder-profile-menu-items.js' => '892f8706', + 'rsrc/js/application/search/behavior-reorder-queries.js' => 'e8754d9a', + 'rsrc/js/application/timetracker/chart/Chart.min.js' => '0c0a9abf', + 'rsrc/js/application/timetracker/chart/jquery.min.js' => '81da9b0e', + 'rsrc/js/application/timetracker/chart/show-graph.js' => 'c1dff964', + 'rsrc/js/application/timetracker/jquery-ui.js' => '4b972dbc', + 'rsrc/js/application/timetracker/jquery.js' => 'af4e8be2', + 'rsrc/js/application/timetracker/timetracker.js' => 'ad4427cb', + 'rsrc/js/application/transactions/behavior-comment-actions.js' => 'dabe390e', + 'rsrc/js/application/transactions/behavior-reorder-configs.js' => 'efce8717', + 'rsrc/js/application/transactions/behavior-reorder-fields.js' => 'e3cebb9a', + 'rsrc/js/application/transactions/behavior-show-older-transactions.js' => 'ae72a2a1', + 'rsrc/js/application/transactions/behavior-transaction-comment-form.js' => '82cb3153', + 'rsrc/js/application/transactions/behavior-transaction-list.js' => '333ce90f', + 'rsrc/js/application/trigger/TriggerRule.js' => '04fa050d', + 'rsrc/js/application/trigger/TriggerRuleControl.js' => '69f2d489', + 'rsrc/js/application/trigger/TriggerRuleEditor.js' => 'd8c8b363', + 'rsrc/js/application/trigger/TriggerRuleType.js' => 'a0632844', + 'rsrc/js/application/trigger/trigger-rule-editor.js' => '3a06ac82', + 'rsrc/js/application/typeahead/behavior-typeahead-browse.js' => 'a6e925d3', + 'rsrc/js/application/typeahead/behavior-typeahead-search.js' => '74175cff', + 'rsrc/js/application/uiexample/gesture-example.js' => '180a90b7', + 'rsrc/js/application/uiexample/notification-example.js' => 'd337bd45', + 'rsrc/js/core/Busy.js' => '5c4ab42a', + 'rsrc/js/core/DragAndDropFileUpload.js' => '5337ec83', + 'rsrc/js/core/DraggableList.js' => '2ecba827', + 'rsrc/js/core/Favicon.js' => '0d352869', + 'rsrc/js/core/FileUpload.js' => 'f20f2119', + 'rsrc/js/core/Hovercard.js' => 'd7707183', + 'rsrc/js/core/HovercardList.js' => '92d63c24', + 'rsrc/js/core/KeyboardShortcut.js' => '71c55d20', + 'rsrc/js/core/KeyboardShortcutManager.js' => '81770869', + 'rsrc/js/core/MultirowRowManager.js' => '8cf58b04', + 'rsrc/js/core/Notification.js' => 'e8ea2329', + 'rsrc/js/core/Prefab.js' => 'ddcf7994', + 'rsrc/js/core/ShapedRequest.js' => '79e07647', + 'rsrc/js/core/TextAreaUtils.js' => '5d79b875', + 'rsrc/js/core/Title.js' => 'dc4dc986', + 'rsrc/js/core/ToolTip.js' => '7e67b544', + 'rsrc/js/core/behavior-audio-source.js' => '49a68708', + 'rsrc/js/core/behavior-autofocus.js' => '3027bdc5', + 'rsrc/js/core/behavior-badge-view.js' => '5f013b5e', + 'rsrc/js/core/behavior-bulk-editor.js' => '96f0778d', + 'rsrc/js/core/behavior-choose-control.js' => '667c3061', + 'rsrc/js/core/behavior-copy.js' => '7a17e183', + 'rsrc/js/core/behavior-detect-timezone.js' => '66d4c8ef', + 'rsrc/js/core/behavior-device.js' => 'fab38736', + 'rsrc/js/core/behavior-drag-and-drop-textarea.js' => '5f454b6c', + 'rsrc/js/core/behavior-fancy-datepicker.js' => 'e9ff303a', + 'rsrc/js/core/behavior-form.js' => '9e54c007', + 'rsrc/js/core/behavior-gesture.js' => 'e08d87ef', + 'rsrc/js/core/behavior-global-drag-and-drop.js' => 'b44cb8be', + 'rsrc/js/core/behavior-high-security-warning.js' => '13933ffe', + 'rsrc/js/core/behavior-history-install.js' => '32636e27', + 'rsrc/js/core/behavior-hovercard.js' => '319cf55a', + 'rsrc/js/core/behavior-keyboard-pager.js' => '7dcfc62b', + 'rsrc/js/core/behavior-keyboard-shortcuts.js' => '9022542a', + 'rsrc/js/core/behavior-lightbox-attachments.js' => '3181860e', + 'rsrc/js/core/behavior-line-linker.js' => '4c8dd68a', + 'rsrc/js/core/behavior-linked-container.js' => '1df032b1', + 'rsrc/js/core/behavior-more.js' => '39d8efb6', + 'rsrc/js/core/behavior-object-selector.js' => '8bf10492', + 'rsrc/js/core/behavior-oncopy.js' => '92f45381', + 'rsrc/js/core/behavior-phabricator-remarkup-assist.js' => '222be76a', + 'rsrc/js/core/behavior-read-only-warning.js' => 'b6ae7ef5', + 'rsrc/js/core/behavior-redirect.js' => '8796e6c1', + 'rsrc/js/core/behavior-refresh-csrf.js' => '9729be58', + 'rsrc/js/core/behavior-remarkup-load-image.js' => 'd7e34a5b', + 'rsrc/js/core/behavior-remarkup-preview.js' => '9e977c33', + 'rsrc/js/core/behavior-reorder-applications.js' => '4d1fb46c', + 'rsrc/js/core/behavior-reveal-content.js' => '5fba5787', + 'rsrc/js/core/behavior-scrollbar.js' => '225f8317', + 'rsrc/js/core/behavior-search-typeahead.js' => 'cb3fa126', + 'rsrc/js/core/behavior-select-content.js' => 'a0b836b0', + 'rsrc/js/core/behavior-select-on-click.js' => 'b07a98e6', + 'rsrc/js/core/behavior-setup-check-https.js' => '1bedf740', + 'rsrc/js/core/behavior-time-typeahead.js' => '70d377b6', + 'rsrc/js/core/behavior-toggle-class.js' => '4c29854b', + 'rsrc/js/core/behavior-tokenizer.js' => '3a365f3a', + 'rsrc/js/core/behavior-tooltip.js' => '98c0c9d1', + 'rsrc/js/core/behavior-user-menu.js' => '1eed45ed', + 'rsrc/js/core/behavior-watch-anchor.js' => '2fa37c8a', + 'rsrc/js/core/behavior-workflow.js' => 'c49be7c7', + 'rsrc/js/core/darkconsole/DarkLog.js' => '69f214f4', + 'rsrc/js/core/darkconsole/DarkMessage.js' => 'f13d3e3a', + 'rsrc/js/core/darkconsole/behavior-dark-console.js' => '81f19452', + 'rsrc/js/core/phtize.js' => '0c2e2e99', + 'rsrc/js/phui/behavior-phui-dropdown-menu.js' => 'e76e2c73', + 'rsrc/js/phui/behavior-phui-file-upload.js' => '9c9cd23d', + 'rsrc/js/phui/behavior-phui-selectable-list.js' => '8084fa7b', + 'rsrc/js/phui/behavior-phui-submenu.js' => '58bc69cb', + 'rsrc/js/phui/behavior-phui-tab-group.js' => '1133d6e4', + 'rsrc/js/phui/behavior-phui-timer-control.js' => '66f14553', + 'rsrc/js/phuix/PHUIXActionListView.js' => '53ff28b6', + 'rsrc/js/phuix/PHUIXActionView.js' => '4f86f4eb', + 'rsrc/js/phuix/PHUIXAutocomplete.js' => '3746c3eb', + 'rsrc/js/phuix/PHUIXButtonView.js' => 'b7c340b9', + 'rsrc/js/phuix/PHUIXDropdownMenu.js' => 'ae501775', + 'rsrc/js/phuix/PHUIXExample.js' => '6f88d4f4', + 'rsrc/js/phuix/PHUIXFormControl.js' => '78e64734', + 'rsrc/js/phuix/PHUIXFormationColumnView.js' => '24e6885b', + 'rsrc/js/phuix/PHUIXFormationFlankView.js' => 'f5aa2659', + 'rsrc/js/phuix/PHUIXFormationView.js' => 'd89b0a1b', + 'rsrc/js/phuix/PHUIXIconView.js' => 'fc62fade', ), 'symbols' => array( - 'almanac-css' => '2e050f4f', - 'aphront-bars' => '4a327b4a', - 'aphront-dark-console-css' => '7f06cda2', - 'aphront-dialog-view-css' => '6f4ea703', - 'aphront-list-filter-view-css' => 'feb64255', - 'aphront-multi-column-view-css' => 'fbc00ba3', - 'aphront-panel-view-css' => '46923d46', - 'aphront-table-view-css' => '0bb61df1', - 'aphront-tokenizer-control-css' => '34e2a838', - 'aphront-tooltip-css' => 'e3f2412f', - 'aphront-typeahead-control-css' => '8779483d', - 'application-search-view-css' => '0f7c06d8', - 'auth-css' => 'c2f23d74', - 'bulk-job-css' => '73af99f5', - 'conduit-api-css' => 'ce2cfc41', - 'config-options-css' => '16c920ae', - 'conpherence-color-css' => 'b17746b0', - 'conpherence-durable-column-view' => '2d57072b', - 'conpherence-header-pane-css' => 'c9a3db8e', - 'conpherence-menu-css' => '67f4680d', - 'conpherence-message-pane-css' => 'd244db1e', - 'conpherence-notification-css' => '6a3d4e58', - 'conpherence-participant-pane-css' => '69e0058a', - 'conpherence-thread-manager' => 'aec8e38c', - 'conpherence-transaction-css' => '3a3f5e7e', - 'd3' => '9d068042', - 'diff-tree-view-css' => 'e2d3e222', - 'differential-changeset-view-css' => '60c3d405', - 'differential-core-view-css' => '7300a73e', - 'differential-revision-add-comment-css' => '7e5900d9', - 'differential-revision-comment-css' => '7dbc8d1d', - 'differential-revision-history-css' => '237a2979', - 'differential-revision-list-css' => '93d2df7d', - 'differential-table-of-contents-css' => 'bba788b9', - 'diffusion-css' => 'e46232d6', - 'diffusion-icons-css' => '23b31a1b', - 'diffusion-readme-css' => 'b68a76e4', - 'diffusion-repository-css' => 'b89e8c6c', - 'diviner-shared-css' => '4bd263b0', - 'font-fontawesome' => '3883938a', - 'font-lato' => '23631304', - 'fuel-grid-css' => '66697240', - 'fuel-handle-list-css' => '2c4cbeca', - 'fuel-map-css' => 'd6e31510', - 'fuel-menu-css' => '21f5d199', - 'global-drag-and-drop-css' => '1d2713a4', - 'harbormaster-css' => '8dfe16b2', - 'herald-css' => '648d39e2', - 'herald-rule-editor' => '2633bef7', - 'herald-test-css' => '7e7bbdae', - 'inline-comment-summary-css' => '81eb368d', - 'javelin-aphlict' => '022516b4', - 'javelin-behavior' => '1b6acc2a', - 'javelin-behavior-aphlict-dropdown' => 'e9a2940f', - 'javelin-behavior-aphlict-listen' => '4e61fa88', - 'javelin-behavior-aphlict-status' => 'c3703a16', - 'javelin-behavior-aphront-basic-tokenizer' => '3b4899b0', - 'javelin-behavior-aphront-drag-and-drop-textarea' => '3277c62d', - 'javelin-behavior-aphront-form-disable-on-submit' => '55d7b788', - 'javelin-behavior-aphront-more' => '506aa3f4', - 'javelin-behavior-audio-source' => '3dc5ad43', - 'javelin-behavior-audit-preview' => 'b7b73831', - 'javelin-behavior-badge-view' => '92cdd7b6', - 'javelin-behavior-bulk-editor' => 'aa6d2308', - 'javelin-behavior-bulk-job-reload' => '3829a3cf', - 'javelin-behavior-calendar-month-view' => '158c64e0', - 'javelin-behavior-choose-control' => '04f8a1e3', - 'javelin-behavior-comment-actions' => '4dffaeb2', - 'javelin-behavior-config-reorder-fields' => '2539f834', - 'javelin-behavior-conpherence-menu' => '8c2ed2bf', - 'javelin-behavior-conpherence-participant-pane' => '43ba89a2', - 'javelin-behavior-conpherence-pontificate' => '4ae58b5a', - 'javelin-behavior-conpherence-search' => '91befbcc', - 'javelin-behavior-countdown-timer' => '6a162524', - 'javelin-behavior-dark-console' => '457f4d16', - 'javelin-behavior-dashboard-async-panel' => '9c01e364', - 'javelin-behavior-dashboard-move-panels' => 'a2ab19be', - 'javelin-behavior-dashboard-query-panel-select' => '1e413dc9', - 'javelin-behavior-dashboard-tab-panel' => '0116d3e8', - 'javelin-behavior-day-view' => '727a5a61', - 'javelin-behavior-desktop-notifications-control' => '070679fe', - 'javelin-behavior-detect-timezone' => '78bc5d94', - 'javelin-behavior-device' => 'ac2b1e01', - 'javelin-behavior-differential-diff-radios' => '925fe8cd', - 'javelin-behavior-differential-populate' => 'b86ef6c2', - 'javelin-behavior-diffusion-commit-branches' => '4b671572', - 'javelin-behavior-diffusion-commit-graph' => 'ac10c917', - 'javelin-behavior-diffusion-locate-file' => '87428eb2', - 'javelin-behavior-diffusion-pull-lastmodified' => 'c715c123', - 'javelin-behavior-document-engine' => '243d6c22', - 'javelin-behavior-doorkeeper-tag' => '6a85bc5a', - 'javelin-behavior-drydock-live-operation-status' => '47a0728b', - 'javelin-behavior-durable-column' => 'fa6f30b2', - 'javelin-behavior-editengine-reorder-configs' => '4842f137', - 'javelin-behavior-editengine-reorder-fields' => '0ad8d31f', - 'javelin-behavior-event-all-day' => '0b1bc990', - 'javelin-behavior-fancy-datepicker' => '36821f8d', - 'javelin-behavior-global-drag-and-drop' => '1cab0e9a', - 'javelin-behavior-harbormaster-log' => 'b347a301', - 'javelin-behavior-herald-rule-editor' => '0922e81d', - 'javelin-behavior-high-security-warning' => 'dae2d55b', - 'javelin-behavior-history-install' => '6a1583a8', - 'javelin-behavior-icon-composer' => '38a6cedb', - 'javelin-behavior-launch-icon-composer' => 'a17b84f1', - 'javelin-behavior-lightbox-attachments' => '14c7ab36', - 'javelin-behavior-line-chart' => 'ad258e28', - 'javelin-behavior-linked-container' => '74446546', - 'javelin-behavior-maniphest-batch-selector' => '139ef688', - 'javelin-behavior-maniphest-list-editor' => 'c687e867', - 'javelin-behavior-owners-path-editor' => 'ff688a7a', - 'javelin-behavior-passphrase-credential-control' => '48fe33d0', - 'javelin-behavior-phabricator-autofocus' => '65bb0011', - 'javelin-behavior-phabricator-clipboard-copy' => 'cf32921f', - 'javelin-behavior-phabricator-gesture' => 'b58d1a2a', - 'javelin-behavior-phabricator-gesture-example' => '242dedd0', - 'javelin-behavior-phabricator-keyboard-pager' => '1325b731', - 'javelin-behavior-phabricator-keyboard-shortcuts' => '42c44e8b', - 'javelin-behavior-phabricator-line-linker' => '0d915ff5', - 'javelin-behavior-phabricator-notification-example' => '29819b75', - 'javelin-behavior-phabricator-object-selector' => '98ef467f', - 'javelin-behavior-phabricator-oncopy' => 'da8f5259', - 'javelin-behavior-phabricator-remarkup-assist' => '54262396', - 'javelin-behavior-phabricator-reveal-content' => 'b105a3a6', - 'javelin-behavior-phabricator-search-typeahead' => '1cb7d027', - 'javelin-behavior-phabricator-show-older-transactions' => '8b5c7d65', - 'javelin-behavior-phabricator-tooltips' => '73ecc1f8', - 'javelin-behavior-phabricator-transaction-comment-form' => '2bdadf1a', - 'javelin-behavior-phabricator-transaction-list' => '9cec214e', - 'javelin-behavior-phabricator-watch-anchor' => 'a77e2cbd', - 'javelin-behavior-pholio-mock-edit' => '3eed1f2b', - 'javelin-behavior-pholio-mock-view' => '5aa1544e', - 'javelin-behavior-phui-dropdown-menu' => '5cf0501a', - 'javelin-behavior-phui-file-upload' => 'e150bd50', - 'javelin-behavior-phui-hovercards' => '183738e6', - 'javelin-behavior-phui-selectable-list' => 'b26a41e4', - 'javelin-behavior-phui-submenu' => 'b5e9bff9', - 'javelin-behavior-phui-tab-group' => '242aa08b', - 'javelin-behavior-phui-timer-control' => 'f84bcbf4', - 'javelin-behavior-phuix-example' => 'c2c500a7', - 'javelin-behavior-policy-control' => '0eaa33a9', - 'javelin-behavior-policy-rule-editor' => '9347f172', - 'javelin-behavior-project-boards' => '58cb6a88', - 'javelin-behavior-project-create' => '34c53422', - 'javelin-behavior-quicksand-blacklist' => '5a6f6a06', - 'javelin-behavior-read-only-warning' => 'b9109f8f', - 'javelin-behavior-redirect' => '407ee861', - 'javelin-behavior-refresh-csrf' => '46116c01', - 'javelin-behavior-remarkup-load-image' => '202bfa3f', - 'javelin-behavior-remarkup-preview' => 'd8a86cfb', - 'javelin-behavior-reorder-applications' => 'aa371860', - 'javelin-behavior-reorder-columns' => '8ac32fd9', - 'javelin-behavior-reorder-profile-menu-items' => 'e5bdb730', - 'javelin-behavior-repository-crossreference' => '44d48cd1', - 'javelin-behavior-scrollbar' => '92388bae', - 'javelin-behavior-search-reorder-queries' => 'b86f297f', - 'javelin-behavior-select-content' => 'e8240b50', - 'javelin-behavior-select-on-click' => '66365ee2', - 'javelin-behavior-setup-check-https' => '01384686', - 'javelin-behavior-stripe-payment-form' => '02cb4398', - 'javelin-behavior-test-payment-form' => '4a7fb02b', - 'javelin-behavior-time-typeahead' => '5803b9e7', - 'javelin-behavior-toggle-class' => '32db8374', - 'javelin-behavior-toggle-widget' => '8f959ad0', - 'javelin-behavior-trigger-rule-editor' => '398fdf13', - 'javelin-behavior-typeahead-browse' => '70245195', - 'javelin-behavior-typeahead-search' => '7b139193', - 'javelin-behavior-user-menu' => '60cd9241', - 'javelin-behavior-view-placeholder' => 'a9942052', - 'javelin-behavior-workflow' => '9623adc1', - 'javelin-chart' => '52e3ff03', - 'javelin-chart-curtain-view' => '86954222', - 'javelin-chart-function-label' => '81de1dab', - 'javelin-color' => '78f811c9', - 'javelin-cookie' => '05d290ef', - 'javelin-diffusion-locate-file-source' => '94243d89', - 'javelin-dom' => 'e4c7622a', - 'javelin-dynval' => '202a2e85', - 'javelin-event' => 'c03f2fb4', - 'javelin-external-editor-link-engine' => '48a8641f', - 'javelin-fx' => '34450586', - 'javelin-history' => '030b4f7a', - 'javelin-install' => '5902260c', - 'javelin-json' => '541f81c3', - 'javelin-leader' => '0d2490ce', - 'javelin-magical-init' => '98e6504a', - 'javelin-mask' => '7c4d8998', - 'javelin-quicksand' => 'd3799cb4', - 'javelin-reactor' => '1c850a26', - 'javelin-reactor-dom' => '6cfa0008', - 'javelin-reactor-node-calmer' => '225bbb98', - 'javelin-reactornode' => '72960bc1', - 'javelin-request' => '84e6891f', - 'javelin-resource' => '20514cc2', - 'javelin-routable' => '6a18c42e', - 'javelin-router' => '32755edb', - 'javelin-scrollbar' => 'a43ae2ae', - 'javelin-sound' => 'd4cc2d2a', - 'javelin-stratcom' => '0889b835', - 'javelin-tokenizer' => '89a1ae3a', - 'javelin-typeahead' => 'a4356cde', - 'javelin-typeahead-composite-source' => '22ee68a5', - 'javelin-typeahead-normalizer' => 'a241536a', - 'javelin-typeahead-ondemand-source' => '23387297', - 'javelin-typeahead-preloaded-source' => '5a79f6c3', - 'javelin-typeahead-source' => '8badee71', - 'javelin-typeahead-static-source' => '80bff3af', - 'javelin-uri' => '2e255291', - 'javelin-util' => 'edb4d8c9', - 'javelin-vector' => 'e9c80beb', - 'javelin-view' => '289bf236', - 'javelin-view-html' => 'f8c4e135', - 'javelin-view-interpreter' => '876506b6', - 'javelin-view-renderer' => '9aae2b66', - 'javelin-view-visitor' => '308f9fe4', - 'javelin-websocket' => 'fdc13e4e', - 'javelin-workboard-board' => 'b46d88c5', - 'javelin-workboard-card' => '0392a5d8', - 'javelin-workboard-card-template' => '84f82dad', - 'javelin-workboard-column' => 'c3d24e63', - 'javelin-workboard-controller' => 'b9d0c2f3', - 'javelin-workboard-drop-effect' => '8e0aa661', - 'javelin-workboard-header' => '111bfd2d', - 'javelin-workboard-header-template' => 'ebe83a6b', - 'javelin-workboard-order-template' => '03e8891f', - 'javelin-workflow' => '945ff654', - 'maniphest-report-css' => '3d53188b', - 'maniphest-task-edit-css' => '272daa84', - 'maniphest-task-summary-css' => '61d1667e', - 'multirow-row-manager' => '5b54c823', - 'owners-path-editor' => '2a8b62d9', - 'owners-path-editor-css' => 'fa7c13ef', - 'paste-css' => 'b37bcd38', - 'path-typeahead' => 'ad486db3', - 'people-picture-menu-item-css' => 'fe8e07cf', - 'people-profile-css' => '2ea2daa1', - 'phabricator-action-list-view-css' => '1b0085b2', - 'phabricator-busy' => '5202e831', - 'phabricator-chatlog-css' => 'abdc76ee', - 'phabricator-content-source-view-css' => 'cdf0d579', - 'phabricator-core-css' => 'b3ebd90d', - 'phabricator-countdown-css' => 'bff8012f', - 'phabricator-darklog' => '3b869402', - 'phabricator-darkmessage' => '26cd4b73', - 'phabricator-dashboard-css' => '5a205b9d', - 'phabricator-diff-changeset' => 'd7d3ba75', - 'phabricator-diff-changeset-list' => 'cc2c5de5', - 'phabricator-diff-inline' => '9c775532', - 'phabricator-diff-inline-content-state' => 'aa51efb4', - 'phabricator-diff-path-view' => '8207abf9', - 'phabricator-diff-tree-view' => '5d83623b', - 'phabricator-drag-and-drop-file-upload' => '4370900d', - 'phabricator-draggable-list' => '0169e425', - 'phabricator-fatal-config-template-css' => '20babf50', - 'phabricator-favicon' => '7930776a', - 'phabricator-feed-css' => 'd8b6e3f8', - 'phabricator-file-upload' => 'ab85e184', - 'phabricator-flag-css' => '2b77be8d', - 'phabricator-keyboard-shortcut' => '1a844c06', - 'phabricator-keyboard-shortcut-manager' => '81debc48', - 'phabricator-main-menu-view' => 'bcec20f0', - 'phabricator-nav-view-css' => '423f92cc', - 'phabricator-notification' => 'a9b91e3f', - 'phabricator-notification-css' => '30240bd2', - 'phabricator-notification-menu-css' => '4df1ee30', - 'phabricator-object-selector-css' => 'ee77366f', - 'phabricator-phtize' => '2f1db1ed', - 'phabricator-prefab' => '5793d835', - 'phabricator-remarkup-css' => '5baa3bd9', - 'phabricator-search-results-css' => '9ea70ace', - 'phabricator-shaped-request' => '995f5102', - 'phabricator-slowvote-css' => '1694baed', - 'phabricator-source-code-view-css' => '03d7ac28', - 'phabricator-standard-page-view' => 'a374f94c', - 'phabricator-textareautils' => 'f340a484', - 'phabricator-title' => '43bc9360', - 'phabricator-tooltip' => '83754533', - 'phabricator-ui-example-css' => 'b4795059', - 'phabricator-zindex-css' => 'ac3bfcd4', - 'phame-css' => 'bb442327', - 'pholio-css' => '88ef5ef1', - 'pholio-edit-css' => '4df55b3b', - 'pholio-inline-comments-css' => '722b48c2', - 'phortune-credit-card-form' => 'd12d214f', - 'phortune-credit-card-form-css' => '3b9868a8', - 'phortune-css' => '508a1a5e', - 'phortune-invoice-css' => '4436b241', - 'phrequent-css' => 'bd79cc67', - 'phriction-document-css' => '03380da0', - 'phui-action-panel-css' => '6c386cbf', - 'phui-badge-view-css' => '666e25ad', - 'phui-basic-nav-view-css' => '56ebd66d', - 'phui-big-info-view-css' => '362ad37b', - 'phui-box-css' => '5ed3b8cb', - 'phui-bulk-editor-css' => '374d5e30', - 'phui-button-bar-css' => 'a4aa75c4', - 'phui-button-css' => 'ea704902', - 'phui-button-simple-css' => '1ff278aa', - 'phui-calendar-css' => 'f11073aa', - 'phui-calendar-day-css' => '9597d706', - 'phui-calendar-list-css' => 'ccd7e4e2', - 'phui-calendar-month-css' => 'cb758c42', - 'phui-chart-css' => '14df9ae3', - 'phui-cms-css' => '8c05c41e', - 'phui-comment-form-css' => '68a2d99a', - 'phui-comment-panel-css' => 'ec4e31c0', - 'phui-crumbs-view-css' => '614f43cf', - 'phui-curtain-object-ref-view-css' => '51d93266', - 'phui-curtain-view-css' => '68c5efb6', - 'phui-document-summary-view-css' => 'b068eed1', - 'phui-document-view-css' => '52b748a5', - 'phui-document-view-pro-css' => 'b9613a10', - 'phui-feed-story-css' => 'a0c05029', - 'phui-font-icon-base-css' => '303c9b87', - 'phui-fontkit-css' => '1ec937e5', - 'phui-form-css' => '1f177cb7', - 'phui-form-view-css' => '01b796c0', - 'phui-formation-view-css' => 'd2dec8ed', - 'phui-head-thing-view-css' => 'd7f293df', - 'phui-header-view-css' => '36c86a58', - 'phui-hovercard' => '6199f752', - 'phui-hovercard-list' => 'de4b4919', - 'phui-hovercard-view-css' => '6ca90fa0', - 'phui-icon-set-selector-css' => '7aa5f3ec', - 'phui-icon-view-css' => '4cbc684a', - 'phui-image-mask-css' => '62c7f4d2', - 'phui-info-view-css' => 'a10a909b', - 'phui-inline-comment-view-css' => '9863a85e', - 'phui-invisible-character-view-css' => 'c694c4a4', - 'phui-left-right-css' => '68513c34', - 'phui-lightbox-css' => '4ebf22da', - 'phui-list-view-css' => '0c04affd', - 'phui-object-box-css' => 'b8d7eea0', - 'phui-oi-big-ui-css' => 'fa74cc35', - 'phui-oi-color-css' => 'b517bfa0', - 'phui-oi-drag-ui-css' => 'da15d3dc', - 'phui-oi-flush-ui-css' => '490e2e2e', - 'phui-oi-list-view-css' => 'af98a277', - 'phui-oi-simple-ui-css' => '6a30fa46', - 'phui-pager-css' => 'd022c7ad', - 'phui-pinboard-view-css' => '1f08f5d8', - 'phui-policy-section-view-css' => '139fdc64', - 'phui-property-list-view-css' => '5adf7078', - 'phui-remarkup-preview-css' => '91767007', - 'phui-segment-bar-view-css' => '5166b370', - 'phui-spacing-css' => 'b05cadc3', - 'phui-status-list-view-css' => '293b5dad', - 'phui-tag-view-css' => 'fb811341', - 'phui-theme-css' => '35883b37', - 'phui-timeline-view-css' => '2d32d7a9', - 'phui-two-column-view-css' => 'f96d319f', - 'phui-workboard-color-css' => 'e86de308', - 'phui-workboard-view-css' => '74fc9d98', - 'phui-workcard-view-css' => '913441b6', - 'phui-workpanel-view-css' => '3ae89b20', - 'phuix-action-list-view' => 'c68f183f', - 'phuix-action-view' => 'a8f573a9', - 'phuix-autocomplete' => '2fbe234d', - 'phuix-button-view' => '55a24e84', - 'phuix-dropdown-menu' => 'b557770a', - 'phuix-form-control-view' => '38c1f3fb', - 'phuix-formation-column-view' => '4bcc1f78', - 'phuix-formation-flank-view' => '6648270a', - 'phuix-formation-view' => 'cef53b3e', - 'phuix-icon-view' => 'a5257c4e', - 'policy-css' => 'ceb56a08', - 'policy-edit-css' => '8794e2ed', - 'policy-transaction-detail-css' => 'c02b8384', - 'ponder-view-css' => '05a09d0a', - 'project-card-view-css' => 'a9f2c2dd', - 'project-triggers-css' => 'cd9c8bb9', - 'project-view-css' => '567858b3', - 'setup-issue-css' => '5eed85b2', - 'sprite-login-css' => '18b368a6', - 'sprite-tokens-css' => 'f1896dc5', - 'syntax-default-css' => '055fc231', - 'syntax-highlighting-css' => '548567f6', - 'tokens-css' => 'ce5a50bd', - 'trigger-rule' => '41b7b4f6', - 'trigger-rule-control' => '5faf27b9', - 'trigger-rule-editor' => 'b49fd60c', - 'trigger-rule-type' => '4feea7d3', - 'typeahead-browse-css' => 'b7ed02d2', - 'unhandled-exception-css' => '9ecfc00d', + 'Chart.min' => '0c0a9abf', + 'almanac-css' => 'cef2e49b', + 'aphront-bars' => '00ff5fe9', + 'aphront-dark-console-css' => '38e4ea36', + 'aphront-dialog-view-css' => 'a084e330', + 'aphront-list-filter-view-css' => '3d222b09', + 'aphront-multi-column-view-css' => 'b9f74f2a', + 'aphront-panel-view-css' => '9e6903c5', + 'aphront-table-view-css' => '5a4d3a38', + 'aphront-tokenizer-control-css' => '8775367f', + 'aphront-tooltip-css' => 'ad116c35', + 'aphront-typeahead-control-css' => '421db43d', + 'application-search-view-css' => '1ce21548', + 'auth-css' => '2b77b6e3', + 'bulk-job-css' => 'e82e5d63', + 'conduit-api-css' => '66faab49', + 'config-options-css' => '76627af5', + 'conpherence-color-css' => '180cfb5f', + 'conpherence-durable-column-view' => '310813dd', + 'conpherence-header-pane-css' => 'fda7e0bb', + 'conpherence-menu-css' => '0291ad8b', + 'conpherence-message-pane-css' => '57759b60', + 'conpherence-notification-css' => '5d16c9b4', + 'conpherence-participant-pane-css' => 'fd95bab6', + 'conpherence-thread-manager' => '7fa6d478', + 'conpherence-transaction-css' => '68ccd991', + 'd3' => '091f6716', + 'diff-tree-view-css' => '4463923c', + 'differential-changeset-view-css' => '9d1e8849', + 'differential-core-view-css' => '948b6550', + 'differential-revision-add-comment-css' => 'ddeac4fb', + 'differential-revision-comment-css' => '09d505d1', + 'differential-revision-history-css' => '5db10da7', + 'differential-revision-list-css' => 'a4b47824', + 'differential-table-of-contents-css' => '29c16bdc', + 'diffusion-css' => '4bb55f03', + 'diffusion-icons-css' => '77dd2265', + 'diffusion-readme-css' => 'd48f0924', + 'diffusion-repository-css' => '7b9e177a', + 'diviner-shared-css' => '6d533b5e', + 'font-fontawesome' => '067fc518', + 'font-lato' => 'ae392e67', + 'fuel-grid-css' => 'a82e3be1', + 'fuel-handle-list-css' => '7284f3f8', + 'fuel-map-css' => 'e08255b0', + 'fuel-menu-css' => '8c4660dd', + 'global-drag-and-drop-css' => '013aa9f1', + 'harbormaster-css' => 'c4d2c934', + 'herald-css' => '97d92574', + 'herald-rule-editor' => 'b377c6c1', + 'herald-test-css' => '63d07577', + 'inline-comment-summary-css' => '232a5fae', + 'javelin-aphlict' => '6778cd25', + 'javelin-behavior' => '823998c6', + 'javelin-behavior-aphlict-dropdown' => 'da025579', + 'javelin-behavior-aphlict-listen' => '5b4d8666', + 'javelin-behavior-aphlict-status' => 'a4e03d76', + 'javelin-behavior-aphront-basic-tokenizer' => '3a365f3a', + 'javelin-behavior-aphront-drag-and-drop-textarea' => '5f454b6c', + 'javelin-behavior-aphront-form-disable-on-submit' => '9e54c007', + 'javelin-behavior-aphront-more' => '39d8efb6', + 'javelin-behavior-audio-source' => '49a68708', + 'javelin-behavior-audit-preview' => '95a14f58', + 'javelin-behavior-badge-view' => '5f013b5e', + 'javelin-behavior-bulk-editor' => '96f0778d', + 'javelin-behavior-bulk-job-reload' => 'fa7092c6', + 'javelin-behavior-calendar-month-view' => '610587d5', + 'javelin-behavior-choose-control' => '667c3061', + 'javelin-behavior-comment-actions' => 'dabe390e', + 'javelin-behavior-config-reorder-fields' => 'bdac8af0', + 'javelin-behavior-conpherence-menu' => '8312ac01', + 'javelin-behavior-conpherence-participant-pane' => '27e29e59', + 'javelin-behavior-conpherence-pontificate' => 'b48f5e2c', + 'javelin-behavior-conpherence-search' => '763dd0b2', + 'javelin-behavior-countdown-timer' => '8c5ff0ff', + 'javelin-behavior-dark-console' => '81f19452', + 'javelin-behavior-dashboard-async-panel' => '87b119e1', + 'javelin-behavior-dashboard-move-panels' => '31a1c4a1', + 'javelin-behavior-dashboard-query-panel-select' => 'bae4d536', + 'javelin-behavior-dashboard-tab-panel' => 'cb1ed345', + 'javelin-behavior-day-view' => '7a61e28c', + 'javelin-behavior-desktop-notifications-control' => 'e4366d54', + 'javelin-behavior-detect-timezone' => '66d4c8ef', + 'javelin-behavior-device' => 'fab38736', + 'javelin-behavior-differential-diff-radios' => '27187c26', + 'javelin-behavior-differential-populate' => 'cb8ced97', + 'javelin-behavior-diffusion-commit-branches' => '63b4e112', + 'javelin-behavior-diffusion-commit-graph' => '8cc0bee0', + 'javelin-behavior-diffusion-locate-file' => 'd9158762', + 'javelin-behavior-diffusion-pull-lastmodified' => '662be695', + 'javelin-behavior-document-engine' => '79b71246', + 'javelin-behavior-doorkeeper-tag' => '66673225', + 'javelin-behavior-drydock-live-operation-status' => 'bcb1469f', + 'javelin-behavior-durable-column' => 'e57f4bdd', + 'javelin-behavior-editengine-reorder-configs' => 'efce8717', + 'javelin-behavior-editengine-reorder-fields' => 'e3cebb9a', + 'javelin-behavior-event-all-day' => 'e7cb91b5', + 'javelin-behavior-fancy-datepicker' => 'e9ff303a', + 'javelin-behavior-global-drag-and-drop' => 'b44cb8be', + 'javelin-behavior-harbormaster-log' => '8233f53b', + 'javelin-behavior-herald-rule-editor' => 'c76477e6', + 'javelin-behavior-high-security-warning' => '13933ffe', + 'javelin-behavior-history-install' => '32636e27', + 'javelin-behavior-icon-composer' => '9151195b', + 'javelin-behavior-launch-icon-composer' => '0bc272f6', + 'javelin-behavior-lightbox-attachments' => '3181860e', + 'javelin-behavior-line-chart' => '479d39e8', + 'javelin-behavior-linked-container' => '1df032b1', + 'javelin-behavior-maniphest-batch-selector' => '44b74260', + 'javelin-behavior-maniphest-list-editor' => '3dfbd876', + 'javelin-behavior-owners-path-editor' => 'f1c80c78', + 'javelin-behavior-passphrase-credential-control' => 'dd66ccfa', + 'javelin-behavior-phabricator-autofocus' => '3027bdc5', + 'javelin-behavior-phabricator-clipboard-copy' => '7a17e183', + 'javelin-behavior-phabricator-gesture' => 'e08d87ef', + 'javelin-behavior-phabricator-gesture-example' => '180a90b7', + 'javelin-behavior-phabricator-keyboard-pager' => '7dcfc62b', + 'javelin-behavior-phabricator-keyboard-shortcuts' => '9022542a', + 'javelin-behavior-phabricator-line-linker' => '4c8dd68a', + 'javelin-behavior-phabricator-notification-example' => 'd337bd45', + 'javelin-behavior-phabricator-object-selector' => '8bf10492', + 'javelin-behavior-phabricator-oncopy' => '92f45381', + 'javelin-behavior-phabricator-remarkup-assist' => '222be76a', + 'javelin-behavior-phabricator-reveal-content' => '5fba5787', + 'javelin-behavior-phabricator-search-typeahead' => 'cb3fa126', + 'javelin-behavior-phabricator-show-older-transactions' => 'ae72a2a1', + 'javelin-behavior-phabricator-tooltips' => '98c0c9d1', + 'javelin-behavior-phabricator-transaction-comment-form' => '82cb3153', + 'javelin-behavior-phabricator-transaction-list' => '333ce90f', + 'javelin-behavior-phabricator-watch-anchor' => '2fa37c8a', + 'javelin-behavior-pholio-mock-edit' => '6e85bec4', + 'javelin-behavior-pholio-mock-view' => 'edd6d697', + 'javelin-behavior-phui-dropdown-menu' => 'e76e2c73', + 'javelin-behavior-phui-file-upload' => '9c9cd23d', + 'javelin-behavior-phui-hovercards' => '319cf55a', + 'javelin-behavior-phui-selectable-list' => '8084fa7b', + 'javelin-behavior-phui-submenu' => '58bc69cb', + 'javelin-behavior-phui-tab-group' => '1133d6e4', + 'javelin-behavior-phui-timer-control' => '66f14553', + 'javelin-behavior-phuix-example' => '6f88d4f4', + 'javelin-behavior-policy-control' => 'd88500e0', + 'javelin-behavior-policy-rule-editor' => '515a18f6', + 'javelin-behavior-project-boards' => '40174fe1', + 'javelin-behavior-project-create' => 'b2d6b2d6', + 'javelin-behavior-quicksand-blacklist' => 'd52a680e', + 'javelin-behavior-read-only-warning' => 'b6ae7ef5', + 'javelin-behavior-redirect' => '8796e6c1', + 'javelin-behavior-refresh-csrf' => '9729be58', + 'javelin-behavior-remarkup-load-image' => 'd7e34a5b', + 'javelin-behavior-remarkup-preview' => '9e977c33', + 'javelin-behavior-reorder-applications' => '4d1fb46c', + 'javelin-behavior-reorder-columns' => 'c829538d', + 'javelin-behavior-reorder-profile-menu-items' => '892f8706', + 'javelin-behavior-repository-crossreference' => '0a9e2f6d', + 'javelin-behavior-scrollbar' => '225f8317', + 'javelin-behavior-search-reorder-queries' => 'e8754d9a', + 'javelin-behavior-select-content' => 'a0b836b0', + 'javelin-behavior-select-on-click' => 'b07a98e6', + 'javelin-behavior-setup-check-https' => '1bedf740', + 'javelin-behavior-stripe-payment-form' => '9d56ea6d', + 'javelin-behavior-test-payment-form' => '39c77ea0', + 'javelin-behavior-time-typeahead' => '70d377b6', + 'javelin-behavior-toggle-class' => '4c29854b', + 'javelin-behavior-toggle-widget' => 'fd92bf06', + 'javelin-behavior-trigger-rule-editor' => '3a06ac82', + 'javelin-behavior-typeahead-browse' => 'a6e925d3', + 'javelin-behavior-typeahead-search' => '74175cff', + 'javelin-behavior-user-menu' => '1eed45ed', + 'javelin-behavior-view-placeholder' => '71e022dc', + 'javelin-behavior-workflow' => 'c49be7c7', + 'javelin-chart' => '39bf659d', + 'javelin-chart-curtain-view' => '959b7022', + 'javelin-chart-function-label' => '9c100943', + 'javelin-color' => '54514c9a', + 'javelin-cookie' => '0a842c5a', + 'javelin-diffusion-locate-file-source' => '31ebed0b', + 'javelin-dom' => '4c7e6cfb', + 'javelin-dynval' => 'cda4ac41', + 'javelin-event' => 'ce95eb52', + 'javelin-external-editor-link-engine' => 'e9da5466', + 'javelin-fx' => '7dbf81e3', + 'javelin-history' => '09ffdb7a', + 'javelin-install' => 'dde695a1', + 'javelin-json' => '3074b5a2', + 'javelin-leader' => 'b10edcf4', + 'javelin-magical-init' => '723d06f9', + 'javelin-mask' => '03bfa877', + 'javelin-quicksand' => '0ad7e653', + 'javelin-reactor' => '50c03f43', + 'javelin-reactor-dom' => 'acd57fb7', + 'javelin-reactor-node-calmer' => '1a7c0789', + 'javelin-reactornode' => '27e4ebfa', + 'javelin-request' => 'c34271ef', + 'javelin-resource' => '290e1d73', + 'javelin-routable' => 'ee039e34', + 'javelin-router' => 'a713fb57', + 'javelin-scrollbar' => '554854d3', + 'javelin-sound' => 'aff6fa58', + 'javelin-stratcom' => '3e63c3da', + 'javelin-tokenizer' => '37381107', + 'javelin-typeahead' => '3b4a8853', + 'javelin-typeahead-composite-source' => '8f32ee03', + 'javelin-typeahead-normalizer' => '0314da2a', + 'javelin-typeahead-ondemand-source' => 'ea897c68', + 'javelin-typeahead-preloaded-source' => '471d9cb0', + 'javelin-typeahead-source' => 'a8149272', + 'javelin-typeahead-static-source' => '2f8a42fc', + 'javelin-uri' => '4b0b997a', + 'javelin-util' => '4ff621eb', + 'javelin-vector' => '0a734ab1', + 'javelin-view' => '1c4dfd8f', + 'javelin-view-html' => '395db5df', + 'javelin-view-interpreter' => 'fb72774b', + 'javelin-view-renderer' => '7b5bd01c', + 'javelin-view-visitor' => 'b6b17353', + 'javelin-websocket' => 'eb6c1fee', + 'javelin-workboard-board' => '556e7da0', + 'javelin-workboard-card' => '0b61ca0a', + 'javelin-workboard-card-template' => '0e6cd2c5', + 'javelin-workboard-column' => '86c9bec8', + 'javelin-workboard-controller' => '6a85a739', + 'javelin-workboard-drop-effect' => '6803f974', + 'javelin-workboard-header' => '868e951d', + 'javelin-workboard-header-template' => '4bead335', + 'javelin-workboard-order-template' => 'bb4c7566', + 'javelin-workflow' => 'e754ba8c', + 'jquery-js' => 'af4e8be2', + 'jquery-ui-css' => 'c686b2c7', + 'jquery-ui-js' => '4b972dbc', + 'jquery.min' => '81da9b0e', + 'maniphest-report-css' => 'fee2dc7f', + 'maniphest-task-edit-css' => '17ca7755', + 'maniphest-task-summary-css' => '801801c3', + 'multirow-row-manager' => '8cf58b04', + 'owners-path-editor' => 'c1810d04', + 'owners-path-editor-css' => '8d54df52', + 'paste-css' => '6c541d60', + 'path-typeahead' => 'ae7d493d', + 'people-picture-menu-item-css' => '0ad886aa', + 'people-profile-css' => '516e6a12', + 'phabricator-action-list-view-css' => '712619b3', + 'phabricator-busy' => '5c4ab42a', + 'phabricator-chatlog-css' => 'f0969b46', + 'phabricator-content-source-view-css' => '2b7b096d', + 'phabricator-core-css' => 'bc564b67', + 'phabricator-countdown-css' => 'b81584b0', + 'phabricator-darklog' => '69f214f4', + 'phabricator-darkmessage' => 'f13d3e3a', + 'phabricator-dashboard-css' => '0c1f90e6', + 'phabricator-diff-changeset' => 'd33517b0', + 'phabricator-diff-changeset-list' => '1683d069', + 'phabricator-diff-inline' => '225287db', + 'phabricator-diff-inline-content-state' => '269f2ce5', + 'phabricator-diff-path-view' => 'a985aa58', + 'phabricator-diff-tree-view' => '28e74c8a', + 'phabricator-drag-and-drop-file-upload' => '5337ec83', + 'phabricator-draggable-list' => '2ecba827', + 'phabricator-fatal-config-template-css' => '41c22f41', + 'phabricator-favicon' => '0d352869', + 'phabricator-feed-css' => 'a1631e46', + 'phabricator-file-upload' => 'f20f2119', + 'phabricator-flag-css' => '4f448b39', + 'phabricator-keyboard-shortcut' => '71c55d20', + 'phabricator-keyboard-shortcut-manager' => '81770869', + 'phabricator-main-menu-view' => '3b66b652', + 'phabricator-nav-view-css' => '763d9410', + 'phabricator-notification' => 'e8ea2329', + 'phabricator-notification-css' => '2093d822', + 'phabricator-notification-menu-css' => '02fb87e2', + 'phabricator-object-selector-css' => 'aedefb77', + 'phabricator-phtize' => '0c2e2e99', + 'phabricator-prefab' => 'ddcf7994', + 'phabricator-remarkup-css' => '86c7abd9', + 'phabricator-search-results-css' => 'a501eb57', + 'phabricator-shaped-request' => '79e07647', + 'phabricator-slowvote-css' => 'a95abbfe', + 'phabricator-source-code-view-css' => 'ea851bff', + 'phabricator-standard-page-view' => '9feb4eae', + 'phabricator-textareautils' => '5d79b875', + 'phabricator-title' => 'dc4dc986', + 'phabricator-tooltip' => '7e67b544', + 'phabricator-ui-example-css' => '3bb522bb', + 'phabricator-zindex-css' => 'f78d61f3', + 'phame-css' => '81285d4e', + 'pholio-css' => '1792b28d', + 'pholio-edit-css' => '1fb1c5ce', + 'pholio-inline-comments-css' => '57bfe2c5', + 'phortune-credit-card-form' => '685f95ed', + 'phortune-credit-card-form-css' => '170d1116', + 'phortune-css' => '2dae92d7', + 'phortune-invoice-css' => 'a6fe8f13', + 'phrequent-css' => '7d5af048', + 'phriction-document-css' => '176dee98', + 'phui-action-panel-css' => 'a6750bff', + 'phui-badge-view-css' => '65615ada', + 'phui-basic-nav-view-css' => 'fb533658', + 'phui-big-info-view-css' => '6d8006d9', + 'phui-box-css' => '34a585e9', + 'phui-bulk-editor-css' => 'd4a48524', + 'phui-button-bar-css' => '103bddc5', + 'phui-button-css' => 'dca27a4d', + 'phui-button-simple-css' => 'bf9b167e', + 'phui-calendar-css' => '7990005c', + 'phui-calendar-day-css' => 'd17a124a', + 'phui-calendar-list-css' => '70e6796e', + 'phui-calendar-month-css' => 'e85ac216', + 'phui-chart-css' => '0d9514c3', + 'phui-cms-css' => '6982140b', + 'phui-comment-form-css' => '1a60a95e', + 'phui-comment-panel-css' => 'af171e27', + 'phui-crumbs-view-css' => '824b38f8', + 'phui-curtain-object-ref-view-css' => 'c5cc2a97', + 'phui-curtain-view-css' => '82abe39e', + 'phui-document-summary-view-css' => '4281c81c', + 'phui-document-view-css' => 'd4bf2736', + 'phui-document-view-pro-css' => '8b6d139b', + 'phui-feed-story-css' => '1c892ee8', + 'phui-font-icon-base-css' => 'f1a6c60c', + 'phui-fontkit-css' => 'cbc011de', + 'phui-form-css' => '95fe0ad3', + 'phui-form-view-css' => '8c117351', + 'phui-formation-view-css' => 'a1c04196', + 'phui-head-thing-view-css' => '8cbc3e5a', + 'phui-header-view-css' => 'eed2b7dc', + 'phui-hovercard' => 'd7707183', + 'phui-hovercard-list' => '92d63c24', + 'phui-hovercard-view-css' => '9e537a9d', + 'phui-icon-set-selector-css' => '1f214f73', + 'phui-icon-view-css' => '84e6d519', + 'phui-image-mask-css' => '58416b6b', + 'phui-info-view-css' => '9cf36210', + 'phui-inline-comment-view-css' => '717e3483', + 'phui-invisible-character-view-css' => '67fac570', + 'phui-left-right-css' => 'bc561f76', + 'phui-lightbox-css' => 'a60db19d', + 'phui-list-view-css' => '56c65dfe', + 'phui-object-box-css' => 'da097b4d', + 'phui-oi-big-ui-css' => 'bc69e075', + 'phui-oi-color-css' => '993aa3c0', + 'phui-oi-drag-ui-css' => '272870cf', + 'phui-oi-flush-ui-css' => '64b01337', + 'phui-oi-list-view-css' => 'dc4d3fe5', + 'phui-oi-simple-ui-css' => '17656c73', + 'phui-pager-css' => '67f21790', + 'phui-pinboard-view-css' => '9da87545', + 'phui-policy-section-view-css' => '724c4b80', + 'phui-property-list-view-css' => 'eaadaa72', + 'phui-remarkup-preview-css' => '4716e81e', + 'phui-segment-bar-view-css' => 'ed889b90', + 'phui-spacing-css' => '660205d0', + 'phui-status-list-view-css' => '63a83125', + 'phui-tag-view-css' => '0773e57c', + 'phui-theme-css' => '108e564a', + 'phui-timeline-view-css' => '6f1d1ae7', + 'phui-two-column-view-css' => 'c5ff8d17', + 'phui-workboard-color-css' => 'dcea01dd', + 'phui-workboard-view-css' => '5179d214', + 'phui-workcard-view-css' => 'e01ca2e0', + 'phui-workpanel-view-css' => 'f6b9fd3a', + 'phuix-action-list-view' => '53ff28b6', + 'phuix-action-view' => '4f86f4eb', + 'phuix-autocomplete' => '3746c3eb', + 'phuix-button-view' => 'b7c340b9', + 'phuix-dropdown-menu' => 'ae501775', + 'phuix-form-control-view' => '78e64734', + 'phuix-formation-column-view' => '24e6885b', + 'phuix-formation-flank-view' => 'f5aa2659', + 'phuix-formation-view' => 'd89b0a1b', + 'phuix-icon-view' => 'fc62fade', + 'policy-css' => 'aee81621', + 'policy-edit-css' => '609fdd48', + 'policy-transaction-detail-css' => '3d0b461d', + 'ponder-view-css' => 'b51b545f', + 'project-card-view-css' => '52c616b0', + 'project-triggers-css' => '565be023', + 'project-view-css' => '87ba8a77', + 'setup-issue-css' => '539e7d71', + 'show-graph' => 'c1dff964', + 'sprite-login-css' => '511a7d30', + 'sprite-tokens-css' => '8fd81966', + 'syntax-default-css' => 'f1d67c7e', + 'syntax-highlighting-css' => '66087f6a', + 'timetracker-js' => 'ad4427cb', + 'tokens-css' => 'e60c1cd7', + 'trigger-rule' => '04fa050d', + 'trigger-rule-control' => '69f2d489', + 'trigger-rule-editor' => 'd8c8b363', + 'trigger-rule-type' => 'a0632844', + 'typeahead-browse-css' => '1b90f79f', + 'unhandled-exception-css' => '237a4011', ), 'requires' => array( - '0116d3e8' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-stratcom', - ), - '01384686' => array( - 'javelin-behavior', - 'javelin-uri', - 'phabricator-notification', + '0314da2a' => array( + 'javelin-install', ), - '0169e425' => array( + '03bfa877' => array( 'javelin-install', 'javelin-dom', + ), + '09ffdb7a' => array( 'javelin-stratcom', + 'javelin-install', + 'javelin-uri', 'javelin-util', - 'javelin-vector', - 'javelin-magical-init', ), - '022516b4' => array( + '0a734ab1' => array( + 'javelin-install', + 'javelin-event', + ), + '0a842c5a' => array( 'javelin-install', 'javelin-util', - 'javelin-websocket', - 'javelin-leader', - 'javelin-json', ), - '02cb4398' => array( + '0a9e2f6d' => array( 'javelin-behavior', 'javelin-dom', - 'phortune-credit-card-form', - ), - '030b4f7a' => array( 'javelin-stratcom', - 'javelin-install', 'javelin-uri', - 'javelin-util', ), - '0392a5d8' => array( + '0ad7e653' => array( 'javelin-install', ), - '03e8891f' => array( + '0b61ca0a' => array( 'javelin-install', ), - '04f8a1e3' => array( + '0bc272f6' => array( 'javelin-behavior', - 'javelin-stratcom', 'javelin-dom', 'javelin-workflow', ), - '05d290ef' => array( - 'javelin-install', + '0c2e2e99' => array( 'javelin-util', ), - '070679fe' => array( - 'javelin-behavior', - 'javelin-stratcom', + '0d352869' => array( + 'javelin-install', 'javelin-dom', - 'javelin-uri', - 'phabricator-notification', ), - '0889b835' => array( + '0e6cd2c5' => array( 'javelin-install', - 'javelin-event', - 'javelin-util', - 'javelin-magical-init', ), - '0922e81d' => array( - 'herald-rule-editor', - 'javelin-behavior', + '103bddc5' => array( + 'phui-button-css', + 'phui-button-simple-css', ), - '0ad8d31f' => array( + '1133d6e4' => array( 'javelin-behavior', 'javelin-stratcom', - 'javelin-workflow', 'javelin-dom', - 'phabricator-draggable-list', ), - '0d2490ce' => array( + '13933ffe' => array( + 'javelin-behavior', + 'javelin-uri', + 'phabricator-notification', + ), + '1683d069' => array( 'javelin-install', + 'phuix-button-view', + 'phabricator-diff-tree-view', ), - '0d915ff5' => array( - 'javelin-behavior', - 'javelin-stratcom', - 'javelin-dom', - 'javelin-history', - 'javelin-external-editor-link-engine', + '17656c73' => array( + 'phui-oi-list-view-css', ), - '0eaa33a9' => array( + '180a90b7' => array( + 'javelin-stratcom', 'javelin-behavior', + 'javelin-vector', 'javelin-dom', - 'javelin-util', - 'phuix-dropdown-menu', - 'phuix-action-list-view', - 'phuix-action-view', - 'javelin-workflow', - 'phuix-icon-view', ), - '111bfd2d' => array( + '1a7c0789' => array( 'javelin-install', + 'javelin-reactor', + 'javelin-util', ), - '1325b731' => array( + '1bedf740' => array( 'javelin-behavior', 'javelin-uri', - 'phabricator-keyboard-shortcut', + 'phabricator-notification', ), - '139ef688' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-stratcom', + '1c4dfd8f' => array( + 'javelin-install', 'javelin-util', ), - '14c7ab36' => array( + '1df032b1' => array( 'javelin-behavior', - 'javelin-stratcom', 'javelin-dom', - 'javelin-mask', - 'javelin-util', - 'phuix-icon-view', - 'phabricator-busy', ), - '183738e6' => array( + '1eed45ed' => array( + 'javelin-behavior', + ), + '222be76a' => array( 'javelin-behavior', - 'javelin-behavior-device', 'javelin-stratcom', + 'javelin-dom', + 'phabricator-phtize', + 'phabricator-textareautils', + 'javelin-workflow', 'javelin-vector', - 'phui-hovercard', - 'phui-hovercard-list', + 'phuix-autocomplete', + 'javelin-mask', ), - '1a844c06' => array( - 'javelin-install', - 'javelin-util', - 'phabricator-keyboard-shortcut-manager', + '225287db' => array( + 'javelin-dom', + 'phabricator-diff-inline-content-state', ), - '1b6acc2a' => array( - 'javelin-magical-init', - 'javelin-util', + '225f8317' => array( + 'javelin-behavior', + 'javelin-scrollbar', ), - '1c850a26' => array( + '24e6885b' => array( 'javelin-install', - 'javelin-util', + 'javelin-dom', ), - '1cab0e9a' => array( - 'javelin-behavior', + '269f2ce5' => array( 'javelin-dom', - 'javelin-uri', - 'javelin-mask', - 'phabricator-drag-and-drop-file-upload', ), - '1cb7d027' => array( + '27187c26' => array( 'javelin-behavior', - 'javelin-typeahead-ondemand-source', - 'javelin-typeahead', - 'javelin-dom', - 'javelin-uri', - 'javelin-util', 'javelin-stratcom', - 'phabricator-prefab', - 'phuix-icon-view', + 'javelin-dom', + ), + '272870cf' => array( + 'phui-oi-list-view-css', ), - '1e413dc9' => array( + '27e29e59' => array( 'javelin-behavior', 'javelin-dom', + 'javelin-stratcom', + 'javelin-workflow', + 'javelin-util', + 'phabricator-notification', + 'conpherence-thread-manager', ), - '1ff278aa' => array( - 'phui-button-css', - ), - '202a2e85' => array( + '27e4ebfa' => array( 'javelin-install', - 'javelin-reactornode', - 'javelin-util', 'javelin-reactor', + 'javelin-util', + 'javelin-reactor-node-calmer', ), - '202bfa3f' => array( - 'javelin-behavior', - 'javelin-request', + '28e74c8a' => array( + 'javelin-dom', ), - '20514cc2' => array( + '290e1d73' => array( 'javelin-util', 'javelin-uri', 'javelin-install', ), - '225bbb98' => array( - 'javelin-install', - 'javelin-reactor', - 'javelin-util', - ), - '22ee68a5' => array( + '2ecba827' => array( 'javelin-install', - 'javelin-typeahead-source', + 'javelin-dom', + 'javelin-stratcom', 'javelin-util', + 'javelin-vector', + 'javelin-magical-init', ), - 23387297 => array( + '2f8a42fc' => array( 'javelin-install', - 'javelin-util', - 'javelin-request', 'javelin-typeahead-source', ), - 23631304 => array( - 'phui-fontkit-css', - ), - '242aa08b' => array( + '2fa37c8a' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', - ), - '242dedd0' => array( - 'javelin-stratcom', - 'javelin-behavior', 'javelin-vector', - 'javelin-dom', ), - '243d6c22' => array( + '3027bdc5' => array( 'javelin-behavior', 'javelin-dom', - 'javelin-stratcom', ), - '2539f834' => array( + '3074b5a2' => array( + 'javelin-install', + ), + '3181860e' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', - 'javelin-json', - 'phabricator-draggable-list', - ), - '2633bef7' => array( - 'multirow-row-manager', - 'javelin-install', + 'javelin-mask', 'javelin-util', - 'javelin-dom', + 'phuix-icon-view', + 'phabricator-busy', + ), + '319cf55a' => array( + 'javelin-behavior', + 'javelin-behavior-device', 'javelin-stratcom', - 'javelin-json', - 'phabricator-prefab', + 'javelin-vector', + 'phui-hovercard', + 'phui-hovercard-list', ), - '289bf236' => array( - 'javelin-install', + '31a1c4a1' => array( + 'javelin-behavior', + 'javelin-dom', 'javelin-util', - ), - '29819b75' => array( - 'phabricator-notification', 'javelin-stratcom', - 'javelin-behavior', + 'javelin-workflow', + 'phabricator-draggable-list', ), - '2a8b62d9' => array( - 'multirow-row-manager', + '31ebed0b' => array( 'javelin-install', - 'path-typeahead', 'javelin-dom', + 'javelin-typeahead-preloaded-source', 'javelin-util', - 'phabricator-prefab', - 'phuix-form-control-view', ), - '2bdadf1a' => array( + '32636e27' => array( 'javelin-behavior', - 'javelin-dom', - 'javelin-util', - 'javelin-request', - 'phabricator-shaped-request', + 'javelin-history', ), - '2e255291' => array( - 'javelin-install', - 'javelin-util', + '333ce90f' => array( + 'javelin-behavior', 'javelin-stratcom', + 'javelin-workflow', + 'javelin-dom', + 'javelin-uri', + 'phabricator-textareautils', ), - '2f1db1ed' => array( + 37381107 => array( + 'javelin-dom', 'javelin-util', + 'javelin-stratcom', + 'javelin-install', ), - '2fbe234d' => array( + '3746c3eb' => array( 'javelin-install', 'javelin-dom', 'phuix-icon-view', 'phabricator-prefab', ), - '308f9fe4' => array( + '395db5df' => array( 'javelin-install', + 'javelin-dom', + 'javelin-view-visitor', 'javelin-util', ), - '32755edb' => array( - 'javelin-install', - 'javelin-util', + '39bf659d' => array( + 'phui-chart-css', + 'd3', + 'javelin-chart-curtain-view', + 'javelin-chart-function-label', ), - '3277c62d' => array( + '39c77ea0' => array( 'javelin-behavior', 'javelin-dom', - 'javelin-json', - 'phabricator-drag-and-drop-file-upload', - 'phabricator-textareautils', + 'phortune-credit-card-form', ), - '32db8374' => array( + '39d8efb6' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', ), - 34450586 => array( - 'javelin-color', + '3a06ac82' => array( + 'javelin-behavior', + 'trigger-rule-editor', + 'trigger-rule', + 'trigger-rule-type', + ), + '3a365f3a' => array( + 'javelin-behavior', + 'phabricator-prefab', + ), + '3b4a8853' => array( 'javelin-install', + 'javelin-dom', + 'javelin-vector', 'javelin-util', ), - '34c53422' => array( + '3b66b652' => array( + 'phui-theme-css', + ), + '3dfbd876' => array( 'javelin-behavior', 'javelin-dom', 'javelin-stratcom', 'javelin-workflow', + 'javelin-fx', + 'javelin-util', ), - '34e2a838' => array( - 'aphront-typeahead-control-css', - 'phui-tag-view-css', + '3e63c3da' => array( + 'javelin-install', + 'javelin-event', + 'javelin-util', + 'javelin-magical-init', ), - '36821f8d' => array( + '40174fe1' => array( 'javelin-behavior', - 'javelin-util', 'javelin-dom', - 'javelin-stratcom', + 'javelin-util', 'javelin-vector', + 'javelin-stratcom', + 'javelin-workflow', + 'javelin-workboard-controller', + 'javelin-workboard-drop-effect', ), - '3829a3cf' => array( - 'javelin-behavior', - 'javelin-uri', - ), - '38a6cedb' => array( + '44b74260' => array( 'javelin-behavior', 'javelin-dom', 'javelin-stratcom', + 'javelin-util', ), - '38c1f3fb' => array( + '471d9cb0' => array( 'javelin-install', - 'javelin-dom', - ), - '398fdf13' => array( - 'javelin-behavior', - 'trigger-rule-editor', - 'trigger-rule', - 'trigger-rule-type', - ), - '3ae89b20' => array( - 'phui-workcard-view-css', + 'javelin-util', + 'javelin-request', + 'javelin-typeahead-source', ), - '3b4899b0' => array( + '479d39e8' => array( 'javelin-behavior', - 'phabricator-prefab', + 'javelin-dom', + 'javelin-chart', ), - '3dc5ad43' => array( + '49a68708' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-vector', 'javelin-dom', ), - '3eed1f2b' => array( + '4b0b997a' => array( + 'javelin-install', + 'javelin-util', + 'javelin-stratcom', + ), + '4bead335' => array( + 'javelin-install', + ), + '4c29854b' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', - 'javelin-workflow', - 'javelin-quicksand', - 'phabricator-phtize', - 'phabricator-drag-and-drop-file-upload', - 'phabricator-draggable-list', ), - '407ee861' => array( - 'javelin-behavior', - 'javelin-uri', - ), - '42c44e8b' => array( - 'javelin-behavior', - 'javelin-workflow', - 'javelin-json', - 'javelin-dom', - 'phabricator-keyboard-shortcut', - ), - '4370900d' => array( + '4c7e6cfb' => array( + 'javelin-magical-init', 'javelin-install', 'javelin-util', - 'javelin-request', - 'javelin-dom', - 'javelin-uri', - 'phabricator-file-upload', - ), - '43ba89a2' => array( - 'javelin-behavior', - 'javelin-dom', + 'javelin-vector', 'javelin-stratcom', - 'javelin-workflow', - 'javelin-util', - 'phabricator-notification', - 'conpherence-thread-manager', - ), - '43bc9360' => array( - 'javelin-install', ), - '44d48cd1' => array( + '4c8dd68a' => array( 'javelin-behavior', - 'javelin-dom', 'javelin-stratcom', - 'javelin-uri', + 'javelin-dom', + 'javelin-history', + 'javelin-external-editor-link-engine', ), - '457f4d16' => array( + '4d1fb46c' => array( 'javelin-behavior', 'javelin-stratcom', - 'javelin-util', + 'javelin-workflow', 'javelin-dom', - 'javelin-request', - 'phabricator-keyboard-shortcut', - 'phabricator-darklog', - 'phabricator-darkmessage', + 'phabricator-draggable-list', ), - '46116c01' => array( - 'javelin-request', - 'javelin-behavior', + '4f86f4eb' => array( + 'javelin-install', 'javelin-dom', - 'javelin-router', 'javelin-util', - 'phabricator-busy', ), - '47a0728b' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-request', + '50c03f43' => array( + 'javelin-install', + 'javelin-util', ), - '4842f137' => array( + '515a18f6' => array( 'javelin-behavior', - 'javelin-stratcom', - 'javelin-workflow', + 'multirow-row-manager', 'javelin-dom', - 'phabricator-draggable-list', + 'javelin-util', + 'phabricator-prefab', + 'javelin-json', ), - '48a8641f' => array( + '5337ec83' => array( 'javelin-install', - ), - '48fe33d0' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-stratcom', - 'javelin-workflow', 'javelin-util', + 'javelin-request', + 'javelin-dom', 'javelin-uri', + 'phabricator-file-upload', ), - '490e2e2e' => array( - 'phui-oi-list-view-css', - ), - '4a7fb02b' => array( - 'javelin-behavior', + '53ff28b6' => array( + 'javelin-install', 'javelin-dom', - 'phortune-credit-card-form', ), - '4ae58b5a' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-util', - 'javelin-workflow', - 'javelin-stratcom', - 'conpherence-thread-manager', + '54514c9a' => array( + 'javelin-install', ), - '4b671572' => array( - 'javelin-behavior', + '554854d3' => array( + 'javelin-install', 'javelin-dom', - 'javelin-util', - 'javelin-request', + 'javelin-stratcom', + 'javelin-vector', ), - '4bcc1f78' => array( + '556e7da0' => array( 'javelin-install', 'javelin-dom', + 'javelin-util', + 'javelin-stratcom', + 'javelin-workflow', + 'phabricator-draggable-list', + 'javelin-workboard-column', + 'javelin-workboard-header-template', + 'javelin-workboard-card-template', + 'javelin-workboard-order-template', ), - '4dffaeb2' => array( + '58bc69cb' => array( 'javelin-behavior', 'javelin-stratcom', - 'javelin-workflow', 'javelin-dom', - 'phuix-form-control-view', - 'phuix-icon-view', - 'javelin-behavior-phabricator-gesture', ), - '4e61fa88' => array( + '5b4d8666' => array( 'javelin-behavior', 'javelin-aphlict', 'javelin-stratcom', @@ -1398,300 +1362,244 @@ 'javelin-sound', 'phabricator-notification', ), - '4feea7d3' => array( - 'trigger-rule-control', - ), - '506aa3f4' => array( - 'javelin-behavior', - 'javelin-stratcom', - 'javelin-dom', - ), - '5202e831' => array( + '5c4ab42a' => array( 'javelin-install', 'javelin-dom', 'javelin-fx', ), - '52e3ff03' => array( - 'phui-chart-css', - 'd3', - 'javelin-chart-curtain-view', - 'javelin-chart-function-label', - ), - '541f81c3' => array( + '5d79b875' => array( 'javelin-install', + 'javelin-dom', + 'javelin-vector', ), - 54262396 => array( + '5f013b5e' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', - 'phabricator-phtize', - 'phabricator-textareautils', - 'javelin-workflow', - 'javelin-vector', - 'phuix-autocomplete', - 'javelin-mask', - ), - '548567f6' => array( - 'syntax-default-css', ), - '55a24e84' => array( - 'javelin-install', + '5f454b6c' => array( + 'javelin-behavior', 'javelin-dom', + 'javelin-json', + 'phabricator-drag-and-drop-file-upload', + 'phabricator-textareautils', ), - '55d7b788' => array( + '5fba5787' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', ), - '5793d835' => array( - 'javelin-install', - 'javelin-util', - 'javelin-dom', - 'javelin-typeahead', - 'javelin-tokenizer', - 'javelin-typeahead-preloaded-source', - 'javelin-typeahead-ondemand-source', + '63b4e112' => array( + 'javelin-behavior', 'javelin-dom', - 'javelin-stratcom', 'javelin-util', + 'javelin-request', ), - '5803b9e7' => array( - 'javelin-behavior', - 'javelin-util', - 'javelin-dom', - 'javelin-stratcom', - 'javelin-vector', - 'javelin-typeahead-static-source', + '64b01337' => array( + 'phui-oi-list-view-css', + ), + '66087f6a' => array( + 'syntax-default-css', ), - '58cb6a88' => array( + '662be695' => array( 'javelin-behavior', 'javelin-dom', 'javelin-util', - 'javelin-vector', - 'javelin-stratcom', 'javelin-workflow', - 'javelin-workboard-controller', - 'javelin-workboard-drop-effect', + 'javelin-json', ), - '5902260c' => array( - 'javelin-util', + 66673225 => array( + 'javelin-behavior', + 'javelin-dom', + 'javelin-json', + 'javelin-workflow', 'javelin-magical-init', ), - '5a6f6a06' => array( + '667c3061' => array( 'javelin-behavior', - 'javelin-quicksand', + 'javelin-stratcom', + 'javelin-dom', + 'javelin-workflow', ), - '5a79f6c3' => array( - 'javelin-install', - 'javelin-util', - 'javelin-request', - 'javelin-typeahead-source', + '66d4c8ef' => array( + 'javelin-behavior', + 'javelin-uri', + 'phabricator-notification', ), - '5aa1544e' => array( + '66f14553' => array( 'javelin-behavior', - 'javelin-util', 'javelin-stratcom', 'javelin-dom', - 'javelin-vector', - 'javelin-magical-init', - 'javelin-request', - 'javelin-history', - 'javelin-workflow', - 'javelin-mask', - 'javelin-behavior-device', - 'phabricator-keyboard-shortcut', ), - '5b54c823' => array( + '6778cd25' => array( 'javelin-install', - 'javelin-stratcom', - 'javelin-dom', 'javelin-util', + 'javelin-websocket', + 'javelin-leader', + 'javelin-json', ), - '5cf0501a' => array( - 'javelin-behavior', - 'javelin-stratcom', + '6803f974' => array( + 'javelin-install', 'javelin-dom', - 'phuix-dropdown-menu', ), - '5d83623b' => array( + '685f95ed' => array( + 'javelin-install', 'javelin-dom', + 'javelin-json', + 'javelin-workflow', + 'javelin-util', ), - '5faf27b9' => array( + '69f2d489' => array( 'phuix-form-control-view', ), - '60c3d405' => array( - 'phui-inline-comment-view-css', - ), - '60cd9241' => array( - 'javelin-behavior', - ), - '6199f752' => array( + '6a85a739' => array( 'javelin-install', 'javelin-dom', + 'javelin-util', 'javelin-vector', - 'javelin-request', - 'javelin-uri', - ), - '65bb0011' => array( - 'javelin-behavior', - 'javelin-dom', + 'javelin-stratcom', + 'javelin-workflow', + 'phabricator-drag-and-drop-file-upload', + 'javelin-workboard-board', ), - '66365ee2' => array( + '6e85bec4' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', + 'javelin-workflow', + 'javelin-quicksand', + 'phabricator-phtize', + 'phabricator-drag-and-drop-file-upload', + 'phabricator-draggable-list', ), - '6648270a' => array( + '6f88d4f4' => array( 'javelin-install', 'javelin-dom', + 'phuix-button-view', ), - '6a1583a8' => array( - 'javelin-behavior', - 'javelin-history', - ), - '6a162524' => array( + '70d377b6' => array( 'javelin-behavior', + 'javelin-util', 'javelin-dom', + 'javelin-stratcom', + 'javelin-vector', + 'javelin-typeahead-static-source', ), - '6a18c42e' => array( + '71c55d20' => array( 'javelin-install', + 'javelin-util', + 'phabricator-keyboard-shortcut-manager', ), - '6a30fa46' => array( - 'phui-oi-list-view-css', - ), - '6a85bc5a' => array( + '71e022dc' => array( 'javelin-behavior', 'javelin-dom', - 'javelin-json', - 'javelin-workflow', - 'javelin-magical-init', - ), - '6cfa0008' => array( - 'javelin-dom', - 'javelin-dynval', - 'javelin-reactor', - 'javelin-reactornode', + 'javelin-view-renderer', 'javelin-install', - 'javelin-util', ), - 70245195 => array( + '74175cff' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-workflow', 'javelin-dom', ), - '727a5a61' => array( - 'phuix-icon-view', + '763dd0b2' => array( + 'javelin-behavior', + 'javelin-dom', + 'javelin-util', + 'javelin-workflow', + 'javelin-stratcom', ), - '72960bc1' => array( + '78e64734' => array( 'javelin-install', - 'javelin-reactor', - 'javelin-util', - 'javelin-reactor-node-calmer', + 'javelin-dom', ), - '73ecc1f8' => array( + '79b71246' => array( 'javelin-behavior', - 'javelin-behavior-device', + 'javelin-dom', 'javelin-stratcom', - 'phabricator-tooltip', ), - 74446546 => array( + '79e07647' => array( + 'javelin-install', + 'javelin-util', + 'javelin-request', + 'javelin-router', + ), + '7a17e183' => array( 'javelin-behavior', 'javelin-dom', + 'javelin-stratcom', ), - '78bc5d94' => array( - 'javelin-behavior', - 'javelin-uri', - 'phabricator-notification', + '7a61e28c' => array( + 'phuix-icon-view', ), - '78f811c9' => array( + '7b5bd01c' => array( 'javelin-install', + 'javelin-util', ), - '7930776a' => array( + '7dbf81e3' => array( + 'javelin-color', 'javelin-install', - 'javelin-dom', + 'javelin-util', ), - '7b139193' => array( + '7dcfc62b' => array( 'javelin-behavior', - 'javelin-stratcom', - 'javelin-workflow', - 'javelin-dom', + 'javelin-uri', + 'phabricator-keyboard-shortcut', ), - '7c4d8998' => array( + '7e67b544' => array( 'javelin-install', + 'javelin-util', 'javelin-dom', + 'javelin-vector', ), - '80bff3af' => array( - 'javelin-install', - 'javelin-typeahead-source', - ), - '81debc48' => array( - 'javelin-install', + '7fa6d478' => array( + 'javelin-dom', 'javelin-util', 'javelin-stratcom', - 'javelin-dom', + 'javelin-install', + 'javelin-aphlict', + 'javelin-workflow', + 'javelin-router', + 'javelin-behavior-device', 'javelin-vector', ), - '8207abf9' => array( + '8084fa7b' => array( + 'javelin-behavior', + 'javelin-stratcom', 'javelin-dom', ), - 83754533 => array( + 81770869 => array( 'javelin-install', 'javelin-util', + 'javelin-stratcom', 'javelin-dom', 'javelin-vector', ), - '84e6891f' => array( - 'javelin-install', + '81f19452' => array( + 'javelin-behavior', 'javelin-stratcom', 'javelin-util', - 'javelin-behavior', - 'javelin-json', 'javelin-dom', - 'javelin-resource', - 'javelin-routable', - ), - '84f82dad' => array( - 'javelin-install', + 'javelin-request', + 'phabricator-keyboard-shortcut', + 'phabricator-darklog', + 'phabricator-darkmessage', ), - '87428eb2' => array( + '8233f53b' => array( 'javelin-behavior', - 'javelin-diffusion-locate-file-source', - 'javelin-dom', - 'javelin-typeahead', - 'javelin-uri', - ), - '876506b6' => array( - 'javelin-view', - 'javelin-install', - 'javelin-dom', ), - '89a1ae3a' => array( - 'javelin-dom', + '823998c6' => array( + 'javelin-magical-init', 'javelin-util', - 'javelin-stratcom', - 'javelin-install', - ), - '8ac32fd9' => array( - 'javelin-behavior', - 'javelin-stratcom', - 'javelin-workflow', - 'javelin-dom', - 'phabricator-draggable-list', ), - '8b5c7d65' => array( + '82cb3153' => array( 'javelin-behavior', - 'javelin-stratcom', 'javelin-dom', - 'phabricator-busy', - ), - '8badee71' => array( - 'javelin-install', 'javelin-util', - 'javelin-dom', - 'javelin-typeahead-normalizer', + 'javelin-request', + 'phabricator-shaped-request', ), - '8c2ed2bf' => array( + '8312ac01' => array( 'javelin-behavior', 'javelin-dom', 'javelin-util', @@ -1705,204 +1613,198 @@ 'phabricator-shaped-request', 'conpherence-thread-manager', ), - '8e0aa661' => array( + '868e951d' => array( 'javelin-install', - 'javelin-dom', ), - '8f959ad0' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-util', - 'javelin-workflow', - 'javelin-stratcom', + '86c9bec8' => array( + 'javelin-install', + 'javelin-workboard-card', + 'javelin-workboard-header', ), - '91befbcc' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-util', - 'javelin-workflow', - 'javelin-stratcom', + '8775367f' => array( + 'aphront-typeahead-control-css', + 'phui-tag-view-css', ), - '92388bae' => array( + '8796e6c1' => array( 'javelin-behavior', - 'javelin-scrollbar', + 'javelin-uri', ), - '925fe8cd' => array( + '87b119e1' => array( 'javelin-behavior', - 'javelin-stratcom', 'javelin-dom', + 'javelin-workflow', ), - '92cdd7b6' => array( + '892f8706' => array( 'javelin-behavior', 'javelin-stratcom', + 'javelin-workflow', 'javelin-dom', + 'phabricator-draggable-list', ), - '9347f172' => array( + '8bf10492' => array( 'javelin-behavior', - 'multirow-row-manager', - 'javelin-dom', - 'javelin-util', - 'phabricator-prefab', - 'javelin-json', - ), - '94243d89' => array( - 'javelin-install', 'javelin-dom', - 'javelin-typeahead-preloaded-source', - 'javelin-util', - ), - '945ff654' => array( - 'javelin-stratcom', 'javelin-request', - 'javelin-dom', - 'javelin-vector', - 'javelin-install', 'javelin-util', - 'javelin-mask', - 'javelin-uri', - 'javelin-routable', ), - '9623adc1' => array( + '8c5ff0ff' => array( 'javelin-behavior', - 'javelin-stratcom', - 'javelin-workflow', 'javelin-dom', - 'javelin-router', ), - '98ef467f' => array( + '8cc0bee0' => array( 'javelin-behavior', 'javelin-dom', - 'javelin-request', - 'javelin-util', + 'javelin-stratcom', ), - '995f5102' => array( + '8cf58b04' => array( 'javelin-install', + 'javelin-stratcom', + 'javelin-dom', 'javelin-util', - 'javelin-request', - 'javelin-router', ), - '9aae2b66' => array( + '8f32ee03' => array( 'javelin-install', + 'javelin-typeahead-source', 'javelin-util', ), - '9c01e364' => array( + '9022542a' => array( 'javelin-behavior', - 'javelin-dom', 'javelin-workflow', - ), - '9c775532' => array( + 'javelin-json', 'javelin-dom', - 'phabricator-diff-inline-content-state', + 'phabricator-keyboard-shortcut', ), - '9cec214e' => array( + '9151195b' => array( 'javelin-behavior', + 'javelin-dom', 'javelin-stratcom', - 'javelin-workflow', + ), + '92d63c24' => array( + 'javelin-install', 'javelin-dom', + 'javelin-vector', + 'javelin-request', 'javelin-uri', - 'phabricator-textareautils', + 'phui-hovercard', ), - 'a17b84f1' => array( + '92f45381' => array( 'javelin-behavior', 'javelin-dom', - 'javelin-workflow', - ), - 'a241536a' => array( - 'javelin-install', ), - 'a2ab19be' => array( + '95a14f58' => array( 'javelin-behavior', 'javelin-dom', 'javelin-util', - 'javelin-stratcom', - 'javelin-workflow', - 'phabricator-draggable-list', + 'phabricator-shaped-request', ), - 'a4356cde' => array( - 'javelin-install', + '96f0778d' => array( + 'javelin-behavior', 'javelin-dom', - 'javelin-vector', 'javelin-util', + 'multirow-row-manager', + 'javelin-json', + 'phuix-form-control-view', ), - 'a43ae2ae' => array( - 'javelin-install', + '9729be58' => array( + 'javelin-request', + 'javelin-behavior', 'javelin-dom', - 'javelin-stratcom', - 'javelin-vector', + 'javelin-router', + 'javelin-util', + 'phabricator-busy', ), - 'a4aa75c4' => array( - 'phui-button-css', - 'phui-button-simple-css', + '98c0c9d1' => array( + 'javelin-behavior', + 'javelin-behavior-device', + 'javelin-stratcom', + 'phabricator-tooltip', ), - 'a5257c4e' => array( - 'javelin-install', - 'javelin-dom', + '993aa3c0' => array( + 'phui-oi-list-view-css', ), - 'a77e2cbd' => array( + '9c9cd23d' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', - 'javelin-vector', + 'phuix-dropdown-menu', ), - 'a8f573a9' => array( - 'javelin-install', + '9d1e8849' => array( + 'phui-inline-comment-view-css', + ), + '9d56ea6d' => array( + 'javelin-behavior', 'javelin-dom', - 'javelin-util', + 'phortune-credit-card-form', ), - 'a9942052' => array( + '9e54c007' => array( 'javelin-behavior', + 'javelin-stratcom', 'javelin-dom', - 'javelin-view-renderer', - 'javelin-install', ), - 'a9b91e3f' => array( - 'javelin-install', + '9e977c33' => array( + 'javelin-behavior', 'javelin-dom', - 'javelin-stratcom', 'javelin-util', - 'phabricator-notification-css', + 'phabricator-shaped-request', + ), + 'a0632844' => array( + 'trigger-rule-control', ), - 'aa371860' => array( + 'a0b836b0' => array( 'javelin-behavior', 'javelin-stratcom', - 'javelin-workflow', 'javelin-dom', - 'phabricator-draggable-list', ), - 'aa51efb4' => array( + 'a4e03d76' => array( + 'javelin-behavior', + 'javelin-aphlict', + 'phabricator-phtize', 'javelin-dom', ), - 'aa6d2308' => array( + 'a6e925d3' => array( 'javelin-behavior', + 'javelin-stratcom', + 'javelin-workflow', 'javelin-dom', + ), + 'a713fb57' => array( + 'javelin-install', 'javelin-util', - 'multirow-row-manager', - 'javelin-json', - 'phuix-form-control-view', ), - 'ab85e184' => array( + 'a8149272' => array( 'javelin-install', + 'javelin-util', 'javelin-dom', - 'phabricator-notification', + 'javelin-typeahead-normalizer', ), - 'ac10c917' => array( - 'javelin-behavior', + 'a985aa58' => array( 'javelin-dom', - 'javelin-stratcom', ), - 'ac2b1e01' => array( - 'javelin-behavior', - 'javelin-stratcom', + 'acd57fb7' => array( 'javelin-dom', - 'javelin-vector', + 'javelin-dynval', + 'javelin-reactor', + 'javelin-reactornode', + 'javelin-install', + 'javelin-util', + ), + 'ae392e67' => array( + 'phui-fontkit-css', + ), + 'ae501775' => array( 'javelin-install', + 'javelin-util', + 'javelin-dom', + 'javelin-vector', + 'javelin-stratcom', ), - 'ad258e28' => array( + 'ae72a2a1' => array( 'javelin-behavior', + 'javelin-stratcom', 'javelin-dom', - 'javelin-chart', + 'phabricator-busy', ), - 'ad486db3' => array( + 'ae7d493d' => array( 'javelin-install', 'javelin-typeahead', 'javelin-dom', @@ -1910,177 +1812,163 @@ 'javelin-typeahead-ondemand-source', 'javelin-util', ), - 'aec8e38c' => array( - 'javelin-dom', - 'javelin-util', - 'javelin-stratcom', + 'aedefb77' => array( + 'aphront-dialog-view-css', + ), + 'af171e27' => array( + 'phui-timeline-view-css', + ), + 'aff6fa58' => array( 'javelin-install', - 'javelin-aphlict', - 'javelin-workflow', - 'javelin-router', - 'javelin-behavior-device', - 'javelin-vector', ), - 'b105a3a6' => array( + 'b07a98e6' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', ), - 'b26a41e4' => array( - 'javelin-behavior', - 'javelin-stratcom', - 'javelin-dom', + 'b10edcf4' => array( + 'javelin-install', ), - 'b347a301' => array( + 'b2d6b2d6' => array( 'javelin-behavior', - ), - 'b46d88c5' => array( - 'javelin-install', 'javelin-dom', - 'javelin-util', 'javelin-stratcom', 'javelin-workflow', - 'phabricator-draggable-list', - 'javelin-workboard-column', - 'javelin-workboard-header-template', - 'javelin-workboard-card-template', - 'javelin-workboard-order-template', ), - 'b49fd60c' => array( + 'b377c6c1' => array( 'multirow-row-manager', - 'trigger-rule', - ), - 'b517bfa0' => array( - 'phui-oi-list-view-css', - ), - 'b557770a' => array( 'javelin-install', 'javelin-util', 'javelin-dom', - 'javelin-vector', 'javelin-stratcom', + 'javelin-json', + 'phabricator-prefab', ), - 'b58d1a2a' => array( + 'b44cb8be' => array( 'javelin-behavior', - 'javelin-behavior-device', - 'javelin-stratcom', - 'javelin-vector', 'javelin-dom', - 'javelin-magical-init', + 'javelin-uri', + 'javelin-mask', + 'phabricator-drag-and-drop-file-upload', ), - 'b5e9bff9' => array( + 'b48f5e2c' => array( 'javelin-behavior', + 'javelin-dom', + 'javelin-util', + 'javelin-workflow', 'javelin-stratcom', + 'conpherence-thread-manager', + ), + 'b6ae7ef5' => array( + 'javelin-behavior', + 'javelin-uri', + 'phabricator-notification', + ), + 'b6b17353' => array( + 'javelin-install', + 'javelin-util', + ), + 'b7c340b9' => array( + 'javelin-install', 'javelin-dom', ), - 'b7b73831' => array( + 'bae4d536' => array( 'javelin-behavior', 'javelin-dom', - 'javelin-util', - 'phabricator-shaped-request', ), - 'b86ef6c2' => array( + 'bb4c7566' => array( + 'javelin-install', + ), + 'bc69e075' => array( + 'phui-oi-list-view-css', + ), + 'bcb1469f' => array( 'javelin-behavior', 'javelin-dom', - 'javelin-stratcom', - 'phabricator-tooltip', - 'phabricator-diff-changeset-list', - 'phabricator-diff-changeset', - 'phuix-formation-view', + 'javelin-request', ), - 'b86f297f' => array( + 'bdac8af0' => array( 'javelin-behavior', 'javelin-stratcom', - 'javelin-workflow', 'javelin-dom', + 'javelin-json', 'phabricator-draggable-list', ), - 'b9109f8f' => array( - 'javelin-behavior', - 'javelin-uri', - 'phabricator-notification', + 'bf9b167e' => array( + 'phui-button-css', ), - 'b9d0c2f3' => array( + 'c1810d04' => array( + 'multirow-row-manager', 'javelin-install', + 'path-typeahead', 'javelin-dom', 'javelin-util', - 'javelin-vector', - 'javelin-stratcom', - 'javelin-workflow', - 'phabricator-drag-and-drop-file-upload', - 'javelin-workboard-board', - ), - 'bcec20f0' => array( - 'phui-theme-css', - ), - 'c03f2fb4' => array( - 'javelin-install', + 'phabricator-prefab', + 'phuix-form-control-view', ), - 'c2c500a7' => array( + 'c34271ef' => array( 'javelin-install', + 'javelin-stratcom', + 'javelin-util', + 'javelin-behavior', + 'javelin-json', 'javelin-dom', - 'phuix-button-view', + 'javelin-resource', + 'javelin-routable', ), - 'c3703a16' => array( + 'c49be7c7' => array( 'javelin-behavior', - 'javelin-aphlict', - 'phabricator-phtize', + 'javelin-stratcom', + 'javelin-workflow', 'javelin-dom', + 'javelin-router', ), - 'c3d24e63' => array( - 'javelin-install', - 'javelin-workboard-card', - 'javelin-workboard-header', + 'c76477e6' => array( + 'herald-rule-editor', + 'javelin-behavior', ), - 'c687e867' => array( + 'c829538d' => array( 'javelin-behavior', - 'javelin-dom', 'javelin-stratcom', 'javelin-workflow', - 'javelin-fx', - 'javelin-util', - ), - 'c68f183f' => array( - 'javelin-install', 'javelin-dom', + 'phabricator-draggable-list', ), - 'c715c123' => array( + 'cb1ed345' => array( 'javelin-behavior', 'javelin-dom', - 'javelin-util', - 'javelin-workflow', - 'javelin-json', - ), - 'cc2c5de5' => array( - 'javelin-install', - 'phuix-button-view', - 'phabricator-diff-tree-view', + 'javelin-stratcom', ), - 'cef53b3e' => array( - 'javelin-install', + 'cb3fa126' => array( + 'javelin-behavior', + 'javelin-typeahead-ondemand-source', + 'javelin-typeahead', 'javelin-dom', - 'phuix-formation-column-view', - 'phuix-formation-flank-view', + 'javelin-uri', + 'javelin-util', + 'javelin-stratcom', + 'phabricator-prefab', + 'phuix-icon-view', ), - 'cf32921f' => array( + 'cb8ced97' => array( 'javelin-behavior', 'javelin-dom', 'javelin-stratcom', + 'phabricator-tooltip', + 'phabricator-diff-changeset-list', + 'phabricator-diff-changeset', + 'phuix-formation-view', ), - 'd12d214f' => array( + 'cda4ac41' => array( 'javelin-install', - 'javelin-dom', - 'javelin-json', - 'javelin-workflow', + 'javelin-reactornode', 'javelin-util', + 'javelin-reactor', ), - 'd3799cb4' => array( - 'javelin-install', - ), - 'd4cc2d2a' => array( + 'ce95eb52' => array( 'javelin-install', ), - 'd7d3ba75' => array( + 'd33517b0' => array( 'javelin-dom', 'javelin-util', 'javelin-stratcom', @@ -2094,116 +1982,248 @@ 'phuix-button-view', 'javelin-external-editor-link-engine', ), - 'd8a86cfb' => array( + 'd337bd45' => array( + 'phabricator-notification', + 'javelin-stratcom', + 'javelin-behavior', + ), + 'd52a680e' => array( 'javelin-behavior', + 'javelin-quicksand', + ), + 'd7707183' => array( + 'javelin-install', 'javelin-dom', - 'javelin-util', - 'phabricator-shaped-request', + 'javelin-vector', + 'javelin-request', + 'javelin-uri', ), - 'da15d3dc' => array( - 'phui-oi-list-view-css', + 'd7e34a5b' => array( + 'javelin-behavior', + 'javelin-request', ), - 'da8f5259' => array( + 'd88500e0' => array( 'javelin-behavior', 'javelin-dom', + 'javelin-util', + 'phuix-dropdown-menu', + 'phuix-action-list-view', + 'phuix-action-view', + 'javelin-workflow', + 'phuix-icon-view', + ), + 'd89b0a1b' => array( + 'javelin-install', + 'javelin-dom', + 'phuix-formation-column-view', + 'phuix-formation-flank-view', + ), + 'd8c8b363' => array( + 'multirow-row-manager', + 'trigger-rule', ), - 'dae2d55b' => array( + 'd9158762' => array( 'javelin-behavior', + 'javelin-diffusion-locate-file-source', + 'javelin-dom', + 'javelin-typeahead', 'javelin-uri', - 'phabricator-notification', ), - 'de4b4919' => array( - 'javelin-install', - 'javelin-dom', - 'javelin-vector', + 'da025579' => array( + 'javelin-behavior', 'javelin-request', + 'javelin-stratcom', + 'javelin-vector', + 'javelin-dom', 'javelin-uri', - 'phui-hovercard', + 'javelin-behavior-device', + 'phabricator-title', + 'phabricator-favicon', ), - 'e150bd50' => array( + 'dabe390e' => array( 'javelin-behavior', 'javelin-stratcom', + 'javelin-workflow', 'javelin-dom', - 'phuix-dropdown-menu', + 'phuix-form-control-view', + 'phuix-icon-view', + 'javelin-behavior-phabricator-gesture', ), - 'e4c7622a' => array( - 'javelin-magical-init', + 'dc4dc986' => array( + 'javelin-install', + ), + 'dd66ccfa' => array( + 'javelin-behavior', + 'javelin-dom', + 'javelin-stratcom', + 'javelin-workflow', + 'javelin-util', + 'javelin-uri', + ), + 'ddcf7994' => array( 'javelin-install', 'javelin-util', - 'javelin-vector', + 'javelin-dom', + 'javelin-typeahead', + 'javelin-tokenizer', + 'javelin-typeahead-preloaded-source', + 'javelin-typeahead-ondemand-source', + 'javelin-dom', + 'javelin-stratcom', + 'javelin-util', + ), + 'dde695a1' => array( + 'javelin-util', + 'javelin-magical-init', + ), + 'e08d87ef' => array( + 'javelin-behavior', + 'javelin-behavior-device', 'javelin-stratcom', + 'javelin-vector', + 'javelin-dom', + 'javelin-magical-init', ), - 'e5bdb730' => array( + 'e3cebb9a' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-workflow', 'javelin-dom', 'phabricator-draggable-list', ), - 'e8240b50' => array( + 'e4366d54' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', + 'javelin-uri', + 'phabricator-notification', ), - 'e9a2940f' => array( + 'e57f4bdd' => array( 'javelin-behavior', - 'javelin-request', + 'javelin-dom', 'javelin-stratcom', - 'javelin-vector', + 'javelin-behavior-device', + 'javelin-scrollbar', + 'javelin-quicksand', + 'phabricator-keyboard-shortcut', + 'conpherence-thread-manager', + ), + 'e754ba8c' => array( + 'javelin-stratcom', + 'javelin-request', 'javelin-dom', + 'javelin-vector', + 'javelin-install', + 'javelin-util', + 'javelin-mask', 'javelin-uri', - 'javelin-behavior-device', - 'phabricator-title', - 'phabricator-favicon', + 'javelin-routable', + ), + 'e76e2c73' => array( + 'javelin-behavior', + 'javelin-stratcom', + 'javelin-dom', + 'phuix-dropdown-menu', ), - 'e9c80beb' => array( + 'e8754d9a' => array( + 'javelin-behavior', + 'javelin-stratcom', + 'javelin-workflow', + 'javelin-dom', + 'phabricator-draggable-list', + ), + 'e8ea2329' => array( 'javelin-install', - 'javelin-event', + 'javelin-dom', + 'javelin-stratcom', + 'javelin-util', + 'phabricator-notification-css', ), - 'ebe83a6b' => array( + 'e9da5466' => array( 'javelin-install', ), - 'ec4e31c0' => array( - 'phui-timeline-view-css', + 'e9ff303a' => array( + 'javelin-behavior', + 'javelin-util', + 'javelin-dom', + 'javelin-stratcom', + 'javelin-vector', ), - 'ee77366f' => array( - 'aphront-dialog-view-css', + 'ea897c68' => array( + 'javelin-install', + 'javelin-util', + 'javelin-request', + 'javelin-typeahead-source', ), - 'f340a484' => array( + 'eb6c1fee' => array( 'javelin-install', + ), + 'edd6d697' => array( + 'javelin-behavior', + 'javelin-util', + 'javelin-stratcom', 'javelin-dom', 'javelin-vector', + 'javelin-magical-init', + 'javelin-request', + 'javelin-history', + 'javelin-workflow', + 'javelin-mask', + 'javelin-behavior-device', + 'phabricator-keyboard-shortcut', + ), + 'ee039e34' => array( + 'javelin-install', ), - 'f84bcbf4' => array( + 'efce8717' => array( 'javelin-behavior', 'javelin-stratcom', + 'javelin-workflow', 'javelin-dom', + 'phabricator-draggable-list', ), - 'f8c4e135' => array( + 'f1c80c78' => array( + 'owners-path-editor', + 'javelin-behavior', + ), + 'f20f2119' => array( 'javelin-install', 'javelin-dom', - 'javelin-view-visitor', - 'javelin-util', + 'phabricator-notification', ), - 'fa6f30b2' => array( - 'javelin-behavior', + 'f5aa2659' => array( + 'javelin-install', 'javelin-dom', + ), + 'f6b9fd3a' => array( + 'phui-workcard-view-css', + ), + 'fa7092c6' => array( + 'javelin-behavior', + 'javelin-uri', + ), + 'fab38736' => array( + 'javelin-behavior', 'javelin-stratcom', - 'javelin-behavior-device', - 'javelin-scrollbar', - 'javelin-quicksand', - 'phabricator-keyboard-shortcut', - 'conpherence-thread-manager', + 'javelin-dom', + 'javelin-vector', + 'javelin-install', ), - 'fa74cc35' => array( - 'phui-oi-list-view-css', + 'fb72774b' => array( + 'javelin-view', + 'javelin-install', + 'javelin-dom', ), - 'fdc13e4e' => array( + 'fc62fade' => array( 'javelin-install', + 'javelin-dom', ), - 'ff688a7a' => array( - 'owners-path-editor', + 'fd92bf06' => array( 'javelin-behavior', + 'javelin-dom', + 'javelin-util', + 'javelin-workflow', + 'javelin-stratcom', ), ), 'packages' => array( diff --git a/resources/sql/autopatches/000.role.sql b/resources/sql/autopatches/000.role.sql new file mode 100644 index 0000000000..60602b0686 --- /dev/null +++ b/resources/sql/autopatches/000.role.sql @@ -0,0 +1,383 @@ +CREATE DATABASE /*!32312 IF NOT EXISTS*/ `{$NAMESPACE}_role` /*!40100 DEFAULT CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} */; + +USE `{$NAMESPACE}_role`; + + SET NAMES utf8 ; + + SET character_set_client = {$CHARSET} ; + +CREATE TABLE IF NOT EXISTS `edge` ( + `src` varbinary(64) NOT NULL, + `type` int(10) unsigned NOT NULL, + `dst` varbinary(64) NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `seq` int(10) unsigned NOT NULL, + `dataID` int(10) unsigned DEFAULT NULL, + PRIMARY KEY (`src`,`type`,`dst`), + UNIQUE KEY `key_dst` (`dst`,`type`,`src`), + KEY `src` (`src`,`type`,`dateCreated`,`seq`) +) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE={$COLLATE_TEXT}; + +USE `{$NAMESPACE}_role`; + + SET NAMES utf8 ; + + SET character_set_client = {$CHARSET} ; + +CREATE TABLE IF NOT EXISTS `edgedata` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `data` longtext CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE={$COLLATE_TEXT}; + +USE `{$NAMESPACE}_role`; + + SET NAMES utf8 ; + + SET character_set_client = {$CHARSET} ; + +CREATE TABLE IF NOT EXISTS `role` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(128) CHARACTER SET {$CHARSET_SORT} COLLATE {$COLLATE_SORT} NOT NULL, + `phid` varbinary(64) NOT NULL, + `authorPHID` varbinary(64) NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + `status` varchar(32) CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + `viewPolicy` varbinary(64) NOT NULL, + `editPolicy` varbinary(64) NOT NULL, + `joinPolicy` varbinary(64) NOT NULL, + `isMembershipLocked` tinyint(1) NOT NULL DEFAULT '0', + `profileImagePHID` varbinary(64) DEFAULT NULL, + `icon` varchar(32) CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + `color` varchar(32) CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + `mailKey` binary(20) NOT NULL, + `primarySlug` varchar(128) CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} DEFAULT NULL, + `parentRolePHID` varbinary(64) DEFAULT NULL, + `hasWorkboard` tinyint(1) NOT NULL, + `hasMilestones` tinyint(1) NOT NULL, + `hasSubroles` tinyint(1) NOT NULL, + `milestoneNumber` int(10) unsigned DEFAULT NULL, + `rolePath` varbinary(64) NOT NULL, + `roleDepth` int(10) unsigned NOT NULL, + `rolePathKey` binary(4) NOT NULL, + `properties` longtext CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + `spacePHID` varbinary(64) DEFAULT NULL, + `subtype` varchar(64) CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_pathkey` (`rolePathKey`), + UNIQUE KEY `key_phid` (`phid`), + UNIQUE KEY `key_primaryslug` (`primarySlug`), + UNIQUE KEY `key_milestone` (`parentRolePHID`,`milestoneNumber`), + KEY `key_icon` (`icon`), + KEY `key_color` (`color`), + KEY `key_path` (`rolePath`,`roleDepth`), + KEY `key_space` (`spacePHID`) +) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE={$COLLATE_TEXT}; + +USE `{$NAMESPACE}_role`; + + SET NAMES utf8 ; + + SET character_set_client = {$CHARSET} ; + +CREATE TABLE IF NOT EXISTS `role_column` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varbinary(64) NOT NULL, + `name` varchar(255) CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + `status` int(10) unsigned NOT NULL, + `sequence` int(10) unsigned NOT NULL, + `rolePHID` varbinary(64) NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + `properties` longtext CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + `proxyPHID` varbinary(64) DEFAULT NULL, + `triggerPHID` varbinary(64) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + UNIQUE KEY `key_proxy` (`rolePHID`,`proxyPHID`), + KEY `key_status` (`rolePHID`,`status`,`sequence`), + KEY `key_sequence` (`rolePHID`,`sequence`), + KEY `key_trigger` (`triggerPHID`) +) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE={$COLLATE_TEXT}; + +USE `{$NAMESPACE}_role`; + + SET NAMES utf8 ; + + SET character_set_client = {$CHARSET} ; + +CREATE TABLE IF NOT EXISTS `role_columnposition` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `boardPHID` varbinary(64) NOT NULL, + `columnPHID` varbinary(64) NOT NULL, + `objectPHID` varbinary(64) NOT NULL, + `sequence` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `boardPHID` (`boardPHID`,`columnPHID`,`objectPHID`), + KEY `objectPHID` (`objectPHID`,`boardPHID`), + KEY `boardPHID_2` (`boardPHID`,`columnPHID`,`sequence`) +) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE={$COLLATE_TEXT}; + +USE `{$NAMESPACE}_role`; + + SET NAMES utf8 ; + + SET character_set_client = {$CHARSET} ; + +CREATE TABLE IF NOT EXISTS `role_columntransaction` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varbinary(64) NOT NULL, + `authorPHID` varbinary(64) NOT NULL, + `objectPHID` varbinary(64) NOT NULL, + `viewPolicy` varbinary(64) NOT NULL, + `editPolicy` varbinary(64) NOT NULL, + `commentPHID` varbinary(64) DEFAULT NULL, + `commentVersion` int(10) unsigned NOT NULL, + `transactionType` varchar(32) CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + `oldValue` longtext CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + `newValue` longtext CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + `contentSource` longtext CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + `metadata` longtext CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + KEY `key_object` (`objectPHID`) +) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE={$COLLATE_TEXT}; + +USE `{$NAMESPACE}_role`; + + SET NAMES utf8 ; + + SET character_set_client = {$CHARSET} ; + +CREATE TABLE IF NOT EXISTS `role_customfieldnumericindex` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `objectPHID` varbinary(64) NOT NULL, + `indexKey` binary(12) NOT NULL, + `indexValue` bigint(20) NOT NULL, + PRIMARY KEY (`id`), + KEY `key_join` (`objectPHID`,`indexKey`,`indexValue`), + KEY `key_find` (`indexKey`,`indexValue`) +) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE={$COLLATE_TEXT}; + +USE `{$NAMESPACE}_role`; + + SET NAMES utf8 ; + + SET character_set_client = {$CHARSET} ; + +CREATE TABLE IF NOT EXISTS `role_customfieldstorage` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `objectPHID` varbinary(64) NOT NULL, + `fieldIndex` binary(12) NOT NULL, + `fieldValue` longtext CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `objectPHID` (`objectPHID`,`fieldIndex`) +) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE={$COLLATE_TEXT}; + +USE `{$NAMESPACE}_role`; + + SET NAMES utf8 ; + + SET character_set_client = {$CHARSET} ; + +CREATE TABLE IF NOT EXISTS `role_customfieldstringindex` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `objectPHID` varbinary(64) NOT NULL, + `indexKey` binary(12) NOT NULL, + `indexValue` longtext CHARACTER SET {$CHARSET_SORT} COLLATE {$COLLATE_SORT} NOT NULL, + PRIMARY KEY (`id`), + KEY `key_join` (`objectPHID`,`indexKey`,`indexValue`(64)), + KEY `key_find` (`indexKey`,`indexValue`(64)) +) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE={$COLLATE_TEXT}; + +USE `{$NAMESPACE}_role`; + + SET NAMES utf8 ; + + SET character_set_client = {$CHARSET} ; + +CREATE TABLE IF NOT EXISTS `role_datasourcetoken` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `roleID` int(10) unsigned NOT NULL, + `token` varchar(128) CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `token` (`token`,`roleID`), + KEY `roleID` (`roleID`) +) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE={$COLLATE_TEXT}; + +USE `{$NAMESPACE}_role`; + + SET NAMES utf8 ; + + SET character_set_client = {$CHARSET} ; + +CREATE TABLE IF NOT EXISTS `role_role_fdocument` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `objectPHID` varbinary(64) NOT NULL, + `isClosed` tinyint(1) NOT NULL, + `authorPHID` varbinary(64) DEFAULT NULL, + `ownerPHID` varbinary(64) DEFAULT NULL, + `epochCreated` int(10) unsigned NOT NULL, + `epochModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_object` (`objectPHID`), + KEY `key_author` (`authorPHID`), + KEY `key_owner` (`ownerPHID`), + KEY `key_created` (`epochCreated`), + KEY `key_modified` (`epochModified`) +) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE={$COLLATE_TEXT}; + +USE `{$NAMESPACE}_role`; + + SET NAMES utf8 ; + + SET character_set_client = {$CHARSET} ; + +CREATE TABLE IF NOT EXISTS `role_role_ffield` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `documentID` int(10) unsigned NOT NULL, + `fieldKey` varchar(4) CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + `rawCorpus` longtext CHARACTER SET {$CHARSET_SORT} COLLATE {$COLLATE_SORT} NOT NULL, + `termCorpus` longtext CHARACTER SET {$CHARSET_SORT} COLLATE {$COLLATE_SORT} NOT NULL, + `normalCorpus` longtext CHARACTER SET {$CHARSET_SORT} COLLATE {$COLLATE_SORT} NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_documentfield` (`documentID`,`fieldKey`) +) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE={$COLLATE_TEXT}; + +USE `{$NAMESPACE}_role`; + + SET NAMES utf8 ; + + SET character_set_client = {$CHARSET} ; + +CREATE TABLE IF NOT EXISTS `role_role_fngrams` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `documentID` int(10) unsigned NOT NULL, + `ngram` char(3) CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + PRIMARY KEY (`id`), + KEY `key_ngram` (`ngram`,`documentID`), + KEY `key_object` (`documentID`) +) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE={$COLLATE_TEXT}; + +USE `{$NAMESPACE}_role`; + + SET NAMES utf8 ; + + SET character_set_client = {$CHARSET} ; + +CREATE TABLE IF NOT EXISTS `role_role_fngrams_common` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `ngram` char(3) CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + `needsCollection` tinyint(1) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_ngram` (`ngram`), + KEY `key_collect` (`needsCollection`) +) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE={$COLLATE_TEXT}; + +USE `{$NAMESPACE}_role`; + + SET NAMES utf8 ; + + SET character_set_client = {$CHARSET} ; + +CREATE TABLE IF NOT EXISTS `role_slug` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `rolePHID` varbinary(64) NOT NULL, + `slug` varchar(128) CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_slug` (`slug`), + KEY `key_rolePHID` (`rolePHID`) +) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE={$COLLATE_TEXT}; + +USE `{$NAMESPACE}_role`; + + SET NAMES utf8 ; + + SET character_set_client = {$CHARSET} ; + +CREATE TABLE IF NOT EXISTS `role_transaction` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varbinary(64) NOT NULL, + `authorPHID` varbinary(64) NOT NULL, + `objectPHID` varbinary(64) NOT NULL, + `viewPolicy` varbinary(64) NOT NULL, + `editPolicy` varbinary(64) NOT NULL, + `commentPHID` varbinary(64) DEFAULT NULL, + `commentVersion` int(10) unsigned NOT NULL, + `transactionType` varchar(32) CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + `oldValue` longtext CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + `newValue` longtext CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + `contentSource` longtext CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + `metadata` longtext CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + KEY `key_object` (`objectPHID`) +) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE={$COLLATE_TEXT}; + +USE `{$NAMESPACE}_role`; + + SET NAMES utf8 ; + + SET character_set_client = {$CHARSET} ; + +CREATE TABLE IF NOT EXISTS `role_trigger` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varbinary(64) NOT NULL, + `name` varchar(255) CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + `editPolicy` varbinary(64) NOT NULL, + `ruleset` longtext CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`) +) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE={$COLLATE_TEXT}; + +USE `{$NAMESPACE}_role`; + + SET NAMES utf8 ; + + SET character_set_client = {$CHARSET} ; + +CREATE TABLE IF NOT EXISTS `role_triggertransaction` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varbinary(64) NOT NULL, + `authorPHID` varbinary(64) NOT NULL, + `objectPHID` varbinary(64) NOT NULL, + `viewPolicy` varbinary(64) NOT NULL, + `editPolicy` varbinary(64) NOT NULL, + `commentPHID` varbinary(64) DEFAULT NULL, + `commentVersion` int(10) unsigned NOT NULL, + `transactionType` varchar(32) COLLATE {$COLLATE_TEXT} NOT NULL, + `oldValue` longtext COLLATE {$COLLATE_TEXT} NOT NULL, + `newValue` longtext COLLATE {$COLLATE_TEXT} NOT NULL, + `contentSource` longtext COLLATE {$COLLATE_TEXT} NOT NULL, + `metadata` longtext COLLATE {$COLLATE_TEXT} NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + KEY `key_object` (`objectPHID`) +) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE={$COLLATE_TEXT}; + +USE `{$NAMESPACE}_role`; + + SET NAMES utf8 ; + + SET character_set_client = {$CHARSET} ; + +CREATE TABLE IF NOT EXISTS `role_triggerusage` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `triggerPHID` varbinary(64) NOT NULL, + `examplePHID` varbinary(64) DEFAULT NULL, + `columnCount` int(10) unsigned NOT NULL, + `activeColumnCount` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_trigger` (`triggerPHID`) +) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE={$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/001.releeph.role.sql b/resources/sql/autopatches/001.releeph.role.sql new file mode 100644 index 0000000000..c6cb13851e --- /dev/null +++ b/resources/sql/autopatches/001.releeph.role.sql @@ -0,0 +1,21 @@ +USE `{$NAMESPACE}_releeph`; + + SET NAMES utf8 ; + + SET character_set_client = {$CHARSET} ; + +CREATE TABLE IF NOT EXISTS `releeph_role` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + `phid` varbinary(64) NOT NULL, + `name` varchar(128) CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + `trunkBranch` varchar(255) CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + `repositoryPHID` varbinary(64) NOT NULL, + `createdByUserPHID` varbinary(64) NOT NULL, + `isActive` tinyint(1) NOT NULL DEFAULT '1', + `details` longtext CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `roleName` (`name`), + UNIQUE KEY `key_phid` (`phid`) +) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE={$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/002.releeph.roledrop.sql b/resources/sql/autopatches/002.releeph.roledrop.sql new file mode 100644 index 0000000000..c1e5d389cc --- /dev/null +++ b/resources/sql/autopatches/002.releeph.roledrop.sql @@ -0,0 +1,7 @@ +USE `{$NAMESPACE}_releeph`; + + SET NAMES utf8 ; + + SET character_set_client = {$CHARSET} ; + +DROP TABLE IF EXISTS `releeph_role`; \ No newline at end of file diff --git a/resources/sql/autopatches/003.timetracker.projectPHID.sql b/resources/sql/autopatches/003.timetracker.projectPHID.sql new file mode 100644 index 0000000000..94bf0d69cb --- /dev/null +++ b/resources/sql/autopatches/003.timetracker.projectPHID.sql @@ -0,0 +1,10 @@ +CREATE DATABASE /*!32312 IF NOT EXISTS*/ `{$NAMESPACE}_timetracker` /*!40100 DEFAULT CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} */; + +USE `{$NAMESPACE}_timetracker`; + + SET NAMES utf8 ; + + SET character_set_client = {$CHARSET} ; + +ALTER TABLE {$NAMESPACE}_timetracker.timetracker_trackedtime + ADD IF NOT EXISTS `projectPHID` varbinary(64) NOT NULL; \ No newline at end of file diff --git a/resources/sql/autopatches/003.timetracker.sql b/resources/sql/autopatches/003.timetracker.sql new file mode 100644 index 0000000000..3772aef8d1 --- /dev/null +++ b/resources/sql/autopatches/003.timetracker.sql @@ -0,0 +1,16 @@ +CREATE DATABASE /*!32312 IF NOT EXISTS*/ `{$NAMESPACE}_timetracker` /*!40100 DEFAULT CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} */; + +USE `{$NAMESPACE}_timetracker`; + + SET NAMES utf8 ; + + SET character_set_client = {$CHARSET} ; + +CREATE TABLE IF NOT EXISTS `timetracker_trackedtime` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `userID` int(10) unsigned NOT NULL, + `numMinutes` int(10) NOT NULL, + `dateWhenTrackedFor` int(12) NOT NULL, + `realDateWhenTracked` int(12) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE={$COLLATE_TEXT}; \ No newline at end of file diff --git a/resources/sql/autopatches/004.vacation.sql b/resources/sql/autopatches/004.vacation.sql new file mode 100644 index 0000000000..834ae77ff0 --- /dev/null +++ b/resources/sql/autopatches/004.vacation.sql @@ -0,0 +1,16 @@ +CREATE DATABASE /*!32312 IF NOT EXISTS*/ `{$NAMESPACE}_vacation` /*!40100 DEFAULT CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} */; + +USE `{$NAMESPACE}_vacation`; + + SET NAMES utf8 ; + + SET character_set_client = {$CHARSET} ; + +CREATE TABLE IF NOT EXISTS `vacation_day` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `userID` int(10) unsigned NOT NULL, + `numMinutes` int(10) NOT NULL, + `dateWhenTrackedFor` int(12) NOT NULL, + `realDateWhenTracked` int(12) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE={$COLLATE_TEXT}; \ No newline at end of file diff --git a/resources/sql/autopatches/090.forceuniquerolenames.php b/resources/sql/autopatches/090.forceuniquerolenames.php new file mode 100644 index 0000000000..cf74c90c0b --- /dev/null +++ b/resources/sql/autopatches/090.forceuniquerolenames.php @@ -0,0 +1,114 @@ +openTransaction(); +$table->beginReadLocking(); + +$roles = $table->loadAll(); + +$slug_map = array(); + +foreach ($roles as $role) { + $slug = PhabricatorSlug::normalizeProjectSlug($role->getName()); + + if (!strlen($slug)) { + $role_id = $role->getID(); + echo pht("Role #%d doesn't have a meaningful name...", $role_id)."\n"; + $role->setName(trim(pht('Unnamed Role %s', $role->getName()))); + } + + $slug_map[$slug][] = $role->getID(); +} + + +foreach ($slug_map as $slug => $similar) { + if (count($similar) <= 1) { + continue; + } + echo pht("Too many roles are similar to '%s'...", $slug)."\n"; + + foreach (array_slice($similar, 1, null, true) as $key => $role_id) { + $role = $roles[$role_id]; + $old_name = $role->getName(); + $new_name = rename_role($role, $roles); + + echo pht( + "Renaming role #%d from '%s' to '%s'.\n", + $role_id, + $old_name, + $new_name); + $role->setName($new_name); + } +} + +$update = $roles; +while ($update) { + $size = count($this->update); + foreach ($update as $key => $role) { + $id = $role->getID(); + $name = $role->getName(); + + $slug = PhabricatorSlug::normalizeProjectSlug($name).'/'; + + echo pht("Updating role #%d '%s' (%s)... ", $id, $name, $slug); + try { + queryfx( + $role->establishConnection('w'), + 'UPDATE %T SET name = %s, phrictionSlug = %s WHERE id = %d', + $role->getTableName(), + $name, + $slug, + $role->getID()); + unset($update[$key]); + echo pht('OKAY')."\n"; + } catch (AphrontDuplicateKeyQueryException $ex) { + echo pht('Failed, will retry.')."\n"; + } + } + if (count($this->update) == $size) { + throw new Exception( + pht( + 'Failed to make any progress while updating roles. Schema upgrade '. + 'has failed. Go manually fix your role names to be unique '. + '(they are probably ridiculous?) and then try again.')); + } +} + +$table->endReadLocking(); +$table->saveTransaction(); +echo pht('Done.')."\n"; + + +/** + * Rename the role so that it has a unique slug, by appending (2), (3), etc. + * to its name. + */ +function rename_role($role, $roles) { + $suffix = 2; + while (true) { + $new_name = $role->getName().' ('.$suffix.')'; + + $new_slug = PhabricatorSlug::normalizeProjectSlug($new_name).'/'; + + $okay = true; + foreach ($roles as $other) { + if ($other->getID() == $role->getID()) { + continue; + } + + $other_slug = PhabricatorSlug::normalizeProjectSlug($other->getName()); + if ($other_slug == $new_slug) { + $okay = false; + break; + } + } + if ($okay) { + break; + } else { + $suffix++; + } + } + + return $new_name; +} diff --git a/resources/sql/autopatches/20220510.file.01.attach.sql b/resources/sql/autopatches/20220510.file.01.attach.sql index 3ca8bacac4..fe59f96369 100644 --- a/resources/sql/autopatches/20220510.file.01.attach.sql +++ b/resources/sql/autopatches/20220510.file.01.attach.sql @@ -1,4 +1,4 @@ -CREATE TABLE {$NAMESPACE}_file.file_attachment ( +CREATE TABLE IF NOT EXISTS {$NAMESPACE}_file.file_attachment ( id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, objectPHID VARBINARY(64) NOT NULL, filePHID VARBINARY(64) NOT NULL, diff --git a/resources/sql/patches/090.forceuniquerolesnames.php b/resources/sql/patches/090.forceuniquerolesnames.php new file mode 100644 index 0000000000..d7bfcfd683 --- /dev/null +++ b/resources/sql/patches/090.forceuniquerolesnames.php @@ -0,0 +1,114 @@ +openTransaction(); +$table->beginReadLocking(); + +$roles = $table->loadAll(); + +$slug_map = array(); + +foreach ($roles as $role) { + $slug = PhabricatorSlug::normalizeProjectSlug($role->getName()); + + if (!strlen($slug)) { + $role_id = $role->getID(); + echo pht("Role #%d doesn't have a meaningful name...", $role_id)."\n"; + $role->setName(trim(pht('Unnamed Role %s', $role->getName()))); + } + + $slug_map[$slug][] = $role->getID(); +} + + +foreach ($slug_map as $slug => $similar) { + if (count($similar) <= 1) { + continue; + } + echo pht("Too many roles are similar to '%s'...", $slug)."\n"; + + foreach (array_slice($similar, 1, null, true) as $key => $role_id) { + $role = $roles[$role_id]; + $old_name = $role->getName(); + $new_name = rename_role($role, $roles); + + echo pht( + "Renaming role #%d from '%s' to '%s'.\n", + $role_id, + $old_name, + $new_name); + $role->setName($new_name); + } +} + +$update = $roles; +while ($update) { + $size = count($update); + foreach ($update as $key => $role) { + $id = $role->getID(); + $name = $role->getName(); + + $slug = PhabricatorSlug::normalizeProjectSlug($name).'/'; + + echo pht("Updating role #%d '%s' (%s)... ", $id, $name, $slug); + try { + queryfx( + $role->establishConnection('w'), + 'UPDATE %T SET name = %s, phrictionSlug = %s WHERE id = %d', + $role->getTableName(), + $name, + $slug, + $role->getID()); + unset($update[$key]); + echo pht('OKAY')."\n"; + } catch (AphrontDuplicateKeyQueryException $ex) { + echo pht('Failed, will retry.')."\n"; + } + } + if (count($update) == $size) { + throw new Exception( + pht( + 'Failed to make any progress while updating roles. Schema upgrade '. + 'has failed. Go manually fix your role names to be unique '. + '(they are probably ridiculous?) and then try again.')); + } +} + +$table->endReadLocking(); +$table->saveTransaction(); +echo pht('Done.')."\n"; + + +/** + * Rename the role so that it has a unique slug, by appending (2), (3), etc. + * to its name. + */ +function rename_role($role, $roles) { + $suffix = 2; + while (true) { + $new_name = $role->getName().' ('.$suffix.')'; + + $new_slug = PhabricatorSlug::normalizeProjectSlug($new_name).'/'; + + $okay = true; + foreach ($roles as $other) { + if ($other->getID() == $role->getID()) { + continue; + } + + $other_slug = PhabricatorSlug::normalizeProjectSlug($other->getName()); + if ($other_slug == $new_slug) { + $okay = false; + break; + } + } + if ($okay) { + break; + } else { + $suffix++; + } + } + + return $new_name; +} diff --git a/resources/sql/patches/20130716.archivememberlessroles.php b/resources/sql/patches/20130716.archivememberlessroles.php new file mode 100644 index 0000000000..088e631ab8 --- /dev/null +++ b/resources/sql/patches/20130716.archivememberlessroles.php @@ -0,0 +1,38 @@ +openTransaction(); + +foreach (new LiskMigrationIterator($table) as $role) { + $members = PhabricatorEdgeQuery::loadDestinationPHIDs( + $role->getPHID(), + PhabricatorRoleRoleHasMemberEdgeType::EDGECONST); + + if (count($members)) { + echo pht( + 'Role "%s" has %d members; skipping.', + $role->getName(), + count($members)), "\n"; + continue; + } + + if ($role->getStatus() == PhabricatorRoleStatus::STATUS_ARCHIVED) { + echo pht( + 'Role "%s" already archived; skipping.', + $role->getName()), "\n"; + continue; + } + + echo pht('Archiving role "%s"...', $role->getName())."\n"; + queryfx( + $table->establishConnection('w'), + 'UPDATE %T SET status = %s WHERE id = %d', + $table->getTableName(), + PhabricatorRoleStatus::STATUS_ARCHIVED, + $role->getID()); +} + +$table->saveTransaction(); +echo "\n".pht('Done.')."\n"; diff --git a/resources/sql/patches/20131020.rxactionmig.php b/resources/sql/patches/20131020.rxactionmig.php new file mode 100644 index 0000000000..dd1ce53452 --- /dev/null +++ b/resources/sql/patches/20131020.rxactionmig.php @@ -0,0 +1,91 @@ +establishConnection('w'); +$conn_w->openTransaction(); + +$src_table = 'role_legacytransaction'; +$dst_table = 'role_transaction'; + +echo pht('Migrating Role transactions to new format...')."\n"; + +$content_source = PhabricatorContentSource::newForSource( + PhabricatorOldWorldContentSource::SOURCECONST)->serialize(); + +$rows = new LiskRawMigrationIterator($conn_w, $src_table); +foreach ($rows as $row) { + $id = $row['id']; + + $role_id = $row['roleID']; + + echo pht('Migrating transaction #%d (Role %d)...', $id, $role_id)."\n"; + + $role_row = queryfx_one( + $conn_w, + 'SELECT phid FROM %T WHERE id = %d', + $role_table->getTableName(), + $role_id); + if (!$role_row) { + continue; + } + + $role_phid = $role_row['phid']; + + $type_map = array( + 'name' => PhabricatorRoleNameTransaction::TRANSACTIONTYPE, + 'members' => PhabricatorRoleTransaction::TYPE_MEMBERS, + 'status' => PhabricatorRoleStatusTransaction::TRANSACTIONTYPE, + 'canview' => PhabricatorTransactions::TYPE_VIEW_POLICY, + 'canedit' => PhabricatorTransactions::TYPE_EDIT_POLICY, + 'canjoin' => PhabricatorTransactions::TYPE_JOIN_POLICY, + ); + + $new_type = idx($type_map, $row['transactionType']); + if (!$new_type) { + continue; + } + + $xaction_phid = PhabricatorPHID::generateNewPHID( + PhabricatorApplicationTransactionTransactionPHIDType::TYPECONST, + PhabricatorRoleRolePHIDType::TYPECONST); + + queryfx( + $conn_w, + 'INSERT IGNORE INTO %T + (phid, authorPHID, objectPHID, + viewPolicy, editPolicy, commentPHID, commentVersion, transactionType, + oldValue, newValue, contentSource, metadata, + dateCreated, dateModified) + VALUES + (%s, %s, %s, + %s, %s, %ns, %d, %s, + %s, %s, %s, %s, + %d, %d)', + $dst_table, + + // PHID, Author, Object + $xaction_phid, + $row['authorPHID'], + $role_phid, + + // View, Edit, Comment, Version, Type + 'public', + $row['authorPHID'], + null, + 0, + $new_type, + + // Old, New, Source, Meta, + $row['oldValue'], + $row['newValue'], + $content_source, + '{}', + + // Created, Modified + $row['dateCreated'], + $row['dateModified']); + +} + +$conn_w->saveTransaction(); +echo pht('Done.')."\n"; diff --git a/resources/sql/patches/migrate-role-edges.php b/resources/sql/patches/migrate-role-edges.php new file mode 100644 index 0000000000..ad83852aad --- /dev/null +++ b/resources/sql/patches/migrate-role-edges.php @@ -0,0 +1,35 @@ +establishConnection('w'); + +foreach (new LiskMigrationIterator($table) as $proj) { + $id = $proj->getID(); + echo pht('Role %d: ', $id); + + $members = queryfx_all( + $proj->establishConnection('w'), + 'SELECT userPHID FROM %T WHERE rolePHID = %s', + 'role_affiliation', + $proj->getPHID()); + + if (!$members) { + echo "-\n"; + continue; + } + + $members = ipull($members, 'userPHID'); + + $editor = new PhabricatorEdgeEditor(); + foreach ($members as $user_phid) { + $editor->addEdge( + $proj->getPHID(), + PhabricatorRoleRoleHasMemberEdgeType::EDGECONST, + $user_phid); + } + $editor->save(); + echo pht('OKAY')."\n"; +} + +echo pht('Done.')."\n"; diff --git a/resources/sql/quickstart.sql b/resources/sql/quickstart.sql index b2796d31e6..8fd63aa5e8 100644 --- a/resources/sql/quickstart.sql +++ b/resources/sql/quickstart.sql @@ -8176,6 +8176,391 @@ CREATE TABLE `ponder_questiontransaction_comment` ( UNIQUE KEY `key_version` (`transactionPHID`,`commentVersion`) ) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE={$COLLATE_TEXT}; +CREATE DATABASE /*!32312 IF NOT EXISTS*/ `{$NAMESPACE}_role` /*!40100 DEFAULT CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} */; + +USE `{$NAMESPACE}_role`; + + SET NAMES utf8 ; + + SET character_set_client = {$CHARSET} ; + +CREATE TABLE `edge` ( + `src` varbinary(64) NOT NULL, + `type` int(10) unsigned NOT NULL, + `dst` varbinary(64) NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `seq` int(10) unsigned NOT NULL, + `dataID` int(10) unsigned DEFAULT NULL, + PRIMARY KEY (`src`,`type`,`dst`), + UNIQUE KEY `key_dst` (`dst`,`type`,`src`), + KEY `src` (`src`,`type`,`dateCreated`,`seq`) +) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE={$COLLATE_TEXT}; + +USE `{$NAMESPACE}_role`; + + SET NAMES utf8 ; + + SET character_set_client = {$CHARSET} ; + +CREATE TABLE `edgedata` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `data` longtext CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE={$COLLATE_TEXT}; + +USE `{$NAMESPACE}_role`; + + SET NAMES utf8 ; + + SET character_set_client = {$CHARSET} ; + +CREATE TABLE `role` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(128) CHARACTER SET {$CHARSET_SORT} COLLATE {$COLLATE_SORT} NOT NULL, + `phid` varbinary(64) NOT NULL, + `authorPHID` varbinary(64) NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + `status` varchar(32) CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + `viewPolicy` varbinary(64) NOT NULL, + `editPolicy` varbinary(64) NOT NULL, + `joinPolicy` varbinary(64) NOT NULL, + `isMembershipLocked` tinyint(1) NOT NULL DEFAULT '0', + `profileImagePHID` varbinary(64) DEFAULT NULL, + `icon` varchar(32) CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + `color` varchar(32) CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + `mailKey` binary(20) NOT NULL, + `primarySlug` varchar(128) CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} DEFAULT NULL, + `parentRolePHID` varbinary(64) DEFAULT NULL, + `hasWorkboard` tinyint(1) NOT NULL, + `hasMilestones` tinyint(1) NOT NULL, + `hasSubroles` tinyint(1) NOT NULL, + `milestoneNumber` int(10) unsigned DEFAULT NULL, + `rolePath` varbinary(64) NOT NULL, + `roleDepth` int(10) unsigned NOT NULL, + `rolePathKey` binary(4) NOT NULL, + `properties` longtext CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + `spacePHID` varbinary(64) DEFAULT NULL, + `subtype` varchar(64) CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_pathkey` (`rolePathKey`), + UNIQUE KEY `key_phid` (`phid`), + UNIQUE KEY `key_primaryslug` (`primarySlug`), + UNIQUE KEY `key_milestone` (`parentRolePHID`,`milestoneNumber`), + KEY `key_icon` (`icon`), + KEY `key_color` (`color`), + KEY `key_path` (`rolePath`,`roleDepth`), + KEY `key_space` (`spacePHID`) +) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE={$COLLATE_TEXT}; + +USE `{$NAMESPACE}_role`; + + SET NAMES utf8 ; + + SET character_set_client = {$CHARSET} ; + +CREATE TABLE `role_column` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varbinary(64) NOT NULL, + `name` varchar(255) CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + `status` int(10) unsigned NOT NULL, + `sequence` int(10) unsigned NOT NULL, + `rolePHID` varbinary(64) NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + `properties` longtext CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + `proxyPHID` varbinary(64) DEFAULT NULL, + `triggerPHID` varbinary(64) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + UNIQUE KEY `key_proxy` (`rolePHID`,`proxyPHID`), + KEY `key_status` (`rolePHID`,`status`,`sequence`), + KEY `key_sequence` (`rolePHID`,`sequence`), + KEY `key_trigger` (`triggerPHID`) +) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE={$COLLATE_TEXT}; + +USE `{$NAMESPACE}_role`; + + SET NAMES utf8 ; + + SET character_set_client = {$CHARSET} ; + +CREATE TABLE `role_columnposition` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `boardPHID` varbinary(64) NOT NULL, + `columnPHID` varbinary(64) NOT NULL, + `objectPHID` varbinary(64) NOT NULL, + `sequence` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `boardPHID` (`boardPHID`,`columnPHID`,`objectPHID`), + KEY `objectPHID` (`objectPHID`,`boardPHID`), + KEY `boardPHID_2` (`boardPHID`,`columnPHID`,`sequence`) +) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE={$COLLATE_TEXT}; + +USE `{$NAMESPACE}_role`; + + SET NAMES utf8 ; + + SET character_set_client = {$CHARSET} ; + +CREATE TABLE `role_columntransaction` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varbinary(64) NOT NULL, + `authorPHID` varbinary(64) NOT NULL, + `objectPHID` varbinary(64) NOT NULL, + `viewPolicy` varbinary(64) NOT NULL, + `editPolicy` varbinary(64) NOT NULL, + `commentPHID` varbinary(64) DEFAULT NULL, + `commentVersion` int(10) unsigned NOT NULL, + `transactionType` varchar(32) CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + `oldValue` longtext CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + `newValue` longtext CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + `contentSource` longtext CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + `metadata` longtext CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + KEY `key_object` (`objectPHID`) +) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE={$COLLATE_TEXT}; + +USE `{$NAMESPACE}_role`; + + SET NAMES utf8 ; + + SET character_set_client = {$CHARSET} ; + +CREATE TABLE `role_customfieldnumericindex` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `objectPHID` varbinary(64) NOT NULL, + `indexKey` binary(12) NOT NULL, + `indexValue` bigint(20) NOT NULL, + PRIMARY KEY (`id`), + KEY `key_join` (`objectPHID`,`indexKey`,`indexValue`), + KEY `key_find` (`indexKey`,`indexValue`) +) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE={$COLLATE_TEXT}; + +USE `{$NAMESPACE}_role`; + + SET NAMES utf8 ; + + SET character_set_client = {$CHARSET} ; + +CREATE TABLE `role_customfieldstorage` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `objectPHID` varbinary(64) NOT NULL, + `fieldIndex` binary(12) NOT NULL, + `fieldValue` longtext CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `objectPHID` (`objectPHID`,`fieldIndex`) +) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE={$COLLATE_TEXT}; + +USE `{$NAMESPACE}_role`; + + SET NAMES utf8 ; + + SET character_set_client = {$CHARSET} ; + +CREATE TABLE `role_customfieldstringindex` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `objectPHID` varbinary(64) NOT NULL, + `indexKey` binary(12) NOT NULL, + `indexValue` longtext CHARACTER SET {$CHARSET_SORT} COLLATE {$COLLATE_SORT} NOT NULL, + PRIMARY KEY (`id`), + KEY `key_join` (`objectPHID`,`indexKey`,`indexValue`(64)), + KEY `key_find` (`indexKey`,`indexValue`(64)) +) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE={$COLLATE_TEXT}; + +USE `{$NAMESPACE}_role`; + + SET NAMES utf8 ; + + SET character_set_client = {$CHARSET} ; + +CREATE TABLE `role_datasourcetoken` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `roleID` int(10) unsigned NOT NULL, + `token` varchar(128) CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `token` (`token`,`roleID`), + KEY `roleID` (`roleID`) +) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE={$COLLATE_TEXT}; + +USE `{$NAMESPACE}_role`; + + SET NAMES utf8 ; + + SET character_set_client = {$CHARSET} ; + +CREATE TABLE `role_role_fdocument` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `objectPHID` varbinary(64) NOT NULL, + `isClosed` tinyint(1) NOT NULL, + `authorPHID` varbinary(64) DEFAULT NULL, + `ownerPHID` varbinary(64) DEFAULT NULL, + `epochCreated` int(10) unsigned NOT NULL, + `epochModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_object` (`objectPHID`), + KEY `key_author` (`authorPHID`), + KEY `key_owner` (`ownerPHID`), + KEY `key_created` (`epochCreated`), + KEY `key_modified` (`epochModified`) +) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE={$COLLATE_TEXT}; + +USE `{$NAMESPACE}_role`; + + SET NAMES utf8 ; + + SET character_set_client = {$CHARSET} ; + +CREATE TABLE `role_role_ffield` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `documentID` int(10) unsigned NOT NULL, + `fieldKey` varchar(4) CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + `rawCorpus` longtext CHARACTER SET {$CHARSET_SORT} COLLATE {$COLLATE_SORT} NOT NULL, + `termCorpus` longtext CHARACTER SET {$CHARSET_SORT} COLLATE {$COLLATE_SORT} NOT NULL, + `normalCorpus` longtext CHARACTER SET {$CHARSET_SORT} COLLATE {$COLLATE_SORT} NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_documentfield` (`documentID`,`fieldKey`) +) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE={$COLLATE_TEXT}; + +USE `{$NAMESPACE}_role`; + + SET NAMES utf8 ; + + SET character_set_client = {$CHARSET} ; + +CREATE TABLE `role_role_fngrams` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `documentID` int(10) unsigned NOT NULL, + `ngram` char(3) CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + PRIMARY KEY (`id`), + KEY `key_ngram` (`ngram`,`documentID`), + KEY `key_object` (`documentID`) +) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE={$COLLATE_TEXT}; + +USE `{$NAMESPACE}_role`; + + SET NAMES utf8 ; + + SET character_set_client = {$CHARSET} ; + +CREATE TABLE `role_role_fngrams_common` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `ngram` char(3) CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + `needsCollection` tinyint(1) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_ngram` (`ngram`), + KEY `key_collect` (`needsCollection`) +) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE={$COLLATE_TEXT}; + +USE `{$NAMESPACE}_role`; + + SET NAMES utf8 ; + + SET character_set_client = {$CHARSET} ; + +CREATE TABLE `role_slug` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `rolePHID` varbinary(64) NOT NULL, + `slug` varchar(128) CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_slug` (`slug`), + KEY `key_rolePHID` (`rolePHID`) +) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE={$COLLATE_TEXT}; + +USE `{$NAMESPACE}_role`; + + SET NAMES utf8 ; + + SET character_set_client = {$CHARSET} ; + +CREATE TABLE `role_transaction` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varbinary(64) NOT NULL, + `authorPHID` varbinary(64) NOT NULL, + `objectPHID` varbinary(64) NOT NULL, + `viewPolicy` varbinary(64) NOT NULL, + `editPolicy` varbinary(64) NOT NULL, + `commentPHID` varbinary(64) DEFAULT NULL, + `commentVersion` int(10) unsigned NOT NULL, + `transactionType` varchar(32) CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + `oldValue` longtext CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + `newValue` longtext CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + `contentSource` longtext CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + `metadata` longtext CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + KEY `key_object` (`objectPHID`) +) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE={$COLLATE_TEXT}; + +USE `{$NAMESPACE}_role`; + + SET NAMES utf8 ; + + SET character_set_client = {$CHARSET} ; + +CREATE TABLE `role_trigger` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varbinary(64) NOT NULL, + `name` varchar(255) CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + `editPolicy` varbinary(64) NOT NULL, + `ruleset` longtext CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`) +) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE={$COLLATE_TEXT}; + +USE `{$NAMESPACE}_role`; + + SET NAMES utf8 ; + + SET character_set_client = {$CHARSET} ; + +CREATE TABLE `role_triggertransaction` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varbinary(64) NOT NULL, + `authorPHID` varbinary(64) NOT NULL, + `objectPHID` varbinary(64) NOT NULL, + `viewPolicy` varbinary(64) NOT NULL, + `editPolicy` varbinary(64) NOT NULL, + `commentPHID` varbinary(64) DEFAULT NULL, + `commentVersion` int(10) unsigned NOT NULL, + `transactionType` varchar(32) COLLATE {$COLLATE_TEXT} NOT NULL, + `oldValue` longtext COLLATE {$COLLATE_TEXT} NOT NULL, + `newValue` longtext COLLATE {$COLLATE_TEXT} NOT NULL, + `contentSource` longtext COLLATE {$COLLATE_TEXT} NOT NULL, + `metadata` longtext COLLATE {$COLLATE_TEXT} NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + KEY `key_object` (`objectPHID`) +) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE={$COLLATE_TEXT}; + +USE `{$NAMESPACE}_role`; + + SET NAMES utf8 ; + + SET character_set_client = {$CHARSET} ; + +CREATE TABLE `role_triggerusage` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `triggerPHID` varbinary(64) NOT NULL, + `examplePHID` varbinary(64) DEFAULT NULL, + `columnCount` int(10) unsigned NOT NULL, + `activeColumnCount` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_trigger` (`triggerPHID`) +) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE={$COLLATE_TEXT}; + + CREATE DATABASE /*!32312 IF NOT EXISTS*/ `{$NAMESPACE}_project` /*!40100 DEFAULT CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} */; USE `{$NAMESPACE}_project`; @@ -8560,6 +8945,214 @@ CREATE TABLE `project_triggerusage` ( UNIQUE KEY `key_trigger` (`triggerPHID`) ) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE={$COLLATE_TEXT}; + + +CREATE DATABASE /*!32312 IF NOT EXISTS*/ `{$NAMESPACE}_releeph` /*!40100 DEFAULT CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} */; + +USE `{$NAMESPACE}_releeph`; + + SET NAMES utf8 ; + + SET character_set_client = {$CHARSET} ; + +CREATE TABLE `releeph_branch` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + `basename` varchar(64) CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + `releephProjectID` int(10) unsigned NOT NULL, + `createdByUserPHID` varbinary(64) NOT NULL, + `cutPointCommitPHID` varbinary(64) NOT NULL, + `isActive` tinyint(1) NOT NULL DEFAULT '1', + `symbolicName` varchar(64) CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} DEFAULT NULL, + `details` longtext CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + `phid` varbinary(64) NOT NULL, + `name` varchar(128) CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `releephProjectID_2` (`releephProjectID`,`basename`), + UNIQUE KEY `releephProjectID_name` (`releephProjectID`,`name`), + UNIQUE KEY `key_phid` (`phid`), + UNIQUE KEY `releephProjectID` (`releephProjectID`,`symbolicName`) +) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE={$COLLATE_TEXT}; + +USE `{$NAMESPACE}_releeph`; + + SET NAMES utf8 ; + + SET character_set_client = {$CHARSET} ; + +CREATE TABLE `releeph_branchtransaction` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varbinary(64) NOT NULL, + `authorPHID` varbinary(64) NOT NULL, + `objectPHID` varbinary(64) NOT NULL, + `viewPolicy` varbinary(64) NOT NULL, + `editPolicy` varbinary(64) NOT NULL, + `commentPHID` varbinary(64) DEFAULT NULL, + `commentVersion` int(10) unsigned NOT NULL, + `transactionType` varchar(32) CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + `oldValue` longtext CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + `newValue` longtext CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + `contentSource` longtext CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + `metadata` longtext CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + KEY `key_object` (`objectPHID`) +) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE={$COLLATE_TEXT}; + +USE `{$NAMESPACE}_releeph`; + + SET NAMES utf8 ; + + SET character_set_client = {$CHARSET} ; + +CREATE TABLE `releeph_producttransaction` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varbinary(64) NOT NULL, + `authorPHID` varbinary(64) NOT NULL, + `objectPHID` varbinary(64) NOT NULL, + `viewPolicy` varbinary(64) NOT NULL, + `editPolicy` varbinary(64) NOT NULL, + `commentPHID` varbinary(64) DEFAULT NULL, + `commentVersion` int(10) unsigned NOT NULL, + `transactionType` varchar(32) CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + `oldValue` longtext CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + `newValue` longtext CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + `contentSource` longtext CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + `metadata` longtext CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + KEY `key_object` (`objectPHID`) +) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE={$COLLATE_TEXT}; + +USE `{$NAMESPACE}_releeph`; + + SET NAMES utf8 ; + + SET character_set_client = {$CHARSET} ; + +CREATE TABLE `releeph_project` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + `phid` varbinary(64) NOT NULL, + `name` varchar(128) CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + `trunkBranch` varchar(255) CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + `repositoryPHID` varbinary(64) NOT NULL, + `createdByUserPHID` varbinary(64) NOT NULL, + `isActive` tinyint(1) NOT NULL DEFAULT '1', + `details` longtext CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `projectName` (`name`), + UNIQUE KEY `key_phid` (`phid`) +) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE={$COLLATE_TEXT}; + +USE `{$NAMESPACE}_releeph`; + + SET NAMES utf8 ; + + SET character_set_client = {$CHARSET} ; + +CREATE TABLE `releeph_role` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + `phid` varbinary(64) NOT NULL, + `name` varchar(128) CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + `trunkBranch` varchar(255) CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + `repositoryPHID` varbinary(64) NOT NULL, + `createdByUserPHID` varbinary(64) NOT NULL, + `isActive` tinyint(1) NOT NULL DEFAULT '1', + `details` longtext CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `roleName` (`name`), + UNIQUE KEY `key_phid` (`phid`) +) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE={$COLLATE_TEXT}; + +USE `{$NAMESPACE}_releeph`; + + SET NAMES utf8 ; + + SET character_set_client = {$CHARSET} ; + +CREATE TABLE `releeph_request` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + `phid` varbinary(64) NOT NULL, + `branchID` int(10) unsigned NOT NULL, + `requestUserPHID` varbinary(64) NOT NULL, + `requestCommitPHID` varbinary(64) DEFAULT NULL, + `commitIdentifier` varchar(40) CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} DEFAULT NULL, + `commitPHID` varbinary(64) DEFAULT NULL, + `pickStatus` int(10) unsigned DEFAULT NULL, + `details` longtext CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + `userIntents` longtext CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT}, + `inBranch` tinyint(1) NOT NULL DEFAULT '0', + `mailKey` binary(20) NOT NULL, + `requestedObjectPHID` varbinary(64) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `phid` (`phid`), + UNIQUE KEY `requestIdentifierBranch` (`requestCommitPHID`,`branchID`), + KEY `branchID` (`branchID`), + KEY `key_requestedObject` (`requestedObjectPHID`) +) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE={$COLLATE_TEXT}; + +USE `{$NAMESPACE}_releeph`; + + SET NAMES utf8 ; + + SET character_set_client = {$CHARSET} ; + +CREATE TABLE `releeph_requesttransaction` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varbinary(64) NOT NULL, + `authorPHID` varbinary(64) NOT NULL, + `objectPHID` varbinary(64) NOT NULL, + `viewPolicy` varbinary(64) NOT NULL, + `editPolicy` varbinary(64) NOT NULL, + `commentPHID` varbinary(64) DEFAULT NULL, + `commentVersion` int(10) unsigned NOT NULL, + `transactionType` varchar(32) CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + `oldValue` longtext CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + `newValue` longtext CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + `metadata` longtext CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + `contentSource` longtext CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + KEY `key_object` (`objectPHID`) +) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE={$COLLATE_TEXT}; + +USE `{$NAMESPACE}_releeph`; + + SET NAMES utf8 ; + + SET character_set_client = {$CHARSET} ; + +CREATE TABLE `releeph_requesttransaction_comment` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varbinary(64) NOT NULL, + `transactionPHID` varbinary(64) DEFAULT NULL, + `authorPHID` varbinary(64) NOT NULL, + `viewPolicy` varbinary(64) NOT NULL, + `editPolicy` varbinary(64) NOT NULL, + `commentVersion` int(10) unsigned NOT NULL, + `content` longtext CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + `contentSource` longtext CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} NOT NULL, + `isDeleted` tinyint(1) NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + UNIQUE KEY `key_version` (`transactionPHID`,`commentVersion`) +) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE={$COLLATE_TEXT}; + CREATE DATABASE /*!32312 IF NOT EXISTS*/ `{$NAMESPACE}_repository` /*!40100 DEFAULT CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} */; USE `{$NAMESPACE}_repository`; @@ -10649,3 +11242,20 @@ CREATE TABLE `xhprof_sample` ( PRIMARY KEY (`id`), UNIQUE KEY `filePHID` (`filePHID`) ) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE={$COLLATE_TEXT}; + +CREATE DATABASE /*!32312 IF NOT EXISTS*/ `{$NAMESPACE}_timetracker` /*!40100 DEFAULT CHARACTER SET {$CHARSET} COLLATE {$COLLATE_TEXT} */; + +USE `{$NAMESPACE}_timetracker`; + + SET NAMES utf8 ; + + SET character_set_client = {$CHARSET} ; + +CREATE TABLE `timetracker_trackedtime` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `userID` int(10) unsigned NOT NULL, + `numMinutes` int(10) NOT NULL, + `dateWhenTrackedFor` int(12) NOT NULL, + `realDateWhenTracked` int(12) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE={$COLLATE_TEXT}; diff --git a/scripts/move_beneath.php b/scripts/move_beneath.php new file mode 100644 index 0000000000..4e3b3ffa1f --- /dev/null +++ b/scripts/move_beneath.php @@ -0,0 +1,237 @@ +#!/usr/bin/env php + for discussion. + +require_once 'scripts/__init_script__.php'; + +$args = new PhutilArgumentParser($argv); +$args->parseStandardArguments(); +$args->parse( + array( + array( + 'name' => 'milestone', + 'help' => pht( + 'Turn the project into a milestone. Or, use --subproject.'), + ), + array( + 'name' => 'child', + 'param' => 'project', + 'help' => pht('The project to make a child of the --parent project.'), + ), + array( + 'name' => 'parent', + 'param' => 'project', + 'help' => pht('The project to make a parent of the --child project.'), + ), + array( + 'name' => 'subproject', + 'help' => pht( + 'Turn the project into a subproject. Or, use --milestone.'), + ), + array( + 'name' => 'keep-members', + 'param' => 'mode', + 'help' => pht('Choose which members to keep: both, child, parent.'), + ), + )); + + +$parent_name = $args->getArg('parent'); +$child_name = $args->getArg('child'); + +if (!$parent_name) { + throw new PhutilArgumentUsageException( + pht( + 'Choose which project should become the parent with --parent.')); +} + +if (!$child_name) { + throw new PhutilArgumentUsageException( + pht( + 'Choose which project should become the child with --child.')); +} + +$keep_members = $args->getArg('keep-members'); +switch ($keep_members) { + case 'both': + case 'child': + case 'parent': + break; + default: + if (!$keep_members) { + throw new PhutilArgumentUsageException( + pht( + 'Choose which members to keep with --keep-members.')); + } else { + throw new PhutilArgumentUsageException( + pht( + 'Valid --keep-members settings are: both, child, parent.')); + } +} + +$want_milestone = $args->getArg('milestone'); +$want_subproject = $args->getArg('subproject'); +if (!$want_milestone && !$want_subproject) { + throw new PhutilArgumentUsageException( + pht( + 'Use --milestone or --subproject to select what kind of child the '. + 'project should become.')); +} else if ($want_milestone && $want_subproject) { + throw new PhutilArgumentUsageException( + pht( + 'Use either --milestone or --subproject, not both, to select what kind '. + 'of project the child should become.')); +} +$is_milestone = $want_milestone; + +$parent = load_project($parent_name); +$child = load_project($child_name); + +if ($parent->isMilestone()) { + throw new PhutilArgumentUsageException( + pht( + 'The selected parent project is a milestone, and milestones may '. + 'not have children.')); +} + +if ($child->getParentProjectPHID()) { + throw new PhutilArgumentUsageException( + pht( + 'The selected child project is already a child of another project. '. + 'This script can only move root-level projects beneath other projects, '. + 'not move children within a hierarchy.')); +} + +if ($child->getHasSubprojects() || $child->getHasMilestones()) { + throw new PhutilArgumentUsageException( + pht( + 'The selected child project already has subprojects or milestones '. + 'of its own. This script can not move entire trees of projects.')); +} + +if ($parent->getPHID() == $child->getPHID()) { + throw new PhutilArgumentUsageException( + pht( + 'The parent and child are the same project. There is no conceivable '. + 'physical interpretation of what you are attempting to do.')); +} + + +if ($is_milestone) { + if (($keep_members != 'parent') && $parent->getHasSubprojects()) { + throw new PhutilArgumentUsageException( + pht( + 'You can not use "child" or "both" modes when making a project a '. + 'milestone of a project with existing subprojects: there is nowhere '. + 'to put the members.')); + } + + $copy_parent = false; + $copy_child = ($keep_members != 'parent'); + $wipe_parent = ($keep_members == 'child'); + $wipe_child = true; +} else { + $copy_parent = ($keep_members != 'child'); + $copy_child = false; + $wipe_parent = true; + $wipe_child = ($keep_members == 'parent'); +} + +$child->setParentProjectPHID($parent->getPHID()); +$child->attachParentProject($parent); + +if ($is_milestone) { + $next_number = $parent->loadNextMilestoneNumber(); + $child->setMilestoneNumber($next_number); +} + +$child->save(); + +$member_type = PhabricatorProjectProjectHasMemberEdgeType::EDGECONST; + +$parent_members = PhabricatorEdgeQuery::loadDestinationPHIDs( + $parent->getPHID(), + $member_type); + +$child_members = PhabricatorEdgeQuery::loadDestinationPHIDs( + $child->getPHID(), + $member_type); + +if ($copy_parent) { + edit_members($parent_members, $child, true); +} + +if ($copy_child) { + edit_members($child_members, $parent, true); +} + +if ($wipe_parent) { + edit_members($parent_members, $parent, false); +} + +if ($wipe_child) { + edit_members($child_members, $child, false); +} + +id(new PhabricatorProjectsMembershipIndexEngineExtension()) + ->rematerialize($parent); + +id(new PhabricatorProjectsMembershipIndexEngineExtension()) + ->rematerialize($child); + +echo tsprintf( + "%s\n", + pht('Done.')); + + +function load_project($name) { + $viewer = PhabricatorUser::getOmnipotentUser(); + + $project = id(new PhabricatorProjectQuery()) + ->setViewer($viewer) + ->withSlugs(array($name)) + ->executeOne(); + if ($project) { + return $project; + } + + $project = id(new PhabricatorProjectQuery()) + ->setViewer($viewer) + ->withPHIDs(array($name)) + ->executeOne(); + if ($project) { + return $project; + } + + $project = id(new PhabricatorProjectQuery()) + ->setViewer($viewer) + ->withIDs(array($name)) + ->executeOne(); + if ($project) { + return $project; + } + + throw new Exception( + pht( + 'Unknown project "%s"! Use a hashtags, PHID, or ID to choose a project.', + $name)); +} + +function edit_members(array $phids, PhabricatorProject $target, $add) { + if (!$phids) { + return; + } + + $member_type = PhabricatorProjectProjectHasMemberEdgeType::EDGECONST; + + $editor = id(new PhabricatorEdgeEditor()); + foreach ($phids as $phid) { + if ($add) { + $editor->addEdge($target->getPHID(), $member_type, $phid); + } else { + $editor->removeEdge($target->getPHID(), $member_type, $phid); + } + } + $editor->save(); +} diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index b3637667ee..4e9a446e23 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1606,6 +1606,7 @@ 'HeraldEmptyFieldValue' => 'applications/herald/value/HeraldEmptyFieldValue.php', 'HeraldEngine' => 'applications/herald/engine/HeraldEngine.php', 'HeraldExactProjectsField' => 'applications/project/herald/HeraldExactProjectsField.php', + 'HeraldExactRolesField' => 'applications/roles/herald/HeraldExactRolesField.php', 'HeraldField' => 'applications/herald/field/HeraldField.php', 'HeraldFieldGroup' => 'applications/herald/field/HeraldFieldGroup.php', 'HeraldFieldTestCase' => 'applications/herald/field/__tests__/HeraldFieldTestCase.php', @@ -1634,6 +1635,7 @@ 'HeraldRelatedFieldGroup' => 'applications/herald/field/HeraldRelatedFieldGroup.php', 'HeraldRemarkupFieldValue' => 'applications/herald/value/HeraldRemarkupFieldValue.php', 'HeraldRemarkupRule' => 'applications/herald/remarkup/HeraldRemarkupRule.php', + 'HeraldRolesField' => 'applications/roles/herald/HeraldRolesField.php', 'HeraldRule' => 'applications/herald/storage/HeraldRule.php', 'HeraldRuleActionAffectsObjectEdgeType' => 'applications/herald/edge/HeraldRuleActionAffectsObjectEdgeType.php', 'HeraldRuleAdapter' => 'applications/herald/adapter/HeraldRuleAdapter.php', @@ -1773,6 +1775,25 @@ 'MacroEditConduitAPIMethod' => 'applications/macro/conduit/MacroEditConduitAPIMethod.php', 'MacroEmojiExample' => 'applications/uiexample/examples/MacroEmojiExample.php', 'MacroQueryConduitAPIMethod' => 'applications/macro/conduit/MacroQueryConduitAPIMethod.php', + 'ManagementPanel' => 'applications/managementpanel/components/ManagementPanel.php', + 'ManagementPanelApplication' => 'applications/managementpanel/application/ManagementPanelApplication.php', + 'ManagementPanelDayDetailsRequestHandler' => 'applications/managementpanel/requesthandlers/ManagementPanelDayDetailsRequestHandler.php', + 'ManagementPanelDayHistoryDetailsBox' => 'applications/managementpanel/components/ManagementPanelDayHistoryDetailsBox.php', + 'ManagementPanelEditUserDate' => 'applications/managementpanel/components/ManagementPanelEditUserDate.php', + 'ManagementPanelMainPanel' => 'applications/managementpanel/components/ManagementPanelMainPanel.php', + 'ManagementPanelMainPanelRequestHandler' => 'applications/managementpanel/requesthandlers/ManagementPanelMainPanelRequestHandler.php', + 'ManagementPanelPanelType' => 'applications/managementpanel/components/ManagementPanelPanelType.php', + 'ManagementPanelRenderController' => 'applications/managementpanel/controller/ManagementPanelRenderController.php', + 'ManagementPanelRequestHandler' => 'applications/managementpanel/requesthandlers/ManagementPanelRequestHandler.php', + 'ManagementPanelStorageManager' => 'applications/managementpanel/application/ManagementPanelStorageManager.php', + 'ManagementPanelSummaryPanel' => 'applications/managementpanel/components/ManagementPanelSummaryPanel.php', + 'ManagementPanelSummaryPanelRequestHandler' => 'applications/managementpanel/requesthandlers/ManagementPanelSummaryPanelRequestHandler.php', + 'ManagementPanelTimeUtils' => 'applications/managementpanel/application/ManagementPanelTimeUtils.php', + 'ManagementPanelUser' => 'applications/managementpanel/components/ManagementPanelUser.php', + 'ManagementPanelUserDateRegistrationBox' => 'applications/managementpanel/components/ManagementPanelUserDateRegistrationBox.php', + 'ManagementPanelUserDateRequestHandler' => 'applications/managementpanel/requesthandlers/ManagementPanelUserDateRequestHandler.php', + 'ManagementPanelUserNameBox' => 'applications/managementpanel/components/ManagementPanelUserNameBox.php', + 'ManagementPanelUserRequestInfo' => 'applications/managementpanel/requesthandlers/ManagementPanelUserRequestInfo.php', 'ManiphestAssignEmailCommand' => 'applications/maniphest/command/ManiphestAssignEmailCommand.php', 'ManiphestAssigneeDatasource' => 'applications/maniphest/typeahead/ManiphestAssigneeDatasource.php', 'ManiphestBulkEditCapability' => 'applications/maniphest/capability/ManiphestBulkEditCapability.php', @@ -1831,7 +1852,9 @@ 'ManiphestTaskClosedStatusDatasource' => 'applications/maniphest/typeahead/ManiphestTaskClosedStatusDatasource.php', 'ManiphestTaskCoverImageTransaction' => 'applications/maniphest/xaction/ManiphestTaskCoverImageTransaction.php', 'ManiphestTaskDependedOnByTaskEdgeType' => 'applications/maniphest/edge/ManiphestTaskDependedOnByTaskEdgeType.php', + 'ManiphestTaskBlockedEdgeType' => 'applications/maniphest/edge/ManiphestTaskBlockedEdgeType.php', 'ManiphestTaskDependsOnTaskEdgeType' => 'applications/maniphest/edge/ManiphestTaskDependsOnTaskEdgeType.php', + 'ManiphestTaskBlockerEdgeType' => 'applications/maniphest/edge/ManiphestTaskBlockerEdgeType.php', 'ManiphestTaskDescriptionHeraldField' => 'applications/maniphest/herald/ManiphestTaskDescriptionHeraldField.php', 'ManiphestTaskDescriptionTransaction' => 'applications/maniphest/xaction/ManiphestTaskDescriptionTransaction.php', 'ManiphestTaskDetailController' => 'applications/maniphest/controller/ManiphestTaskDetailController.php', @@ -1848,9 +1871,11 @@ 'ManiphestTaskHasMockEdgeType' => 'applications/maniphest/edge/ManiphestTaskHasMockEdgeType.php', 'ManiphestTaskHasMockRelationship' => 'applications/maniphest/relationship/ManiphestTaskHasMockRelationship.php', 'ManiphestTaskHasParentRelationship' => 'applications/maniphest/relationship/ManiphestTaskHasParentRelationship.php', + 'ManiphestTaskHasBlockedRelationship' => 'applications/maniphest/relationship/ManiphestTaskHasBlockedRelationship.php', 'ManiphestTaskHasRevisionEdgeType' => 'applications/maniphest/edge/ManiphestTaskHasRevisionEdgeType.php', 'ManiphestTaskHasRevisionRelationship' => 'applications/maniphest/relationship/ManiphestTaskHasRevisionRelationship.php', 'ManiphestTaskHasSubtaskRelationship' => 'applications/maniphest/relationship/ManiphestTaskHasSubtaskRelationship.php', + 'ManiphestTaskHasBlockerRelationship' => 'applications/maniphest/relationship/ManiphestTaskHasBlockerRelationship.php', 'ManiphestTaskHeraldField' => 'applications/maniphest/herald/ManiphestTaskHeraldField.php', 'ManiphestTaskHeraldFieldGroup' => 'applications/maniphest/herald/ManiphestTaskHeraldFieldGroup.php', 'ManiphestTaskIsDuplicateOfTaskEdgeType' => 'applications/maniphest/edge/ManiphestTaskIsDuplicateOfTaskEdgeType.php', @@ -1867,6 +1892,7 @@ 'ManiphestTaskPHIDResolver' => 'applications/maniphest/httpparametertype/ManiphestTaskPHIDResolver.php', 'ManiphestTaskPHIDType' => 'applications/maniphest/phid/ManiphestTaskPHIDType.php', 'ManiphestTaskParentTransaction' => 'applications/maniphest/xaction/ManiphestTaskParentTransaction.php', + 'ManiphestTaskBlockedTransaction' => 'applications/maniphest/xaction/ManiphestTaskBlockedTransaction.php', 'ManiphestTaskPoints' => 'applications/maniphest/constants/ManiphestTaskPoints.php', 'ManiphestTaskPointsTransaction' => 'applications/maniphest/xaction/ManiphestTaskPointsTransaction.php', 'ManiphestTaskPolicyCodex' => 'applications/maniphest/policy/ManiphestTaskPolicyCodex.php', @@ -1889,6 +1915,7 @@ 'ManiphestTaskStatusTransaction' => 'applications/maniphest/xaction/ManiphestTaskStatusTransaction.php', 'ManiphestTaskSubpriorityTransaction' => 'applications/maniphest/xaction/ManiphestTaskSubpriorityTransaction.php', 'ManiphestTaskSubtaskController' => 'applications/maniphest/controller/ManiphestTaskSubtaskController.php', + 'ManiphestTaskBlockerController' => 'applications/maniphest/controller/ManiphestTaskBlockerController.php', 'ManiphestTaskSubtypeDatasource' => 'applications/maniphest/typeahead/ManiphestTaskSubtypeDatasource.php', 'ManiphestTaskTitleHeraldField' => 'applications/maniphest/herald/ManiphestTaskTitleHeraldField.php', 'ManiphestTaskTitleTransaction' => 'applications/maniphest/xaction/ManiphestTaskTitleTransaction.php', @@ -4717,6 +4744,210 @@ 'PhabricatorRobotsPlatformController' => 'applications/system/controller/robots/PhabricatorRobotsPlatformController.php', 'PhabricatorRobotsResourceController' => 'applications/system/controller/robots/PhabricatorRobotsResourceController.php', 'PhabricatorRobotsShortController' => 'applications/system/controller/robots/PhabricatorRobotsShortController.php', + 'PhabricatorRole' => 'applications/roles/storage/PhabricatorRole.php', + 'PhabricatorRoleActivityChartEngine' => 'applications/roles/chart/PhabricatorRoleActivityChartEngine.php', + 'PhabricatorRoleAddHeraldAction' => 'applications/roles/herald/PhabricatorRoleAddHeraldAction.php', + 'PhabricatorRoleApplication' => 'applications/roles/application/PhabricatorRoleApplication.php', + 'PhabricatorRoleArchiveController' => 'applications/roles/controller/PhabricatorRoleArchiveController.php', + 'PhabricatorRoleBoardBackgroundController' => 'applications/roles/controller/PhabricatorRoleBoardBackgroundController.php', + 'PhabricatorRoleBoardColumnsSearchEngineAttachment' => 'applications/roles/engineextension/PhabricatorRoleBoardColumnsSearchEngineAttachment.php', + 'PhabricatorRoleBoardController' => 'applications/roles/controller/PhabricatorRoleBoardController.php', + 'PhabricatorRoleBoardDefaultController' => 'applications/roles/controller/PhabricatorRoleBoardDefaultController.php', + 'PhabricatorRoleBoardDisableController' => 'applications/roles/controller/PhabricatorRoleBoardDisableController.php', + 'PhabricatorRoleBoardFilterController' => 'applications/roles/controller/PhabricatorRoleBoardFilterController.php', + 'PhabricatorRoleBoardImportController' => 'applications/roles/controller/PhabricatorRoleBoardImportController.php', + 'PhabricatorRoleBoardLayoutEngine' => 'applications/roles/engine/PhabricatorRoleBoardLayoutEngine.php', + 'PhabricatorRoleBoardManageController' => 'applications/roles/controller/PhabricatorRoleBoardManageController.php', + 'PhabricatorRoleBoardReloadController' => 'applications/roles/controller/PhabricatorRoleBoardReloadController.php', + 'PhabricatorRoleBoardRenderingEngine' => 'applications/roles/engine/PhabricatorRoleBoardRenderingEngine.php', + 'PhabricatorRoleBoardReorderController' => 'applications/roles/controller/PhabricatorRoleBoardReorderController.php', + 'PhabricatorRoleBoardResponseEngine' => 'applications/roles/engine/PhabricatorRoleBoardResponseEngine.php', + 'PhabricatorRoleBoardViewController' => 'applications/roles/controller/PhabricatorRoleBoardViewController.php', + 'PhabricatorRoleBurndownChartEngine' => 'applications/roles/chart/PhabricatorRoleBurndownChartEngine.php', + 'PhabricatorRoleCardView' => 'applications/roles/view/PhabricatorRoleCardView.php', + 'PhabricatorRoleColorTransaction' => 'applications/roles/xaction/PhabricatorRoleColorTransaction.php', + 'PhabricatorRoleColorsConfigType' => 'applications/roles/config/PhabricatorRoleColorsConfigType.php', + 'PhabricatorRoleColumn' => 'applications/roles/storage/PhabricatorRoleColumn.php', + 'PhabricatorRoleColumnAuthorOrder' => 'applications/roles/order/PhabricatorRoleColumnAuthorOrder.php', + 'PhabricatorRoleColumnBulkEditController' => 'applications/roles/controller/PhabricatorRoleColumnBulkEditController.php', + 'PhabricatorRoleColumnBulkMoveController' => 'applications/roles/controller/PhabricatorRoleColumnBulkMoveController.php', + 'PhabricatorRoleColumnCreatedOrder' => 'applications/roles/order/PhabricatorRoleColumnCreatedOrder.php', + 'PhabricatorRoleColumnDetailController' => 'applications/roles/controller/PhabricatorRoleColumnDetailController.php', + 'PhabricatorRoleColumnEditController' => 'applications/roles/controller/PhabricatorRoleColumnEditController.php', + 'PhabricatorRoleColumnHeader' => 'applications/roles/order/PhabricatorRoleColumnHeader.php', + 'PhabricatorRoleColumnHideController' => 'applications/roles/controller/PhabricatorRoleColumnHideController.php', + 'PhabricatorRoleColumnLimitTransaction' => 'applications/roles/xaction/column/PhabricatorRoleColumnLimitTransaction.php', + 'PhabricatorRoleColumnNameTransaction' => 'applications/roles/xaction/column/PhabricatorRoleColumnNameTransaction.php', + 'PhabricatorRoleColumnNaturalOrder' => 'applications/roles/order/PhabricatorRoleColumnNaturalOrder.php', + 'PhabricatorRoleColumnOrder' => 'applications/roles/order/PhabricatorRoleColumnOrder.php', + 'PhabricatorRoleColumnOwnerOrder' => 'applications/roles/order/PhabricatorRoleColumnOwnerOrder.php', + 'PhabricatorRoleColumnPHIDType' => 'applications/roles/phid/PhabricatorRoleColumnPHIDType.php', + 'PhabricatorRoleColumnPointsOrder' => 'applications/roles/order/PhabricatorRoleColumnPointsOrder.php', + 'PhabricatorRoleColumnPosition' => 'applications/roles/storage/PhabricatorRoleColumnPosition.php', + 'PhabricatorRoleColumnPositionQuery' => 'applications/roles/query/PhabricatorRoleColumnPositionQuery.php', + 'PhabricatorRoleColumnPriorityOrder' => 'applications/roles/order/PhabricatorRoleColumnPriorityOrder.php', + 'PhabricatorRoleColumnProxyInterface' => 'applications/roles/interface/PhabricatorRoleColumnProxyInterface.php', + 'PhabricatorRoleColumnQuery' => 'applications/roles/query/PhabricatorRoleColumnQuery.php', + 'PhabricatorRoleColumnRemoveTriggerController' => 'applications/roles/controller/PhabricatorRoleColumnRemoveTriggerController.php', + 'PhabricatorRoleColumnSearchEngine' => 'applications/roles/query/PhabricatorRoleColumnSearchEngine.php', + 'PhabricatorRoleColumnStatusOrder' => 'applications/roles/order/PhabricatorRoleColumnStatusOrder.php', + 'PhabricatorRoleColumnStatusTransaction' => 'applications/roles/xaction/column/PhabricatorRoleColumnStatusTransaction.php', + 'PhabricatorRoleColumnTitleOrder' => 'applications/roles/order/PhabricatorRoleColumnTitleOrder.php', + 'PhabricatorRoleColumnTransaction' => 'applications/roles/storage/PhabricatorRoleColumnTransaction.php', + 'PhabricatorRoleColumnTransactionEditor' => 'applications/roles/editor/PhabricatorRoleColumnTransactionEditor.php', + 'PhabricatorRoleColumnTransactionQuery' => 'applications/roles/query/PhabricatorRoleColumnTransactionQuery.php', + 'PhabricatorRoleColumnTransactionType' => 'applications/roles/xaction/column/PhabricatorRoleColumnTransactionType.php', + 'PhabricatorRoleColumnTriggerTransaction' => 'applications/roles/xaction/column/PhabricatorRoleColumnTriggerTransaction.php', + 'PhabricatorRoleColumnViewQueryController' => 'applications/roles/controller/PhabricatorRoleColumnViewQueryController.php', + 'PhabricatorRoleConfigOptions' => 'applications/roles/config/PhabricatorRoleConfigOptions.php', + 'PhabricatorRoleConfiguredCustomField' => 'applications/roles/customfield/PhabricatorRoleConfiguredCustomField.php', + 'PhabricatorRoleController' => 'applications/roles/controller/PhabricatorRoleController.php', + 'PhabricatorRoleCoreTestCase' => 'applications/roles/__tests__/PhabricatorRoleCoreTestCase.php', + 'PhabricatorRoleCoverController' => 'applications/roles/controller/PhabricatorRoleCoverController.php', + 'PhabricatorRoleCustomField' => 'applications/roles/customfield/PhabricatorRoleCustomField.php', + 'PhabricatorRoleCustomFieldNumericIndex' => 'applications/roles/storage/PhabricatorRoleCustomFieldNumericIndex.php', + 'PhabricatorRoleCustomFieldStorage' => 'applications/roles/storage/PhabricatorRoleCustomFieldStorage.php', + 'PhabricatorRoleCustomFieldStringIndex' => 'applications/roles/storage/PhabricatorRoleCustomFieldStringIndex.php', + 'PhabricatorRoleDAO' => 'applications/roles/storage/PhabricatorRoleDAO.php', + 'PhabricatorRoleDatasource' => 'applications/roles/typeahead/PhabricatorRoleDatasource.php', + 'PhabricatorRoleDescriptionField' => 'applications/roles/customfield/PhabricatorRoleDescriptionField.php', + 'PhabricatorRoleDetailsProfileMenuItem' => 'applications/roles/menuitem/PhabricatorRoleDetailsProfileMenuItem.php', + 'PhabricatorRoleDropEffect' => 'applications/roles/icon/PhabricatorRoleDropEffect.php', + 'PhabricatorRoleEditController' => 'applications/roles/controller/PhabricatorRoleEditController.php', + 'PhabricatorRoleEditEngine' => 'applications/roles/engine/PhabricatorRoleEditEngine.php', + 'PhabricatorRoleEditPictureController' => 'applications/roles/controller/PhabricatorRoleEditPictureController.php', + 'PhabricatorRoleFerretEngine' => 'applications/roles/search/PhabricatorRoleFerretEngine.php', + 'PhabricatorRoleFilterTransaction' => 'applications/roles/xaction/PhabricatorRoleFilterTransaction.php', + 'PhabricatorRoleFulltextEngine' => 'applications/roles/search/PhabricatorRoleFulltextEngine.php', + 'PhabricatorRoleHeraldAction' => 'applications/roles/herald/PhabricatorRoleHeraldAction.php', + 'PhabricatorRoleHeraldAdapter' => 'applications/roles/herald/PhabricatorRoleHeraldAdapter.php', + 'PhabricatorRoleHeraldFieldGroup' => 'applications/roles/herald/PhabricatorRoleHeraldFieldGroup.php', + 'PhabricatorRoleHovercardEngineExtension' => 'applications/roles/engineextension/PhabricatorRoleHovercardEngineExtension.php', + 'PhabricatorRoleIconSet' => 'applications/roles/icon/PhabricatorRoleIconSet.php', + 'PhabricatorRoleIconTransaction' => 'applications/roles/xaction/PhabricatorRoleIconTransaction.php', + 'PhabricatorRoleIconsConfigType' => 'applications/roles/config/PhabricatorRoleIconsConfigType.php', + 'PhabricatorRoleImageTransaction' => 'applications/roles/xaction/PhabricatorRoleImageTransaction.php', + 'PhabricatorRoleInterface' => 'applications/roles/interface/PhabricatorRoleInterface.php', + 'PhabricatorRoleListController' => 'applications/roles/controller/PhabricatorRoleListController.php', + 'PhabricatorRoleListView' => 'applications/roles/view/PhabricatorRoleListView.php', + 'PhabricatorRoleLockController' => 'applications/roles/controller/PhabricatorRoleLockController.php', + 'PhabricatorRoleLockTransaction' => 'applications/roles/xaction/PhabricatorRoleLockTransaction.php', + 'PhabricatorRoleLogicalAncestorDatasource' => 'applications/roles/typeahead/PhabricatorRoleLogicalAncestorDatasource.php', + 'PhabricatorRoleLogicalDatasource' => 'applications/roles/typeahead/PhabricatorRoleLogicalDatasource.php', + 'PhabricatorRoleLogicalOnlyDatasource' => 'applications/roles/typeahead/PhabricatorRoleLogicalOnlyDatasource.php', + 'PhabricatorRoleLogicalOrNotDatasource' => 'applications/roles/typeahead/PhabricatorRoleLogicalOrNotDatasource.php', + 'PhabricatorRoleLogicalUserDatasource' => 'applications/roles/typeahead/PhabricatorRoleLogicalUserDatasource.php', + 'PhabricatorRoleLogicalViewerDatasource' => 'applications/roles/typeahead/PhabricatorRoleLogicalViewerDatasource.php', + 'PhabricatorRoleManageController' => 'applications/roles/controller/PhabricatorRoleManageController.php', + 'PhabricatorRoleManageProfileMenuItem' => 'applications/roles/menuitem/PhabricatorRoleManageProfileMenuItem.php', + 'PhabricatorRoleMaterializedMemberEdgeType' => 'applications/roles/edge/PhabricatorRoleMaterializedMemberEdgeType.php', + 'PhabricatorRoleMemberListView' => 'applications/roles/view/PhabricatorRoleMemberListView.php', + 'PhabricatorRoleMemberOfRoleEdgeType' => 'applications/roles/edge/PhabricatorRoleMemberOfRoleEdgeType.php', + 'PhabricatorRoleMembersAddController' => 'applications/roles/controller/PhabricatorRoleMembersAddController.php', + 'PhabricatorRoleMembersDatasource' => 'applications/roles/typeahead/PhabricatorRoleMembersDatasource.php', + 'PhabricatorRoleMembersPolicyRule' => 'applications/roles/policyrule/PhabricatorRoleMembersPolicyRule.php', + 'PhabricatorRoleMembersProfileMenuItem' => 'applications/roles/menuitem/PhabricatorRoleMembersProfileMenuItem.php', + 'PhabricatorRoleMembersRemoveController' => 'applications/roles/controller/PhabricatorRoleMembersRemoveController.php', + 'PhabricatorRoleMembersViewController' => 'applications/roles/controller/PhabricatorRoleMembersViewController.php', + 'PhabricatorRoleMenuItemController' => 'applications/roles/controller/PhabricatorRoleMenuItemController.php', + 'PhabricatorRoleMilestoneTransaction' => 'applications/roles/xaction/PhabricatorRoleMilestoneTransaction.php', + 'PhabricatorRoleMoveController' => 'applications/roles/controller/PhabricatorRoleMoveController.php', + 'PhabricatorRoleNameContextFreeGrammar' => 'applications/roles/lipsum/PhabricatorRoleNameContextFreeGrammar.php', + 'PhabricatorRoleNameTransaction' => 'applications/roles/xaction/PhabricatorRoleNameTransaction.php', + 'PhabricatorRoleNoRolesDatasource' => 'applications/roles/typeahead/PhabricatorRoleNoRolesDatasource.php', + 'PhabricatorRoleObjectHasRoleEdgeType' => 'applications/roles/edge/PhabricatorRoleObjectHasRoleEdgeType.php', + 'PhabricatorRoleOrUserDatasource' => 'applications/roles/typeahead/PhabricatorRoleOrUserDatasource.php', + 'PhabricatorRoleOrUserFunctionDatasource' => 'applications/roles/typeahead/PhabricatorRoleOrUserFunctionDatasource.php', + 'PhabricatorRoleParentTransaction' => 'applications/roles/xaction/PhabricatorRoleParentTransaction.php', + 'PhabricatorRolePictureProfileMenuItem' => 'applications/roles/menuitem/PhabricatorRolePictureProfileMenuItem.php', + 'PhabricatorRolePointsProfileMenuItem' => 'applications/roles/menuitem/PhabricatorRolePointsProfileMenuItem.php', + 'PhabricatorRoleProfileController' => 'applications/roles/controller/PhabricatorRoleProfileController.php', + 'PhabricatorRoleProfileMenuEngine' => 'applications/roles/engine/PhabricatorRoleProfileMenuEngine.php', + 'PhabricatorRoleQuery' => 'applications/roles/query/PhabricatorRoleQuery.php', + 'PhabricatorRoleRemoveHeraldAction' => 'applications/roles/herald/PhabricatorRoleRemoveHeraldAction.php', + 'PhabricatorRoleReportsController' => 'applications/roles/controller/PhabricatorRoleReportsController.php', + 'PhabricatorRoleReportsProfileMenuItem' => 'applications/roles/menuitem/PhabricatorRoleReportsProfileMenuItem.php', + 'PhabricatorRoleRoleHasMemberEdgeType' => 'applications/roles/edge/PhabricatorRoleRoleHasMemberEdgeType.php', + 'PhabricatorRoleRoleHasObjectEdgeType' => 'applications/roles/edge/PhabricatorRoleRoleHasObjectEdgeType.php', + 'PhabricatorRoleRolePHIDType' => 'applications/roles/phid/PhabricatorRoleRolePHIDType.php', + 'PhabricatorRoleSchemaSpec' => 'applications/roles/storage/PhabricatorRoleSchemaSpec.php', + 'PhabricatorRoleSearchEngine' => 'applications/roles/query/PhabricatorRoleSearchEngine.php', + 'PhabricatorRoleSearchField' => 'applications/roles/searchfield/PhabricatorRoleSearchField.php', + 'PhabricatorRoleSilenceController' => 'applications/roles/controller/PhabricatorRoleSilenceController.php', + 'PhabricatorRoleSilencedEdgeType' => 'applications/roles/edge/PhabricatorRoleSilencedEdgeType.php', + 'PhabricatorRoleSlug' => 'applications/roles/storage/PhabricatorRoleSlug.php', + 'PhabricatorRoleSlugsTransaction' => 'applications/roles/xaction/PhabricatorRoleSlugsTransaction.php', + 'PhabricatorRoleSortTransaction' => 'applications/roles/xaction/PhabricatorRoleSortTransaction.php', + 'PhabricatorRoleStandardCustomField' => 'applications/roles/customfield/PhabricatorRoleStandardCustomField.php', + 'PhabricatorRoleStatus' => 'applications/roles/constants/PhabricatorRoleStatus.php', + 'PhabricatorRoleStatusTransaction' => 'applications/roles/xaction/PhabricatorRoleStatusTransaction.php', + 'PhabricatorRoleSubroleWarningController' => 'applications/roles/controller/PhabricatorRoleSubroleWarningController.php', + 'PhabricatorRoleSubrolesController' => 'applications/roles/controller/PhabricatorRoleSubrolesController.php', + 'PhabricatorRoleSubrolesProfileMenuItem' => 'applications/roles/menuitem/PhabricatorRoleSubrolesProfileMenuItem.php', + 'PhabricatorRoleSubtypeDatasource' => 'applications/roles/typeahead/PhabricatorRoleSubtypeDatasource.php', + 'PhabricatorRoleSubtypesConfigType' => 'applications/roles/config/PhabricatorRoleSubtypesConfigType.php', + 'PhabricatorRoleTagsAddedField' => 'applications/roles/herald/PhabricatorRoleTagsAddedField.php', + 'PhabricatorRoleTagsField' => 'applications/roles/herald/PhabricatorRoleTagsField.php', + 'PhabricatorRoleTagsRemovedField' => 'applications/roles/herald/PhabricatorRoleTagsRemovedField.php', + 'PhabricatorRoleTestDataGenerator' => 'applications/roles/lipsum/PhabricatorRoleTestDataGenerator.php', + 'PhabricatorRoleTransaction' => 'applications/roles/storage/PhabricatorRoleTransaction.php', + 'PhabricatorRoleTransactionEditor' => 'applications/roles/editor/PhabricatorRoleTransactionEditor.php', + 'PhabricatorRoleTransactionQuery' => 'applications/roles/query/PhabricatorRoleTransactionQuery.php', + 'PhabricatorRoleTransactionType' => 'applications/roles/xaction/PhabricatorRoleTransactionType.php', + 'PhabricatorRoleTrigger' => 'applications/roles/storage/PhabricatorRoleTrigger.php', + 'PhabricatorRoleTriggerAddRolesRule' => 'applications/roles/trigger/PhabricatorRoleTriggerAddRolesRule.php', + 'PhabricatorRoleTriggerController' => 'applications/roles/controller/trigger/PhabricatorRoleTriggerController.php', + 'PhabricatorRoleTriggerCorruptionException' => 'applications/roles/exception/PhabricatorRoleTriggerCorruptionException.php', + 'PhabricatorRoleTriggerEditController' => 'applications/roles/controller/trigger/PhabricatorRoleTriggerEditController.php', + 'PhabricatorRoleTriggerEditor' => 'applications/roles/editor/PhabricatorRoleTriggerEditor.php', + 'PhabricatorRoleTriggerInvalidRule' => 'applications/roles/trigger/PhabricatorRoleTriggerInvalidRule.php', + 'PhabricatorRoleTriggerListController' => 'applications/roles/controller/trigger/PhabricatorRoleTriggerListController.php', + 'PhabricatorRoleTriggerManiphestOwnerRule' => 'applications/roles/trigger/PhabricatorRoleTriggerManiphestOwnerRule.php', + 'PhabricatorRoleTriggerManiphestPriorityRule' => 'applications/roles/trigger/PhabricatorRoleTriggerManiphestPriorityRule.php', + 'PhabricatorRoleTriggerManiphestStatusRule' => 'applications/roles/trigger/PhabricatorRoleTriggerManiphestStatusRule.php', + 'PhabricatorRoleTriggerNameTransaction' => 'applications/roles/xaction/trigger/PhabricatorRoleTriggerNameTransaction.php', + 'PhabricatorRoleTriggerPHIDType' => 'applications/roles/phid/PhabricatorRoleTriggerPHIDType.php', + 'PhabricatorRoleTriggerPlaySoundRule' => 'applications/roles/trigger/PhabricatorRoleTriggerPlaySoundRule.php', + 'PhabricatorRoleTriggerQuery' => 'applications/roles/query/PhabricatorRoleTriggerQuery.php', + 'PhabricatorRoleTriggerRemoveRolesRule' => 'applications/roles/trigger/PhabricatorRoleTriggerRemoveProjectsRule.php', + 'PhabricatorRoleTriggerRule' => 'applications/roles/trigger/PhabricatorRoleTriggerRule.php', + 'PhabricatorRoleTriggerRuleRecord' => 'applications/roles/trigger/PhabricatorRoleTriggerRuleRecord.php', + 'PhabricatorRoleTriggerRulesetTransaction' => 'applications/roles/xaction/trigger/PhabricatorRoleTriggerRulesetTransaction.php', + 'PhabricatorRoleTriggerSearchEngine' => 'applications/roles/query/PhabricatorRoleTriggerSearchEngine.php', + 'PhabricatorRoleTriggerTransaction' => 'applications/roles/storage/PhabricatorRoleTriggerTransaction.php', + 'PhabricatorRoleTriggerTransactionQuery' => 'applications/roles/query/PhabricatorRoleTriggerTransactionQuery.php', + 'PhabricatorRoleTriggerTransactionType' => 'applications/roles/xaction/trigger/PhabricatorRoleTriggerTransactionType.php', + 'PhabricatorRoleTriggerUnknownRule' => 'applications/roles/trigger/PhabricatorRoleTriggerUnknownRule.php', + 'PhabricatorRoleTriggerUsage' => 'applications/roles/storage/PhabricatorRoleTriggerUsage.php', + 'PhabricatorRoleTriggerUsageIndexEngineExtension' => 'applications/roles/engineextension/PhabricatorRoleTriggerUsageIndexEngineExtension.php', + 'PhabricatorRoleTriggerViewController' => 'applications/roles/controller/trigger/PhabricatorRoleTriggerViewController.php', + 'PhabricatorRoleTypeTransaction' => 'applications/roles/xaction/PhabricatorRoleTypeTransaction.php', + 'PhabricatorRoleUIEventListener' => 'applications/roles/events/PhabricatorRoleUIEventListener.php', + 'PhabricatorRoleUpdateController' => 'applications/roles/controller/PhabricatorRoleUpdateController.php', + 'PhabricatorRoleUserFunctionDatasource' => 'applications/roles/typeahead/PhabricatorRoleUserFunctionDatasource.php', + 'PhabricatorRoleUserListView' => 'applications/roles/view/PhabricatorRoleUserListView.php', + 'PhabricatorRoleViewController' => 'applications/roles/controller/PhabricatorRoleViewController.php', + 'PhabricatorRoleWatchController' => 'applications/roles/controller/PhabricatorRoleWatchController.php', + 'PhabricatorRoleWatcherListView' => 'applications/roles/view/PhabricatorRoleWatcherListView.php', + 'PhabricatorRoleWorkboardBackgroundColor' => 'applications/roles/constants/PhabricatorRoleWorkboardBackgroundColor.php', + 'PhabricatorRoleWorkboardBackgroundTransaction' => 'applications/roles/xaction/PhabricatorRoleWorkboardBackgroundTransaction.php', + 'PhabricatorRoleWorkboardInterface' => 'applications/roles/interface/PhabricatorRoleWorkboardInterface.php', + 'PhabricatorRoleWorkboardProfileMenuItem' => 'applications/roles/menuitem/PhabricatorRoleWorkboardProfileMenuItem.php', + 'PhabricatorRoleWorkboardTransaction' => 'applications/roles/xaction/PhabricatorRoleWorkboardTransaction.php', + 'PhabricatorRoleWorkboardViewState' => 'applications/roles/state/PhabricatorWorkboardViewState.php', + 'PhabricatorRolesAllPolicyRule' => 'applications/roles/policyrule/PhabricatorRolesAllPolicyRule.php', + 'PhabricatorRolesAncestorsSearchEngineAttachment' => 'applications/roles/engineextension/PhabricatorRolesAncestorsSearchEngineAttachment.php', + 'PhabricatorRolesBasePolicyRule' => 'applications/roles/policyrule/PhabricatorRolesBasePolicyRule.php', + 'PhabricatorRolesCurtainExtension' => 'applications/roles/engineextension/PhabricatorRolesCurtainExtension.php', + 'PhabricatorRolesEditEngineExtension' => 'applications/roles/engineextension/PhabricatorRolesEditEngineExtension.php', + 'PhabricatorRolesFulltextEngineExtension' => 'applications/roles/engineextension/PhabricatorRolesFulltextEngineExtension.php', + 'PhabricatorRolesMailEngineExtension' => 'applications/roles/engineextension/PhabricatorRolesMailEngineExtension.php', + 'PhabricatorRolesMembersSearchEngineAttachment' => 'applications/roles/engineextension/PhabricatorRolesMembersSearchEngineAttachment.php', + 'PhabricatorRolesMembershipIndexEngineExtension' => 'applications/roles/engineextension/PhabricatorRolesMembershipIndexEngineExtension.php', + 'PhabricatorRolesPolicyRule' => 'applications/roles/policyrule/PhabricatorRolesPolicyRule.php', + 'PhabricatorRolesSearchEngineAttachment' => 'applications/roles/engineextension/PhabricatorRolesSearchEngineAttachment.php', + 'PhabricatorRolesSearchEngineExtension' => 'applications/roles/engineextension/PhabricatorRolesSearchEngineExtension.php', + 'PhabricatorRolesWatchersSearchEngineAttachment' => 'applications/roles/engineextension/PhabricatorRolesWatchersSearchEngineAttachment.php', 'PhabricatorS3FileStorageEngine' => 'applications/files/engine/PhabricatorS3FileStorageEngine.php', 'PhabricatorSMSAuthFactor' => 'applications/auth/factor/PhabricatorSMSAuthFactor.php', 'PhabricatorSQLPatchList' => 'infrastructure/storage/patch/PhabricatorSQLPatchList.php', @@ -5868,6 +6099,23 @@ 'RemarkupValue' => 'applications/remarkup/RemarkupValue.php', 'RepositoryConduitAPIMethod' => 'applications/repository/conduit/RepositoryConduitAPIMethod.php', 'RepositoryQueryConduitAPIMethod' => 'applications/repository/conduit/RepositoryQueryConduitAPIMethod.php', + 'RoleAddRolesEmailCommand' => 'applications/roles/command/ProjectAddRolesEmailCommand.php', + 'RoleBoardTaskCard' => 'applications/roles/view/ProjectBoardTaskCard.php', + 'RoleCanLockRolesCapability' => 'applications/roles/capability/RoleCanLockRolesCapability.php', + 'RoleColumnSearchConduitAPIMethod' => 'applications/roles/conduit/RoleColumnSearchConduitAPIMethod.php', + 'RoleConduitAPIMethod' => 'applications/roles/conduit/RoleConduitAPIMethod.php', + 'RoleCreateConduitAPIMethod' => 'applications/roles/conduit/RoleCreateConduitAPIMethod.php', + 'RoleCreateRolesCapability' => 'applications/roles/capability/RoleCreateRolesCapability.php', + 'RoleDatasourceEngineExtension' => 'applications/roles/engineextension/RoleDatasourceEngineExtension.php', + 'RoleDefaultEditCapability' => 'applications/roles/capability/RoleDefaultEditCapability.php', + 'RoleDefaultJoinCapability' => 'applications/roles/capability/RoleDefaultJoinCapability.php', + 'RoleDefaultViewCapability' => 'applications/roles/capability/RoleDefaultViewCapability.php', + 'RoleEditConduitAPIMethod' => 'applications/roles/conduit/RoleEditConduitAPIMethod.php', + 'RoleQueryConduitAPIMethod' => 'applications/roles/conduit/RoleQueryConduitAPIMethod.php', + 'RoleRemarkupRule' => 'applications/roles/remarkup/RoleRemarkupRule.php', + 'RoleRemarkupRuleTestCase' => 'applications/roles/remarkup/__tests__/RoleRemarkupRuleTestCase.php', + 'RoleReplyHandler' => 'applications/roles/mail/RoleReplyHandler.php', + 'RoleSearchConduitAPIMethod' => 'applications/roles/conduit/RoleSearchConduitAPIMethod.php', 'ShellLogView' => 'applications/harbormaster/view/ShellLogView.php', 'SlowvoteConduitAPIMethod' => 'applications/slowvote/conduit/SlowvoteConduitAPIMethod.php', 'SlowvoteEmbedView' => 'applications/slowvote/view/SlowvoteEmbedView.php', @@ -5879,6 +6127,24 @@ 'SlowvoteSearchConduitAPIMethod' => 'applications/slowvote/conduit/SlowvoteSearchConduitAPIMethod.php', 'SubscriptionListDialogBuilder' => 'applications/subscriptions/view/SubscriptionListDialogBuilder.php', 'SubscriptionListStringBuilder' => 'applications/subscriptions/view/SubscriptionListStringBuilder.php', + 'TimeTracker' => 'applications/timetracker/components/TimeTracker.php', + 'TimeTrackerApplication' => 'applications/timetracker/application/TimeTrackerApplication.php', + 'TimeTrackerDAO' => 'applications/timetracker/storage/TimeTrackerDAO.php', + 'TimeTrackerDayDetailsRequestHandler' => 'applications/timetracker/requesthandlers/TimeTrackerDayDetailsRequestHandler.php', + 'TimeTrackerDayHistoryDetailsBox' => 'applications/timetracker/components/TimeTrackerDayHistoryDetailsBox.php', + 'TimeTrackerMainPanel' => 'applications/timetracker/components/TimeTrackerMainPanel.php', + 'TimeTrackerMainPanelRequestHandler' => 'applications/timetracker/requesthandlers/TimeTrackerMainPanelRequestHandler.php', + 'TimeTrackerMonthSummaryBox' => 'applications/timetracker/components/TimeTrackerMonthSummaryBox.php', + 'TimeTrackerPanelType' => 'applications/timetracker/components/TimeTrackerPanelType.php', + 'TimeTrackerProjectDateSummaryBox' => 'applications/timetracker/components/TimeTrackerProjectDateSummaryBox.php', + 'TimeTrackerProjectMonthSummaryBox' => 'applications/timetracker/components/TimeTrackerProjectMonthSummaryBox.php', + 'TimeTrackerRenderController' => 'applications/timetracker/controller/TimeTrackerRenderController.php', + 'TimeTrackerRequestHandler' => 'applications/timetracker/requesthandlers/TimeTrackerRequestHandler.php', + 'TimeTrackerStorageManager' => 'applications/timetracker/application/TimeTrackerStorageManager.php', + 'TimeTrackerSummaryPanel' => 'applications/timetracker/components/TimeTrackerSummaryPanel.php', + 'TimeTrackerSummaryPanelRequestHandler' => 'applications/timetracker/requesthandlers/TimeTrackerSummaryPanelRequestHandler.php', + 'TimeTrackerTimeUtils' => 'applications/timetracker/application/TimeTrackerTimeUtils.php', + 'TimeTrackerTrackedTime' => 'applications/timetracker/storage/TimeTrackerTrackedTime.php', 'TokenConduitAPIMethod' => 'applications/tokens/conduit/TokenConduitAPIMethod.php', 'TokenGiveConduitAPIMethod' => 'applications/tokens/conduit/TokenGiveConduitAPIMethod.php', 'TokenGivenConduitAPIMethod' => 'applications/tokens/conduit/TokenGivenConduitAPIMethod.php', @@ -5892,6 +6158,22 @@ 'UserQueryConduitAPIMethod' => 'applications/people/conduit/UserQueryConduitAPIMethod.php', 'UserSearchConduitAPIMethod' => 'applications/people/conduit/UserSearchConduitAPIMethod.php', 'UserWhoAmIConduitAPIMethod' => 'applications/people/conduit/UserWhoAmIConduitAPIMethod.php', + 'Vacation' => 'applications/vacation/components/Vacation.php', + 'VacationApplication' => 'applications/vacation/application/VacationApplication.php', + 'VacationDAO' => 'applications/vacation/storage/VacationDAO.php', + 'VacationDayDetailsRequestHandler' => 'applications/vacation/requesthandlers/VacationDayDetailsRequestHandler.php', + 'VacationDayHistoryDetailsBox' => 'applications/vacation/components/VacationDayHistoryDetailsBox.php', + 'VacationMainPanel' => 'applications/vacation/components/VacationMainPanel.php', + 'VacationMainPanelRequestHandler' => 'applications/vacation/requesthandlers/VacationMainPanelRequestHandler.php', + 'VacationMonthSummaryBox' => 'applications/vacation/components/VacationMonthSummaryBox.php', + 'VacationPanelType' => 'applications/vacation/components/VacationPanelType.php', + 'VacationRenderController' => 'applications/vacation/controller/VacationRenderController.php', + 'VacationRequestHandler' => 'applications/vacation/requesthandlers/VacationRequestHandler.php', + 'VacationStorageManager' => 'applications/vacation/application/VacationStorageManager.php', + 'VacationSummaryPanel' => 'applications/vacation/components/VacationSummaryPanel.php', + 'VacationSummaryPanelRequestHandler' => 'applications/vacation/requesthandlers/VacationSummaryPanelRequestHandler.php', + 'VacationTimeUtils' => 'applications/vacation/application/VacationTimeUtils.php', + 'VacationVacationDay' => 'applications/vacation/storage/VacationVacationDay.php', ), 'function' => array( 'celerity_generate_unique_node_id' => 'applications/celerity/api.php', @@ -7763,6 +8045,7 @@ 'HeraldEmptyFieldValue' => 'HeraldFieldValue', 'HeraldEngine' => 'Phobject', 'HeraldExactProjectsField' => 'HeraldField', + 'HeraldExactRolesField' => 'HeraldField', 'HeraldField' => 'Phobject', 'HeraldFieldGroup' => 'HeraldGroup', 'HeraldFieldTestCase' => 'PhutilTestCase', @@ -7791,6 +8074,7 @@ 'HeraldRelatedFieldGroup' => 'HeraldFieldGroup', 'HeraldRemarkupFieldValue' => 'HeraldFieldValue', 'HeraldRemarkupRule' => 'PhabricatorObjectRemarkupRule', + 'HeraldRolesField' => 'PhabricatorRoleTagsField', 'HeraldRule' => array( 'HeraldDAO', 'PhabricatorApplicationTransactionInterface', @@ -7967,6 +8251,16 @@ 'MacroEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod', 'MacroEmojiExample' => 'PhabricatorUIExample', 'MacroQueryConduitAPIMethod' => 'MacroConduitAPIMethod', + 'ManagementPanel' => 'Phobject', + 'ManagementPanelApplication' => 'PhabricatorApplication', + 'ManagementPanelDayDetailsRequestHandler' => 'ManagementPanelRequestHandler', + 'ManagementPanelEditUserDate' => 'ManagementPanel', + 'ManagementPanelMainPanel' => 'ManagementPanel', + 'ManagementPanelMainPanelRequestHandler' => 'ManagementPanelRequestHandler', + 'ManagementPanelRenderController' => 'PhabricatorController', + 'ManagementPanelSummaryPanel' => 'ManagementPanel', + 'ManagementPanelSummaryPanelRequestHandler' => 'ManagementPanelRequestHandler', + 'ManagementPanelUserDateRequestHandler' => 'ManagementPanelRequestHandler', 'ManiphestAssignEmailCommand' => 'ManiphestEmailCommand', 'ManiphestAssigneeDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'ManiphestBulkEditCapability' => 'PhabricatorPolicyCapability', @@ -8051,7 +8345,9 @@ 'ManiphestTaskClosedStatusDatasource' => 'PhabricatorTypeaheadDatasource', 'ManiphestTaskCoverImageTransaction' => 'ManiphestTaskTransactionType', 'ManiphestTaskDependedOnByTaskEdgeType' => 'PhabricatorEdgeType', + 'ManiphestTaskBlockedEdgeType' => 'PhabricatorEdgeType', 'ManiphestTaskDependsOnTaskEdgeType' => 'PhabricatorEdgeType', + 'ManiphestTaskBlockerEdgeType' => 'PhabricatorEdgeType', 'ManiphestTaskDescriptionHeraldField' => 'ManiphestTaskHeraldField', 'ManiphestTaskDescriptionTransaction' => 'ManiphestTaskTransactionType', 'ManiphestTaskDetailController' => 'ManiphestController', @@ -8068,9 +8364,11 @@ 'ManiphestTaskHasMockEdgeType' => 'PhabricatorEdgeType', 'ManiphestTaskHasMockRelationship' => 'ManiphestTaskRelationship', 'ManiphestTaskHasParentRelationship' => 'ManiphestTaskRelationship', + 'ManiphestTaskHasBlockedRelationship' => 'ManiphestTaskRelationship', 'ManiphestTaskHasRevisionEdgeType' => 'PhabricatorEdgeType', 'ManiphestTaskHasRevisionRelationship' => 'ManiphestTaskRelationship', 'ManiphestTaskHasSubtaskRelationship' => 'ManiphestTaskRelationship', + 'ManiphestTaskHasBlockerRelationship' => 'ManiphestTaskRelationship', 'ManiphestTaskHeraldField' => 'HeraldField', 'ManiphestTaskHeraldFieldGroup' => 'HeraldFieldGroup', 'ManiphestTaskIsDuplicateOfTaskEdgeType' => 'PhabricatorEdgeType', @@ -8087,6 +8385,7 @@ 'ManiphestTaskPHIDResolver' => 'PhabricatorPHIDResolver', 'ManiphestTaskPHIDType' => 'PhabricatorPHIDType', 'ManiphestTaskParentTransaction' => 'ManiphestTaskTransactionType', + 'ManiphestTaskBlockedTransaction' => 'ManiphestTaskTransactionType', 'ManiphestTaskPoints' => 'Phobject', 'ManiphestTaskPointsTransaction' => 'ManiphestTaskTransactionType', 'ManiphestTaskPolicyCodex' => 'PhabricatorPolicyCodex', @@ -8109,6 +8408,7 @@ 'ManiphestTaskStatusTransaction' => 'ManiphestTaskTransactionType', 'ManiphestTaskSubpriorityTransaction' => 'ManiphestTaskTransactionType', 'ManiphestTaskSubtaskController' => 'ManiphestController', + 'ManiphestTaskBlockerController' => 'ManiphestController', 'ManiphestTaskSubtypeDatasource' => 'PhabricatorTypeaheadDatasource', 'ManiphestTaskTitleHeraldField' => 'ManiphestTaskHeraldField', 'ManiphestTaskTitleTransaction' => 'ManiphestTaskTransactionType', @@ -11420,6 +11720,244 @@ 'PhabricatorRobotsPlatformController' => 'PhabricatorRobotsController', 'PhabricatorRobotsResourceController' => 'PhabricatorRobotsController', 'PhabricatorRobotsShortController' => 'PhabricatorRobotsController', + 'PhabricatorRole' => array( + 'PhabricatorRoleDAO', + 'PhabricatorApplicationTransactionInterface', + 'PhabricatorFlaggableInterface', + 'PhabricatorPolicyInterface', + 'PhabricatorExtendedPolicyInterface', + 'PhabricatorCustomFieldInterface', + 'PhabricatorDestructibleInterface', + 'PhabricatorFulltextInterface', + 'PhabricatorFerretInterface', + 'PhabricatorConduitResultInterface', + 'PhabricatorRoleColumnProxyInterface', + 'PhabricatorSpacesInterface', + 'PhabricatorEditEngineSubtypeInterface', + 'PhabricatorRoleWorkboardInterface', + ), + 'PhabricatorRoleActivityChartEngine' => 'PhabricatorChartEngine', + 'PhabricatorRoleAddHeraldAction' => 'PhabricatorRoleHeraldAction', + 'PhabricatorRoleApplication' => 'PhabricatorApplication', + 'PhabricatorRoleArchiveController' => 'PhabricatorRoleController', + 'PhabricatorRoleBoardBackgroundController' => 'PhabricatorRoleBoardController', + 'PhabricatorRoleBoardColumnsSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment', + 'PhabricatorRoleBoardController' => 'PhabricatorRoleController', + 'PhabricatorRoleBoardDefaultController' => 'PhabricatorRoleBoardController', + 'PhabricatorRoleBoardDisableController' => 'PhabricatorRoleBoardController', + 'PhabricatorRoleBoardFilterController' => 'PhabricatorRoleBoardController', + 'PhabricatorRoleBoardImportController' => 'PhabricatorRoleBoardController', + 'PhabricatorRoleBoardLayoutEngine' => 'Phobject', + 'PhabricatorRoleBoardManageController' => 'PhabricatorRoleBoardController', + 'PhabricatorRoleBoardReloadController' => 'PhabricatorRoleBoardController', + 'PhabricatorRoleBoardRenderingEngine' => 'Phobject', + 'PhabricatorRoleBoardReorderController' => 'PhabricatorRoleBoardController', + 'PhabricatorRoleBoardResponseEngine' => 'Phobject', + 'PhabricatorRoleBoardViewController' => 'PhabricatorRoleBoardController', + 'PhabricatorRoleBurndownChartEngine' => 'PhabricatorChartEngine', + 'PhabricatorRoleCardView' => 'AphrontTagView', + 'PhabricatorRoleColorTransaction' => 'PhabricatorRoleTransactionType', + 'PhabricatorRoleColorsConfigType' => 'PhabricatorJSONConfigType', + 'PhabricatorRoleColumn' => array( + 'PhabricatorRoleDAO', + 'PhabricatorApplicationTransactionInterface', + 'PhabricatorPolicyInterface', + 'PhabricatorDestructibleInterface', + 'PhabricatorExtendedPolicyInterface', + 'PhabricatorConduitResultInterface', + ), + 'PhabricatorRoleColumnAuthorOrder' => 'PhabricatorRoleColumnOrder', + 'PhabricatorRoleColumnBulkEditController' => 'PhabricatorRoleBoardController', + 'PhabricatorRoleColumnBulkMoveController' => 'PhabricatorRoleBoardController', + 'PhabricatorRoleColumnCreatedOrder' => 'PhabricatorRoleColumnOrder', + 'PhabricatorRoleColumnDetailController' => 'PhabricatorRoleBoardController', + 'PhabricatorRoleColumnEditController' => 'PhabricatorRoleBoardController', + 'PhabricatorRoleColumnHeader' => 'Phobject', + 'PhabricatorRoleColumnHideController' => 'PhabricatorRoleBoardController', + 'PhabricatorRoleColumnLimitTransaction' => 'PhabricatorRoleColumnTransactionType', + 'PhabricatorRoleColumnNameTransaction' => 'PhabricatorRoleColumnTransactionType', + 'PhabricatorRoleColumnNaturalOrder' => 'PhabricatorRoleColumnOrder', + 'PhabricatorRoleColumnOrder' => 'Phobject', + 'PhabricatorRoleColumnOwnerOrder' => 'PhabricatorRoleColumnOrder', + 'PhabricatorRoleColumnPHIDType' => 'PhabricatorPHIDType', + 'PhabricatorRoleColumnPointsOrder' => 'PhabricatorRoleColumnOrder', + 'PhabricatorRoleColumnPosition' => array( + 'PhabricatorRoleDAO', + 'PhabricatorPolicyInterface', + ), + 'PhabricatorRoleColumnPositionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PhabricatorRoleColumnPriorityOrder' => 'PhabricatorRoleColumnOrder', + 'PhabricatorRoleColumnQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PhabricatorRoleColumnRemoveTriggerController' => 'PhabricatorRoleBoardController', + 'PhabricatorRoleColumnSearchEngine' => 'PhabricatorApplicationSearchEngine', + 'PhabricatorRoleColumnStatusOrder' => 'PhabricatorRoleColumnOrder', + 'PhabricatorRoleColumnStatusTransaction' => 'PhabricatorRoleColumnTransactionType', + 'PhabricatorRoleColumnTitleOrder' => 'PhabricatorRoleColumnOrder', + 'PhabricatorRoleColumnTransaction' => 'PhabricatorModularTransaction', + 'PhabricatorRoleColumnTransactionEditor' => 'PhabricatorApplicationTransactionEditor', + 'PhabricatorRoleColumnTransactionQuery' => 'PhabricatorApplicationTransactionQuery', + 'PhabricatorRoleColumnTransactionType' => 'PhabricatorModularTransactionType', + 'PhabricatorRoleColumnTriggerTransaction' => 'PhabricatorRoleColumnTransactionType', + 'PhabricatorRoleColumnViewQueryController' => 'PhabricatorRoleBoardController', + 'PhabricatorRoleConfigOptions' => 'PhabricatorApplicationConfigOptions', + 'PhabricatorRoleConfiguredCustomField' => array( + 'PhabricatorRoleStandardCustomField', + 'PhabricatorStandardCustomFieldInterface', + ), + 'PhabricatorRoleController' => 'PhabricatorController', + 'PhabricatorRoleCoreTestCase' => 'PhabricatorTestCase', + 'PhabricatorRoleCoverController' => 'PhabricatorRoleController', + 'PhabricatorRoleCustomField' => 'PhabricatorCustomField', + 'PhabricatorRoleCustomFieldNumericIndex' => 'PhabricatorCustomFieldNumericIndexStorage', + 'PhabricatorRoleCustomFieldStorage' => 'PhabricatorCustomFieldStorage', + 'PhabricatorRoleCustomFieldStringIndex' => 'PhabricatorCustomFieldStringIndexStorage', + 'PhabricatorRoleDAO' => 'PhabricatorLiskDAO', + 'PhabricatorRoleDatasource' => 'PhabricatorTypeaheadDatasource', + 'PhabricatorRoleDescriptionField' => 'PhabricatorRoleStandardCustomField', + 'PhabricatorRoleDetailsProfileMenuItem' => 'PhabricatorProfileMenuItem', + 'PhabricatorRoleDropEffect' => 'Phobject', + 'PhabricatorRoleEditController' => 'PhabricatorRoleController', + 'PhabricatorRoleEditEngine' => 'PhabricatorEditEngine', + 'PhabricatorRoleEditPictureController' => 'PhabricatorRoleController', + 'PhabricatorRoleFerretEngine' => 'PhabricatorFerretEngine', + 'PhabricatorRoleFilterTransaction' => 'PhabricatorRoleTransactionType', + 'PhabricatorRoleFulltextEngine' => 'PhabricatorFulltextEngine', + 'PhabricatorRoleHeraldAction' => 'HeraldAction', + 'PhabricatorRoleHeraldAdapter' => 'HeraldAdapter', + 'PhabricatorRoleHeraldFieldGroup' => 'HeraldFieldGroup', + 'PhabricatorRoleHovercardEngineExtension' => 'PhabricatorHovercardEngineExtension', + 'PhabricatorRoleIconSet' => 'PhabricatorIconSet', + 'PhabricatorRoleIconTransaction' => 'PhabricatorRoleTransactionType', + 'PhabricatorRoleIconsConfigType' => 'PhabricatorJSONConfigType', + 'PhabricatorRoleImageTransaction' => 'PhabricatorRoleTransactionType', + 'PhabricatorRoleListController' => 'PhabricatorRoleController', + 'PhabricatorRoleListView' => 'AphrontView', + 'PhabricatorRoleLockController' => 'PhabricatorRoleController', + 'PhabricatorRoleLockTransaction' => 'PhabricatorRoleTransactionType', + 'PhabricatorRoleLogicalAncestorDatasource' => 'PhabricatorTypeaheadCompositeDatasource', + 'PhabricatorRoleLogicalDatasource' => 'PhabricatorTypeaheadCompositeDatasource', + 'PhabricatorRoleLogicalOnlyDatasource' => 'PhabricatorTypeaheadDatasource', + 'PhabricatorRoleLogicalOrNotDatasource' => 'PhabricatorTypeaheadCompositeDatasource', + 'PhabricatorRoleLogicalUserDatasource' => 'PhabricatorTypeaheadCompositeDatasource', + 'PhabricatorRoleLogicalViewerDatasource' => 'PhabricatorTypeaheadDatasource', + 'PhabricatorRoleManageController' => 'PhabricatorRoleController', + 'PhabricatorRoleManageProfileMenuItem' => 'PhabricatorProfileMenuItem', + 'PhabricatorRoleMaterializedMemberEdgeType' => 'PhabricatorEdgeType', + 'PhabricatorRoleMemberListView' => 'PhabricatorRoleUserListView', + 'PhabricatorRoleMemberOfRoleEdgeType' => 'PhabricatorEdgeType', + 'PhabricatorRoleMembersAddController' => 'PhabricatorRoleController', + 'PhabricatorRoleMembersDatasource' => 'PhabricatorTypeaheadCompositeDatasource', + 'PhabricatorRoleMembersPolicyRule' => 'PhabricatorPolicyRule', + 'PhabricatorRoleMembersProfileMenuItem' => 'PhabricatorProfileMenuItem', + 'PhabricatorRoleMembersRemoveController' => 'PhabricatorRoleController', + 'PhabricatorRoleMembersViewController' => 'PhabricatorRoleController', + 'PhabricatorRoleMenuItemController' => 'PhabricatorRoleController', + 'PhabricatorRoleMilestoneTransaction' => 'PhabricatorRoleTypeTransaction', + 'PhabricatorRoleMoveController' => 'PhabricatorRoleController', + 'PhabricatorRoleNameContextFreeGrammar' => 'PhutilContextFreeGrammar', + 'PhabricatorRoleNameTransaction' => 'PhabricatorRoleTransactionType', + 'PhabricatorRoleNoRolesDatasource' => 'PhabricatorTypeaheadDatasource', + 'PhabricatorRoleObjectHasRoleEdgeType' => 'PhabricatorEdgeType', + 'PhabricatorRoleOrUserDatasource' => 'PhabricatorTypeaheadCompositeDatasource', + 'PhabricatorRoleOrUserFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource', + 'PhabricatorRoleParentTransaction' => 'PhabricatorRoleTypeTransaction', + 'PhabricatorRolePictureProfileMenuItem' => 'PhabricatorProfileMenuItem', + 'PhabricatorRolePointsProfileMenuItem' => 'PhabricatorProfileMenuItem', + 'PhabricatorRoleProfileController' => 'PhabricatorRoleController', + 'PhabricatorRoleProfileMenuEngine' => 'PhabricatorProfileMenuEngine', + 'PhabricatorRoleQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PhabricatorRoleRemoveHeraldAction' => 'PhabricatorRoleHeraldAction', + 'PhabricatorRoleReportsController' => 'PhabricatorRoleController', + 'PhabricatorRoleReportsProfileMenuItem' => 'PhabricatorProfileMenuItem', + 'PhabricatorRoleRoleHasMemberEdgeType' => 'PhabricatorEdgeType', + 'PhabricatorRoleRoleHasObjectEdgeType' => 'PhabricatorEdgeType', + 'PhabricatorRoleRolePHIDType' => 'PhabricatorPHIDType', + 'PhabricatorRoleSchemaSpec' => 'PhabricatorConfigSchemaSpec', + 'PhabricatorRoleSearchEngine' => 'PhabricatorApplicationSearchEngine', + 'PhabricatorRoleSearchField' => 'PhabricatorSearchTokenizerField', + 'PhabricatorRoleSilenceController' => 'PhabricatorRoleController', + 'PhabricatorRoleSilencedEdgeType' => 'PhabricatorEdgeType', + 'PhabricatorRoleSlug' => 'PhabricatorRoleDAO', + 'PhabricatorRoleSlugsTransaction' => 'PhabricatorRoleTransactionType', + 'PhabricatorRoleSortTransaction' => 'PhabricatorRoleTransactionType', + 'PhabricatorRoleStandardCustomField' => array( + 'PhabricatorRoleCustomField', + 'PhabricatorStandardCustomFieldInterface', + ), + 'PhabricatorRoleStatus' => 'Phobject', + 'PhabricatorRoleStatusTransaction' => 'PhabricatorRoleTransactionType', + 'PhabricatorRoleSubroleWarningController' => 'PhabricatorRoleController', + 'PhabricatorRoleSubrolesController' => 'PhabricatorRoleController', + 'PhabricatorRoleSubrolesProfileMenuItem' => 'PhabricatorProfileMenuItem', + 'PhabricatorRoleSubtypeDatasource' => 'PhabricatorTypeaheadDatasource', + 'PhabricatorRoleSubtypesConfigType' => 'PhabricatorJSONConfigType', + 'PhabricatorRoleTagsAddedField' => 'PhabricatorRoleTagsField', + 'PhabricatorRoleTagsField' => 'HeraldField', + 'PhabricatorRoleTagsRemovedField' => 'PhabricatorRoleTagsField', + 'PhabricatorRoleTestDataGenerator' => 'PhabricatorTestDataGenerator', + 'PhabricatorRoleTransaction' => 'PhabricatorModularTransaction', + 'PhabricatorRoleTransactionEditor' => 'PhabricatorApplicationTransactionEditor', + 'PhabricatorRoleTransactionQuery' => 'PhabricatorApplicationTransactionQuery', + 'PhabricatorRoleTransactionType' => 'PhabricatorModularTransactionType', + 'PhabricatorRoleTrigger' => array( + 'PhabricatorRoleDAO', + 'PhabricatorApplicationTransactionInterface', + 'PhabricatorPolicyInterface', + 'PhabricatorIndexableInterface', + 'PhabricatorDestructibleInterface', + ), + 'PhabricatorRoleTriggerAddRolesRule' => 'PhabricatorRoleTriggerRule', + 'PhabricatorRoleTriggerController' => 'PhabricatorRoleController', + 'PhabricatorRoleTriggerCorruptionException' => 'Exception', + 'PhabricatorRoleTriggerEditController' => 'PhabricatorRoleTriggerController', + 'PhabricatorRoleTriggerEditor' => 'PhabricatorApplicationTransactionEditor', + 'PhabricatorRoleTriggerInvalidRule' => 'PhabricatorRoleTriggerRule', + 'PhabricatorRoleTriggerListController' => 'PhabricatorRoleTriggerController', + 'PhabricatorRoleTriggerManiphestOwnerRule' => 'PhabricatorRoleTriggerRule', + 'PhabricatorRoleTriggerManiphestPriorityRule' => 'PhabricatorRoleTriggerRule', + 'PhabricatorRoleTriggerManiphestStatusRule' => 'PhabricatorRoleTriggerRule', + 'PhabricatorRoleTriggerNameTransaction' => 'PhabricatorRoleTriggerTransactionType', + 'PhabricatorRoleTriggerPHIDType' => 'PhabricatorPHIDType', + 'PhabricatorRoleTriggerPlaySoundRule' => 'PhabricatorRoleTriggerRule', + 'PhabricatorRoleTriggerQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PhabricatorRoleTriggerRemoveRolesRule' => 'PhabricatorRoleTriggerRule', + 'PhabricatorRoleTriggerRule' => 'Phobject', + 'PhabricatorRoleTriggerRuleRecord' => 'Phobject', + 'PhabricatorRoleTriggerRulesetTransaction' => 'PhabricatorRoleTriggerTransactionType', + 'PhabricatorRoleTriggerSearchEngine' => 'PhabricatorApplicationSearchEngine', + 'PhabricatorRoleTriggerTransaction' => 'PhabricatorModularTransaction', + 'PhabricatorRoleTriggerTransactionQuery' => 'PhabricatorApplicationTransactionQuery', + 'PhabricatorRoleTriggerTransactionType' => 'PhabricatorModularTransactionType', + 'PhabricatorRoleTriggerUnknownRule' => 'PhabricatorRoleTriggerRule', + 'PhabricatorRoleTriggerUsage' => 'PhabricatorRoleDAO', + 'PhabricatorRoleTriggerUsageIndexEngineExtension' => 'PhabricatorIndexEngineExtension', + 'PhabricatorRoleTriggerViewController' => 'PhabricatorRoleTriggerController', + 'PhabricatorRoleTypeTransaction' => 'PhabricatorRoleTransactionType', + 'PhabricatorRoleUIEventListener' => 'PhabricatorEventListener', + 'PhabricatorRoleUpdateController' => 'PhabricatorRoleController', + 'PhabricatorRoleUserFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource', + 'PhabricatorRoleUserListView' => 'AphrontView', + 'PhabricatorRoleViewController' => 'PhabricatorRoleController', + 'PhabricatorRoleWatchController' => 'PhabricatorRoleController', + 'PhabricatorRoleWatcherListView' => 'PhabricatorRoleUserListView', + 'PhabricatorRoleWorkboardBackgroundColor' => 'Phobject', + 'PhabricatorRoleWorkboardBackgroundTransaction' => 'PhabricatorRoleTransactionType', + 'PhabricatorRoleWorkboardProfileMenuItem' => 'PhabricatorProfileMenuItem', + 'PhabricatorRoleWorkboardTransaction' => 'PhabricatorRoleTransactionType', + 'PhabricatorRoleWorkboardViewState' => 'Phobject', + 'PhabricatorRolesAllPolicyRule' => 'PhabricatorRolesBasePolicyRule', + 'PhabricatorRolesAncestorsSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment', + 'PhabricatorRolesBasePolicyRule' => 'PhabricatorPolicyRule', + 'PhabricatorRolesCurtainExtension' => 'PHUICurtainExtension', + 'PhabricatorRolesEditEngineExtension' => 'PhabricatorEditEngineExtension', + 'PhabricatorRolesFulltextEngineExtension' => 'PhabricatorFulltextEngineExtension', + 'PhabricatorRolesMailEngineExtension' => 'PhabricatorMailEngineExtension', + 'PhabricatorRolesMembersSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment', + 'PhabricatorRolesMembershipIndexEngineExtension' => 'PhabricatorIndexEngineExtension', + 'PhabricatorRolesPolicyRule' => 'PhabricatorRolesBasePolicyRule', + 'PhabricatorRolesSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment', + 'PhabricatorRolesSearchEngineExtension' => 'PhabricatorSearchEngineExtension', + 'PhabricatorRolesWatchersSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment', 'PhabricatorS3FileStorageEngine' => 'PhabricatorFileStorageEngine', 'PhabricatorSMSAuthFactor' => 'PhabricatorAuthFactor', 'PhabricatorSQLPatchList' => 'Phobject', @@ -12772,6 +13310,23 @@ 'RemarkupValue' => 'Phobject', 'RepositoryConduitAPIMethod' => 'ConduitAPIMethod', 'RepositoryQueryConduitAPIMethod' => 'RepositoryConduitAPIMethod', + 'RoleAddRolesEmailCommand' => 'MetaMTAEmailTransactionCommand', + 'RoleBoardTaskCard' => 'Phobject', + 'RoleCanLockRolesCapability' => 'PhabricatorPolicyCapability', + 'RoleColumnSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod', + 'RoleConduitAPIMethod' => 'ConduitAPIMethod', + 'RoleCreateConduitAPIMethod' => 'RoleConduitAPIMethod', + 'RoleCreateRolesCapability' => 'PhabricatorPolicyCapability', + 'RoleDatasourceEngineExtension' => 'PhabricatorDatasourceEngineExtension', + 'RoleDefaultEditCapability' => 'PhabricatorPolicyCapability', + 'RoleDefaultJoinCapability' => 'PhabricatorPolicyCapability', + 'RoleDefaultViewCapability' => 'PhabricatorPolicyCapability', + 'RoleEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod', + 'RoleQueryConduitAPIMethod' => 'RoleConduitAPIMethod', + 'RoleRemarkupRule' => 'PhabricatorObjectRemarkupRule', + 'RoleRemarkupRuleTestCase' => 'PhabricatorTestCase', + 'RoleReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler', + 'RoleSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod', 'ShellLogView' => 'AphrontView', 'SlowvoteConduitAPIMethod' => 'ConduitAPIMethod', 'SlowvoteEmbedView' => 'AphrontView', @@ -12783,6 +13338,16 @@ 'SlowvoteSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod', 'SubscriptionListDialogBuilder' => 'Phobject', 'SubscriptionListStringBuilder' => 'Phobject', + 'TimeTracker' => 'Phobject', + 'TimeTrackerApplication' => 'PhabricatorApplication', + 'TimeTrackerDAO' => 'PhabricatorLiskDAO', + 'TimeTrackerDayDetailsRequestHandler' => 'TimeTrackerRequestHandler', + 'TimeTrackerMainPanel' => 'TimeTracker', + 'TimeTrackerMainPanelRequestHandler' => 'TimeTrackerRequestHandler', + 'TimeTrackerRenderController' => 'PhabricatorController', + 'TimeTrackerSummaryPanel' => 'TimeTracker', + 'TimeTrackerSummaryPanelRequestHandler' => 'TimeTrackerRequestHandler', + 'TimeTrackerTrackedTime' => 'TimeTrackerDAO', 'TokenConduitAPIMethod' => 'ConduitAPIMethod', 'TokenGiveConduitAPIMethod' => 'TokenConduitAPIMethod', 'TokenGivenConduitAPIMethod' => 'TokenConduitAPIMethod', @@ -12796,5 +13361,15 @@ 'UserQueryConduitAPIMethod' => 'UserConduitAPIMethod', 'UserSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod', 'UserWhoAmIConduitAPIMethod' => 'UserConduitAPIMethod', + 'Vacation' => 'Phobject', + 'VacationApplication' => 'PhabricatorApplication', + 'VacationDAO' => 'PhabricatorLiskDAO', + 'VacationDayDetailsRequestHandler' => 'VacationRequestHandler', + 'VacationMainPanel' => 'Vacation', + 'VacationMainPanelRequestHandler' => 'VacationRequestHandler', + 'VacationRenderController' => 'PhabricatorController', + 'VacationSummaryPanel' => 'Vacation', + 'VacationSummaryPanelRequestHandler' => 'VacationRequestHandler', + 'VacationVacationDay' => 'VacationDAO', ), )); diff --git a/src/aphront/response/AphrontResponse.php b/src/aphront/response/AphrontResponse.php index 5dae168c73..1f556fce7d 100644 --- a/src/aphront/response/AphrontResponse.php +++ b/src/aphront/response/AphrontResponse.php @@ -162,7 +162,7 @@ private function newContentSecurityPolicyHeader() { // On a small number of pages, including the Stripe workflow and the // ReCAPTCHA challenge, we embed external Javascript directly. - $csp[] = $this->newContentSecurityPolicy('script-src', $default); + $csp[] = $this->newContentSecurityPolicy('script-src', "{$default} 'unsafe-inline'"); // We need to specify that we can connect to ourself in order for AJAX // requests to work. diff --git a/src/applications/auth/controller/PhabricatorAuthNeedsApprovalController.php b/src/applications/auth/controller/PhabricatorAuthNeedsApprovalController.php index 1121dfb14a..c2c58e933a 100644 --- a/src/applications/auth/controller/PhabricatorAuthNeedsApprovalController.php +++ b/src/applications/auth/controller/PhabricatorAuthNeedsApprovalController.php @@ -51,7 +51,7 @@ private function newCustomWaitForApprovalInstructions() { $viewer, PhabricatorAuthWaitForApprovalMessageType::MESSAGEKEY); - if (!strlen($text)) { + if ($text === null || $text === '') { return null; } diff --git a/src/applications/auth/engine/PhabricatorAuthPasswordEngine.php b/src/applications/auth/engine/PhabricatorAuthPasswordEngine.php index a1fec4a6d2..38002116d2 100644 --- a/src/applications/auth/engine/PhabricatorAuthPasswordEngine.php +++ b/src/applications/auth/engine/PhabricatorAuthPasswordEngine.php @@ -156,7 +156,7 @@ public function checkNewPassword( $terms_map = array(); foreach ($blocklist as $term) { $terms_map[$term] = $term; - foreach (preg_split('/[ @.]/', $term) as $subterm) { + foreach (preg_split('/[ @.]/', phutil_string_cast($term)) as $subterm) { $terms_map[$subterm] = $term; } } diff --git a/src/applications/files/controller/PhabricatorFileDropUploadController.php b/src/applications/files/controller/PhabricatorFileDropUploadController.php index 21714994ff..41d8942ea9 100644 --- a/src/applications/files/controller/PhabricatorFileDropUploadController.php +++ b/src/applications/files/controller/PhabricatorFileDropUploadController.php @@ -20,12 +20,12 @@ public function handleRequest(AphrontRequest $request) { $name = $request->getStr('name'); $file_phid = $request->getStr('phid'); - // If there's no explicit view policy, make it very restrictive by default. + // If there's no explicit view policy, make it viewable by default. // This is the correct policy for files dropped onto objects during // creation, comment and edit flows. $view_policy = $request->getStr('viewPolicy'); if (!$view_policy) { - $view_policy = $viewer->getPHID(); + $view_policy = PhabricatorPolicies::getMostOpenPolicy(); } $is_chunks = $request->getBool('querychunks'); diff --git a/src/applications/files/storage/PhabricatorFile.php b/src/applications/files/storage/PhabricatorFile.php index 32029886f0..82aefb130f 100644 --- a/src/applications/files/storage/PhabricatorFile.php +++ b/src/applications/files/storage/PhabricatorFile.php @@ -328,7 +328,7 @@ private static function buildFromFileData($data, array $params = array()) { if (isset($params['storageEngines'])) { $engines = $params['storageEngines']; } else { - $size = strlen($data); + $size = strlen(phutil_string_cast($data)); $engines = PhabricatorFileStorageEngine::loadStorageEngines($size); if (!$engines) { @@ -406,7 +406,7 @@ private static function buildFromFileData($data, array $params = array()) { $exceptions); } - $file->setByteSize(strlen($data)); + $file->setByteSize(strlen(phutil_string_cast($data))); $hash = self::hashFileContent($data); $file->setContentHash($hash); @@ -786,7 +786,7 @@ public static function hashFileContent($data) { // it just means we'll store extra data when users upload duplicate files // instead of being able to deduplicate it. - $hash = hash('sha256', $data, $raw_output = false); + $hash = hash('sha256', phutil_string_cast($data), $raw_output = false); if ($hash === false) { return null; } diff --git a/src/applications/managementpanel/application/ManagementPanelApplication.php b/src/applications/managementpanel/application/ManagementPanelApplication.php new file mode 100644 index 0000000000..6ea0129c16 --- /dev/null +++ b/src/applications/managementpanel/application/ManagementPanelApplication.php @@ -0,0 +1,54 @@ + array( + '' => 'ManagementPanelRenderController', + 'view/(?P[^/]+)/' => 'ManagementPanelRenderController', + ), + ); + } + +} diff --git a/src/applications/managementpanel/application/ManagementPanelStorageManager.php b/src/applications/managementpanel/application/ManagementPanelStorageManager.php new file mode 100644 index 0000000000..a2a742e4f1 --- /dev/null +++ b/src/applications/managementpanel/application/ManagementPanelStorageManager.php @@ -0,0 +1,44 @@ +establishConnection('w'); + + $userID = queryfx_one( + $connection, + 'SELECT id FROM user WHERE phid = %s', $userPhid); + + return $userID['id']; + + } + + public static function getUserName($userID){ + $dao = new PhabricatorUser(); + $connection = id($dao)->establishConnection('w'); + + $userName = queryfx_one( + $connection, + 'SELECT realName FROM user WHERE id = %s', $userID); + + return $userName['realName']; + + } + + public static function getUserDateCreated($userID){ + $dao = new PhabricatorUser(); + $connection = id($dao)->establishConnection('w'); + + $userName = queryfx_one( + $connection, + 'SELECT dateCreated FROM user WHERE id = %s', $userID); + + return $userName['dateCreated']; + } + + + private function getNumMinutesToTrack($numHours, $numMinutes) { + return $numHours * 60 + $numMinutes; + } +} diff --git a/src/applications/managementpanel/application/ManagementPanelTimeUtils.php b/src/applications/managementpanel/application/ManagementPanelTimeUtils.php new file mode 100644 index 0000000000..3b14d565c5 --- /dev/null +++ b/src/applications/managementpanel/application/ManagementPanelTimeUtils.php @@ -0,0 +1,60 @@ +translateDate('Y', $date); + } + + public static function getCurrentMonth() { + $date = new DateTime('@' . time()); + return PhutilTranslator::getInstance()->translateDate('m', $date); + } + + public static function getCurrentDay() { + $date = new DateTime('@' . time()); + return PhutilTranslator::getInstance()->translateDate('d', $date); + } + + public static function getTodayTimestamp() { + $currentDay = ManagementPanelTimeUtils::getCurrentDay(); + $currentMonth = ManagementPanelTimeUtils::getCurrentMonth(); + $currentYear = ManagementPanelTimeUtils::getCurrentYear(); + + return ManagementPanelTimeUtils::getTimestamp($currentDay, $currentMonth, $currentYear); + } + + public static function getTimestamp($day, $month, $year) { + return strtotime($year . '-' . $month . '-' . $day); + } + + public static function numMinutesToString($numMinutes) { + $isNegative = $numMinutes < 0; + + $numMinutes = abs($numMinutes); + if ($numMinutes < 60) { + $str = ''; + if ($isNegative) { + $str .= '-'; + } + $str .= $numMinutes . ' minutes'; + return $str; + } + + $numHours = floor($numMinutes / 60); + $remainingMinutes = $numMinutes % 60; + + $str = ''; + if ($isNegative) { + $str .= '-'; + } + $str .= $numHours . ' hours'; + if ($remainingMinutes > 0) { + $str .= ' ' . $remainingMinutes . ' minutes'; + } + return $str; + } +} \ No newline at end of file diff --git a/src/applications/managementpanel/components/ManagementPanel.php b/src/applications/managementpanel/components/ManagementPanel.php new file mode 100644 index 0000000000..faf4352c19 --- /dev/null +++ b/src/applications/managementpanel/components/ManagementPanel.php @@ -0,0 +1,33 @@ +request = $request; + return $this; + } + + public function getRequest() { + return $this->request; + } + + public function setRequestHandler($handler) { + $this->requestHandler = $handler; + } + + public function getRequestHandler() { + return $this->requestHandler; + } + + abstract public function getName(); + abstract public function getDescription(); + abstract public function renderPage($user); + abstract protected function getPanelType(); + + public function getCategory() { + return pht('General'); + } +} diff --git a/src/applications/managementpanel/components/ManagementPanelDayHistoryDetailsBox.php b/src/applications/managementpanel/components/ManagementPanelDayHistoryDetailsBox.php new file mode 100644 index 0000000000..e26c2b7b9d --- /dev/null +++ b/src/applications/managementpanel/components/ManagementPanelDayHistoryDetailsBox.php @@ -0,0 +1,55 @@ +userId = $userId; + $this->timestamp = $timestamp; + } + + public function getDetailsBox() { + + $dao = new TimeTrackerTrackedTime(); + $connection = id($dao)->establishConnection('w'); + + $result = queryfx_all( + $connection, + 'SELECT numMinutes, realDateWhenTracked FROM timetracker_trackedtime WHERE userID = %d + AND dateWhenTrackedFor = %d ORDER BY realDateWhenTracked ASC', $this->userId, $this->timestamp); + + $totalTrackedTime = 0; + $data = array(); + foreach ($result as $row) { + $totalTrackedTime += $row['numMinutes']; + $row['realDateWhenTracked'] = date("Y-m-d H:i:s", $row['realDateWhenTracked']); + $row['numMinutes'] = $this->numMinutesToString($row['numMinutes']); + $data[] = $row; + } + + $totalTrackedTime = $this->numMinutesToString($totalTrackedTime); + + $list = new PHUIStatusListView(); + + foreach ($data as $row) { + $iconColor = ($row['numMinutes'] > 0) ? 'green' : 'red'; + $list->addItem(id(new PHUIStatusItemView()) + ->setIcon(PHUIStatusItemView::ICON_CLOCK, $iconColor, pht('')) + ->setTarget(pht($row['numMinutes'])) + ->setNote(pht('tracked ' . $row['realDateWhenTracked']))); + } + + $date = date('d-m-Y', $this->timestamp); + + $box = id(new PHUIObjectBoxView()) + ->setHeaderText('Tracked time history for ' . $date . ' (' . $totalTrackedTime . ' total)') + ->appendChild($list); + + return $box; + } + + private function numMinutesToString($numMinutes) { + return ManagementPanelTimeUtils::numMinutesToString($numMinutes); + } +} \ No newline at end of file diff --git a/src/applications/managementpanel/components/ManagementPanelEditUserDate.php b/src/applications/managementpanel/components/ManagementPanelEditUserDate.php new file mode 100644 index 0000000000..fc78da3a11 --- /dev/null +++ b/src/applications/managementpanel/components/ManagementPanelEditUserDate.php @@ -0,0 +1,167 @@ + + First, select a user. Then enter/select a new registration date.'); + } + + public function renderPage($user) + { + + $timeTrackingFormBox = $this->getFormFindUser($user); + + $view = new PHUIInfoView(); + $view->setSeverity(PHUIInfoView::SEVERITY_NOTICE); + $view->setTitle('Examples how to edit user registration date'); + $view->setErrors( + array( + pht('Select needed user -> click FIND'), + pht('Select new regestration date (01/01/2000)'), + pht('Click SAVE') + ) + ); + $arr = array($view, $timeTrackingFormBox); + + $responseBox = $this->getResponseBox(); + $userSetter = new ManagementPanelUser($this->userID); + if ($responseBox != null) { + } + + if ($userSetter != null && $userSetter->getID() != -1) { + + $arr[] = $this->getUserName($this->userID); + $arr[] = $this->getUserDateRegistrationBox($this->userID); + $arr[] = $this->getFormEditUserDateRegistration($user); + } + return $arr; + } + + private function getFormFindUser($user) + { + require_celerity_resource('jquery-js'); + require_celerity_resource('jquery-ui-js'); + require_celerity_resource('timetracker-js'); + require_celerity_resource('jquery-ui-css'); + + $submit = id(new AphrontFormSubmitControl()); + $submit->setValue(pht('FIND')) + ->setControlStyle('width: 13%; margin-left: 40%;'); + + $form = id(new AphrontFormView()) + ->setUser($user) + ->addHiddenInput('add', '1') + ->appendControl( + id(new AphrontFormTokenizerControl()) + ->setName('add_person') + ->setUser($user) + ->setLimit(1) + ->setDatasource(new PhabricatorPeopleDatasource()) + ) + ->addHiddenInput('isSet', '1') + ->addHiddenInput('panelType', $this->getPanelType()) + ->appendChild($submit); + + $box = id(new PHUIObjectBoxView()) + ->setForm($form) + ->setHeaderText('Select a user') + ->appendChild(id(new PHUIBoxView())); + + return $box; + } + + + private function getFormEditUserDateRegistration($user) + { + require_celerity_resource('jquery-js'); + require_celerity_resource('jquery-ui-js'); + require_celerity_resource('timetracker-js'); + require_celerity_resource('jquery-ui-css'); + + $submit = id(new AphrontFormSubmitControl()); + $submit->setValue(pht('SAVE')) + ->setControlStyle('width: 13%; margin-left: 3%;'); + + $fromDateInput = (id(new AphrontFormTextControl()) + ->setLabel(pht('Date:')) + ->setDisableAutocomplete(true) + ->setName('newDate') + ->setValue('') + ->setID('datepicker') + ->setControlStyle('width: 13%; margin-left: 3%;')); + + $form = id(new AphrontFormView()) + ->setUser($this->getRequest()->getUser()) + ->appendChild($fromDateInput) + ->addHiddenInput('isSent', '1') + ->addHiddenInput('userID', $this->userID) + ->addHiddenInput('panelType', $this->getPanelType()) + ->appendChild($submit); + + $box = id(new PHUIObjectBoxView()) + ->setForm($form) + ->setHeaderText('Edit vacation hours') + ->appendChild(id(new PHUIBoxView())); + + return $box; + } + + private function getUserName($user) + { + + $summaryBox = new ManagementPanelUserNameBox($user); + return $summaryBox->getBox(); + } + + private function getUserDateRegistrationBox($user) + { + + $summaryBox = new ManagementPanelUserDateRegistrationBox($user); + return $summaryBox->getBox(); + } + + private function getCurrentDate() + { + $currentDay = ManagementPanelTimeUtils::getCurrentDay(); + $currentMonth = ManagementPanelTimeUtils::getCurrentMonth(); + $currentYear = ManagementPanelTimeUtils::getCurrentYear(); + return $currentMonth . '/' . $currentDay . '/' . $currentYear; + } + + private function getResponseBox() + { + $handler = $this->getRequestHandler(); + if ($handler != null) { + + $info = new ManagementPanelUserRequestInfo($handler->getRequest()); + $this->userID = $info->getUserID(); + + return $handler->getResponsePanel(); + } + return null; + } + + private function getPrefilledDate() + { + $handler = $this->getRequestHandler(); + if ($handler != null) { + return $handler->getRequest()->getStr('date'); + } + return $this->getCurrentDate(); + } +} diff --git a/src/applications/managementpanel/components/ManagementPanelMainPanel.php b/src/applications/managementpanel/components/ManagementPanelMainPanel.php new file mode 100644 index 0000000000..65830e4796 --- /dev/null +++ b/src/applications/managementpanel/components/ManagementPanelMainPanel.php @@ -0,0 +1,174 @@ + + First select a user. Enter the required number of hours. If you need to add time to the user, enter the value with a minus.
+ If you need decrease, then enter the required amount.'); + } + + public function renderPage($user) { + + $timeTrackingFormBox = $this->getFormFindUser($user); + + $view = new PHUIInfoView(); + $view->setSeverity(PHUIInfoView::SEVERITY_NOTICE); + $view->setTitle('Examples how to track your time'); + $view->setErrors( + array( + phutil_safe_html('8h - if you need decrease'), + phutil_safe_html('-8h - if you need add time'), + )); + $arr = array($view, $timeTrackingFormBox); + + $responseBox = $this->getResponseBox(); + $userSetter = new ManagementPanelUser($this->userID); + if ($responseBox != null) { + $arr[] = $responseBox; + + $date = $this->getRequest()->getStr('date'); + $pieces = explode('/', $date); + + $day = $pieces[1]; + $month = $pieces[0]; + $year = $pieces[2]; + + $timestamp = VacationTimeUtils::getTimestamp($day, $month, $year); + + $dayHistoryDetails = new VacationDayHistoryDetailsBox($this->userID, $timestamp); + $box = $dayHistoryDetails->getDetailsBox(); + $arr[] = $box; + + } + + if($userSetter != null && $userSetter->getID() != -1){ + + $arr[] = $this->getUserName($this->userID); // TODO added needed userID + $arr[] = $this->getSummaryHoursBox($userSetter); + $arr[] = $this->getFormEditVacationTime($user); + } + return $arr; + } + + private function getFormFindUser($user) { + require_celerity_resource('jquery-js'); + require_celerity_resource('jquery-ui-js'); + require_celerity_resource('timetracker-js'); + require_celerity_resource('jquery-ui-css'); + + $submit = id(new AphrontFormSubmitControl()); + $submit->setValue(pht('FIND')) + ->setControlStyle('width: 13%; margin-left: 40%;'); + + $form = id(new AphrontFormView()) + ->setUser($user) + ->addHiddenInput('add', '1') + ->appendControl( + id(new AphrontFormTokenizerControl()) + ->setName('add_person') + ->setUser($user) + ->setLimit(1) + ->setDatasource(new PhabricatorPeopleDatasource())) + ->addHiddenInput('isSet', '1') + ->addHiddenInput('panelType', $this->getPanelType()) + ->appendChild($submit); + + $box = id(new PHUIObjectBoxView()) + ->setForm($form) + ->setHeaderText('Select a user') + ->appendChild(id(new PHUIBoxView())); + + return $box; + } + + + private function getFormEditVacationTime($user) { + require_celerity_resource('jquery-js'); + require_celerity_resource('jquery-ui-js'); + require_celerity_resource('timetracker-js'); + require_celerity_resource('jquery-ui-css'); + + $submit = id(new AphrontFormSubmitControl()); + $submit->setValue(pht('SAVE')) + ->setControlStyle('width: 13%; margin-left: 3%;'); + + $dateFormComponent = (id(new AphrontFormTextControl()) + ->setLabel(pht('Date:')) + ->setDisableAutocomplete(true) + ->setName('date') + ->setValue($this->getPrefilledDate()) + ->setControlStyle('width: 13%; margin-left: 3%;') + ->setID('datepicker')); + + $form = id(new AphrontFormView()) + ->setUser($this->getRequest()->getUser()) + ->appendChild(id(new AphrontFormTextControl()) + ->setDisableAutocomplete(true) + ->setControlStyle('width: 13%; margin-left: 3%;') + ->setLabel(pht('Time:')) + ->setName('timeTracked') + ->setValue('')) + ->addHiddenInput('isSent', '1') + ->addHiddenInput('userID', $this->userID) + ->addHiddenInput('panelType', $this->getPanelType()) + ->appendChild($dateFormComponent) + ->appendChild($submit); + + $box = id(new PHUIObjectBoxView()) + ->setForm($form) + ->setHeaderText('Edit vacation hours') + ->appendChild(id(new PHUIBoxView())); + + return $box; +} + + private function getUserName($user) { + + $summaryBox = new ManagementPanelUserNameBox($user); + return $summaryBox->getBox(); +} + + private function getSummaryHoursBox($user) { + + $summaryBox = new VacationMonthSummaryBox($user); + return $summaryBox->getBox(); + } + + private function getCurrentDate() { + $currentDay = ManagementPanelTimeUtils::getCurrentDay(); + $currentMonth = ManagementPanelTimeUtils::getCurrentMonth(); + $currentYear = ManagementPanelTimeUtils::getCurrentYear(); + return $currentMonth . '/' . $currentDay . '/' . $currentYear; + } + + private function getResponseBox() { + $handler = $this->getRequestHandler(); + if ($handler != null) { + + $info = new ManagementPanelUserRequestInfo($handler->getRequest()); + $this->userID = $info->getUserID(); + + return $handler->getResponsePanel(); + } + return null; + } + + private function getPrefilledDate() { + $handler = $this->getRequestHandler(); + if ($handler != null) { + return $handler->getRequest()->getStr('date'); + } + return $this->getCurrentDate(); + } +} diff --git a/src/applications/managementpanel/components/ManagementPanelPanelType.php b/src/applications/managementpanel/components/ManagementPanelPanelType.php new file mode 100644 index 0000000000..d015b2c127 --- /dev/null +++ b/src/applications/managementpanel/components/ManagementPanelPanelType.php @@ -0,0 +1,8 @@ + + First, select a user. Then pick the date range and click GO to check your working hours between those dates.
'); + } + + protected function getPanelType() + { + return ManagementPanelPanelType::SUMMARY; + } + + public function renderPage($user) + { + $formFindUser = $this->getFormFindUser($user); + + $elements = array(); + + $elements[] = $formFindUser; + + $this->getResponseBox(); + + $userSetter = new ManagementPanelUser($this->userID); + + if ($userSetter != null && $userSetter->getID() != -1) { + $elements[] = $this->getUserName($this->userID); + + $dateRangeFormBox = $this->getDateRangeFormBox($userSetter->getID()); + $elements[] = $dateRangeFormBox; + } + $requestHandler = $this->getRequestHandler(); + if ($requestHandler != null) { + + $chartBox = null; + + $isSent = $requestHandler->getRequest()->getStr('isSent') == '1'; + + if ($isSent != null && $isSent) { + $chartBox = $this->getChartBox(); + } + + if ($chartBox != null) { + $elements[] = $chartBox; + } + } + + return $elements; + } + + private function getDateRangeFormBox($user) + { + require_celerity_resource('jquery-js'); + require_celerity_resource('jquery-ui-js'); + require_celerity_resource('timetracker-js'); + require_celerity_resource('jquery-ui-css'); + + $submit = id(new AphrontFormSubmitControl()); + $submit->setValue(pht('GO')) + ->setControlStyle('width: 13%; margin-left: 3%;'); + + $fromDateInput = (id(new AphrontFormTextControl()) + ->setLabel(pht('From: ')) + ->setDisableAutocomplete(false) + ->setName('from') + ->setValue('') + ->setID('datepicker') + ->setControlStyle('width: 13%; margin-left: 3%;')); + + $toDateInput = (id(new AphrontFormTextControl()) + ->setLabel(pht('To: ')) + ->setDisableAutocomplete(false) + ->setName('to') + ->setValue('') + ->setID('datepicker2') + ->setControlStyle('width: 13%; margin-left: 3%;')); + + $form = id(new AphrontFormView()) + ->setUser($this->getRequest()->getUser()) + ->appendChild($fromDateInput) + ->appendChild($toDateInput) + ->addHiddenInput('isSent', '1') + ->addHiddenInput('userID', $this->userID) + ->addHiddenInput('panelType', $this->getPanelType()) + ->appendChild($submit); + + $box = id(new PHUIObjectBoxView()) + ->setForm($form) + ->setHeaderText('Pick days range') + ->appendChild(id(new PHUIBoxView())); + + return $box; + } + + + private function getFormFindUser($user) + { + require_celerity_resource('jquery-js'); + require_celerity_resource('jquery-ui-js'); + require_celerity_resource('timetracker-js'); + require_celerity_resource('jquery-ui-css'); + + $submit = id(new AphrontFormSubmitControl()); + $submit->setValue(pht('FIND')) + ->setControlStyle('width: 13%; margin-left: 40%;'); + + $form = id(new AphrontFormView()) + ->setUser($user) + ->addHiddenInput('add', '1') + ->appendControl( + id(new AphrontFormTokenizerControl()) + ->setName('add_person') + ->setUser($user) + ->setLimit(1) + ->setDatasource(new PhabricatorPeopleDatasource()) + ) + ->addHiddenInput('isSet', '1') + ->addHiddenInput('panelType', $this->getPanelType()) + ->appendChild($submit); + + $box = id(new PHUIObjectBoxView()) + ->setForm($form) + ->setHeaderText('Select user') + ->appendChild(id(new PHUIBoxView())); + + return $box; + } + + + private function getUserName($user) + { + + $summaryBox = new ManagementPanelUserNameBox($user); + return $summaryBox->getBox(); + } + + private function getChartBox() + { + $requestHandler = $this->getRequestHandler(); + if ($requestHandler == null || !($requestHandler instanceof ManagementPanelSummaryPanelRequestHandler)) { + return null; + } + $chartJsonData = $requestHandler->getChartJsonData(); + + $map = CelerityResourceMap::getNamedInstance('phabricator'); + $chart = CelerityAPI::getStaticResourceResponse()->getURI($map, 'rsrc/js/application/timetracker/chart/Chart.min.js'); + $jquery = CelerityAPI::getStaticResourceResponse()->getURI($map, 'rsrc/js/application/timetracker/chart/jquery.min.js'); + $showGraph = CelerityAPI::getStaticResourceResponse()->getURI($map, 'rsrc/js/application/timetracker/chart/show-graph.js'); + + $widthPercentage = '65%'; + $content = phutil_safe_html(pht('
+
+ + + + ', $widthPercentage, $jquery, $chart, $showGraph, $chartJsonData)); + + $userSelected = new ManagementPanelUser($requestHandler->getSelectedUserID()); + $from = $requestHandler->getDateFrom(); + $to = $requestHandler->getDateTo(); + $projectsInfo = $this->getDetailsTrackedHoursOnProjects($userSelected, $from, $to); + + $box = id(new PHUIObjectBoxView()) + ->setHeader('Title') + ->appendChild($content) + ->appendChild($projectsInfo) + ->appendChild(id(new PHUIBoxView())); + + return $box; + } + + private function getDetailsTrackedHoursOnProjects($user, $fromDateInput, $toDateInput) + { + $projectsInfo = new TimeTrackerProjectDateSummaryBox($user, $fromDateInput, $toDateInput); + return $projectsInfo->getBox(); + } + private function getResponseBox() + { + $handler = $this->getRequestHandler(); + if ($handler != null) { + + $info = new ManagementPanelUserRequestInfo($handler->getRequest()); + $this->userID = $info->getUserID(); + // $this->userID = $handler->getRequest()->getStr('userID'); + } + } + + private function getDayDetailsBox() + { + $requestHandler = $this->getRequestHandler(); + if ($requestHandler == null || !($requestHandler instanceof ManagementPanelDayDetailsRequestHandler)) { + return null; + } + + $box = $requestHandler->getDayDetailsBox(); + return $box; + } +} diff --git a/src/applications/managementpanel/components/ManagementPanelUser.php b/src/applications/managementpanel/components/ManagementPanelUser.php new file mode 100644 index 0000000000..df120d6e29 --- /dev/null +++ b/src/applications/managementpanel/components/ManagementPanelUser.php @@ -0,0 +1,23 @@ +userID = $id; +} + public function getID(){ + return $this->userID; + } + + public function getDateCreated(){ + $dataCreated = ManagementPanelStorageManager::getUserDateCreated($this->userID); + return $dataCreated; + } + + +} diff --git a/src/applications/managementpanel/components/ManagementPanelUserDateRegistrationBox.php b/src/applications/managementpanel/components/ManagementPanelUserDateRegistrationBox.php new file mode 100644 index 0000000000..0f0b874418 --- /dev/null +++ b/src/applications/managementpanel/components/ManagementPanelUserDateRegistrationBox.php @@ -0,0 +1,21 @@ +user = $user; + } + + public function getBox() { + $userDateSelected = ManagementPanelStorageManager::getUserDateCreated($this->user); + $userDateSelected = date('d-m-Y', $userDateSelected); + $view = new PHUIInfoView(); + $view->setSeverity(PHUIInfoView::SEVERITY_NOTICE); + $view->setErrors( + array( + phutil_safe_html('User date registration: ' . $userDateSelected . ''), + )); + return $view; + } +} \ No newline at end of file diff --git a/src/applications/managementpanel/components/ManagementPanelUserNameBox.php b/src/applications/managementpanel/components/ManagementPanelUserNameBox.php new file mode 100644 index 0000000000..fc07d9f379 --- /dev/null +++ b/src/applications/managementpanel/components/ManagementPanelUserNameBox.php @@ -0,0 +1,20 @@ +userID = $userID; + } + + public function getBox() { + $userNameSelected = ManagementPanelStorageManager::getUserName($this->userID); + $view = new PHUIInfoView(); + $view->setSeverity(PHUIInfoView::SEVERITY_NOTICE); + $view->setErrors( + array( + phutil_safe_html('User name: ' . $userNameSelected . ''), + )); + return $view; + } +} \ No newline at end of file diff --git a/src/applications/managementpanel/controller/ManagementPanelRenderController.php b/src/applications/managementpanel/controller/ManagementPanelRenderController.php new file mode 100644 index 0000000000..4b59a30dc6 --- /dev/null +++ b/src/applications/managementpanel/controller/ManagementPanelRenderController.php @@ -0,0 +1,88 @@ +getRequestHandler($request); + if ($requestHandler != null) { + $requestHandler->handleRequest($request); + } + + $classes = id(new PhutilClassMapQuery()) + ->setAncestorClass('ManagementPanel') + ->setSortMethod('getName') + ->execute(); + + $nav = new AphrontSideNavFilterView(); + $nav->setBaseURI(new PhutilURI($this->getApplicationURI('view/'))); + + $groups = mgroup($classes, 'getCategory'); + ksort($groups); + foreach ($groups as $group => $group_classes) { + $nav->addLabel($group); + foreach ($group_classes as $class => $obj) { + $name = $obj->getName(); + $nav->addFilter($class, $name); + } + } + + $id = $request->getURIData('class'); + $selected = $nav->selectFilter($id, head_key($classes)); + + $page = $classes[$selected]; + $page->setRequest($this->getRequest()); + $page->setRequestHandler($requestHandler); + + $result = $page->renderPage($request->getUser()); + if ($result instanceof AphrontResponse) { + // This allows examples to generate dialogs, etc., for demonstration. + return $result; + } + + require_celerity_resource('phabricator-ui-example-css'); + + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb($page->getName()); + + $note = id(new PHUIInfoView()) + ->setTitle(pht('%s', $page->getName())) + ->appendChild($page->getDescription()) + ->setSeverity(PHUIInfoView::SEVERITY_NODATA); + + + + $nav->appendChild( + array( + $crumbs, + $note, + $result, + )); + + return $this->newPage() + ->setTitle($page->getName()) + ->appendChild($nav); + } + + private function getRequestHandler($request) { + $panelType = $request->getStr('panelType'); + + if (strcmp($panelType, ManagementPanelPanelType::MAIN) == 0) { + return new ManagementPanelMainPanelRequestHandler(); + } + if (strcmp($panelType, ManagementPanelPanelType::SECOND) == 0) { + return new ManagementPanelUserDateRequestHandler(); + } + if (strcmp($panelType, ManagementPanelPanelType::SUMMARY) == 0) { + return new ManagementPanelSummaryPanelRequestHandler(); + } + if (strcmp($panelType, ManagementPanelPanelType::DAY_DETAILS) == 0) { + return new ManagementPanelDayDetailsRequestHandler(); + } + return null; + } +} diff --git a/src/applications/managementpanel/requesthandlers/ManagementPanelDayDetailsRequestHandler.php b/src/applications/managementpanel/requesthandlers/ManagementPanelDayDetailsRequestHandler.php new file mode 100644 index 0000000000..30957f1c2a --- /dev/null +++ b/src/applications/managementpanel/requesthandlers/ManagementPanelDayDetailsRequestHandler.php @@ -0,0 +1,39 @@ +request = $request; + + $dayTimestamp = $this->getTimestampFromInput($request->getStr('day')); + $box = new ManagementPanelDayHistoryDetailsBox($this->userID, $dayTimestamp); + $this->dayDetailsBox = $box->getDetailsBox(); + $box = new ManagementPanelUserNameBox($this->userID); + } + + private function getTimestampFromInput($dateInput) { + $dateInput = trim($dateInput); + $pieces = explode('-', $dateInput); + + $day = $pieces[0]; + $month = $pieces[1]; + $year = $pieces[2]; + + return ManagementPanelTimeUtils::getTimestamp($day, $month, $year); + } + + + public function getDayDetailsBox() { + return $this->dayDetailsBox; + } + + public function getRequest() { + return $this->request; + } +} diff --git a/src/applications/managementpanel/requesthandlers/ManagementPanelMainPanelRequestHandler.php b/src/applications/managementpanel/requesthandlers/ManagementPanelMainPanelRequestHandler.php new file mode 100644 index 0000000000..a11d6bd833 --- /dev/null +++ b/src/applications/managementpanel/requesthandlers/ManagementPanelMainPanelRequestHandler.php @@ -0,0 +1,156 @@ +request = $request; + + $isSent = $request->getStr('isSent') == '1'; + + if ($isSent) { + + $this->user = new ManagementPanelUser($request->getStr('userID')); + + $correctRequest = $this->parseTrackTimeRequest($request); + + if (!$correctRequest) { + $this->responsePanel = $this->createResponsePanel(false); + } + else { + $date = $request->getStr('date'); + $date = trim($date); + $pieces = explode('/', $date); + + $day = $pieces[1]; + $month = $pieces[0]; + $year = $pieces[2]; + + + $manager = new VacationStorageManager(); + $manager->trackTime($this->user, $this->numHours, $this->numMinutes, $day, $month, $year); + + $this->responsePanel = $this->createResponsePanel(true); + } + } + } + + + + public function parseTrackTimeRequest($request) { + $timeTracked = $request->getStr('timeTracked'); + + $timeTracked = trim($timeTracked); + $timeTracked = strtolower($timeTracked); + + if (!strpbrk($timeTracked, '0123456789')) { + return false; + } + + $hasMinutes = strpos($timeTracked, 'm') !== false; + $hasHours = strpos($timeTracked, 'h') !== false; + $isNegative = strcmp(substr($timeTracked, 0, 1), '-') == 0; + $isRange = (strpos($timeTracked, '-') !== false) && !$isNegative; + + if (!$hasMinutes && !$hasHours && !$isRange) { + return false; + } + + $date = $request->getStr('date'); + $date = trim($date); + $pieces = explode('/', $date); + + $day = $pieces[1]; + $month = $pieces[0]; + $year = $pieces[2]; + + $date = VacationTimeUtils::getTimestamp($day,$month,$year); + + $correctInput = true; + if ($isRange) { + $correctInput = $this->parseRange($timeTracked); + } + else { + $correctInput = $this->parseSingleTimeInput($timeTracked, $hasMinutes, $hasHours, $isNegative); + } + + return $correctInput; + } + + private function parseSingleTimeInput($timeTracked, $hasMinutes, $hasHours, $isNegative) { + if ($hasMinutes && $hasHours) { + list($this->numHours, $this->numMinutes) = explode('h', $timeTracked); + $this->numMinutes = trim(str_replace('m', '', $this->numMinutes)); + } + else if ($hasMinutes && !$hasHours) { + $pieces = explode('m', $timeTracked); + $this->numMinutes = $pieces[0]; + } + else if (!$hasMinutes && $hasHours) { + $pieces = explode('h', $timeTracked); + $this->numHours = $pieces[0]; + } + + $this->numMinutes = str_replace('-', '', $this->numMinutes); + $this->numHours = str_replace('-', '', $this->numHours); + + if ($isNegative) { + $this->numMinutes *= -1; + $this->numHours *= -1; + } + return true; + } + + private function parseRange($timeTracked) { + $pieces = explode('-', $timeTracked); + $from = trim($pieces[0]); + $till = trim($pieces[1]); + + if ($from > 24 || $from < 0 || $till > 24 || $till < 0 || $from == $till) { + return false; + } + + if ($from > $till) { + $this->numHours = 24 - $from + $till; + } + else { + $this->numHours = $till - $from; + } + return true; + } + + private function createResponsePanel($success) { + $severity = $success ? PHUIInfoView::SEVERITY_SUCCESS : PHUIInfoView::SEVERITY_ERROR; + $responseText = ''; + if ($success) { + $responseText = 'Successfully tracked'; + if ($this->numHours != 0) { + $responseText .= ' ' . $this->numHours . ' hours'; + } + if ($this->numMinutes != 0) { + $responseText .= ' ' . $this->numMinutes . ' minutes'; + } + } + else { + $responseText = 'Incorrect input'; + } + + $view = new PHUIInfoView(); + $view->setSeverity($severity); + $view->setErrors(array(pht($responseText))); + return $view; + } + + public function getResponsePanel() { + return $this->responsePanel; + } + + public function getRequest() { + return $this->request; + } +} diff --git a/src/applications/managementpanel/requesthandlers/ManagementPanelRequestHandler.php b/src/applications/managementpanel/requesthandlers/ManagementPanelRequestHandler.php new file mode 100644 index 0000000000..4d80d5bca6 --- /dev/null +++ b/src/applications/managementpanel/requesthandlers/ManagementPanelRequestHandler.php @@ -0,0 +1,5 @@ +request = $request; + + $isSent = $request->getStr('isSent') == '1'; + + if ($isSent) { + $from = $request->getStr('from'); + $to = $request->getStr('to'); + + if ($to == '') { + $to = $from; + } + + + $fromTimestamp = $this->getTimestampFromInput($from); + $toTimestamp = $this->getTimestampFromInput($to); + + $this->dateTo = $toTimestamp; + $this->dateFrom = $fromTimestamp; + + $userID = $request->getStr('userID'); + $this->userID = $userID; + + $dao = new TimeTrackerTrackedTime(); + $connection = id($dao)->establishConnection('w'); + + $result = queryfx_all( + $connection, + 'SELECT SUM(numMinutes) numMinutes, dateWhenTrackedFor FROM timetracker_trackedtime WHERE userID = %d + AND dateWhenTrackedFor >= %d AND dateWhenTrackedFor <= %d + GROUP BY dateWhenTrackedFor ORDER BY dateWhenTrackedFor ASC', $userID, $fromTimestamp, $toTimestamp); + + $data = array(); + foreach ($result as $row) { + $data[] = $row; + } + + $data = $this->fillEmptyDays($fromTimestamp, $toTimestamp, $data); + $data = $this->sortData($data); + $data = $this->timestampToReadableDate($data); + + $this->chartJsonData = json_encode($data); + } + + } + + + private function timestampToReadableDate($data) { + foreach ($data as &$row) { + $row['dateWhenTrackedFor'] = date('d-m-y', $row['dateWhenTrackedFor']); + } + return $data; + } + + private function sortData($data) { + usort($data, function($a, $b) { + return $a['dateWhenTrackedFor'] < $b['dateWhenTrackedFor'] ? -1 : 1; + }); + return $data; + } + + private function fillEmptyDays($fromTimestamp, $toTimestamp, $data) { + $rangeOfDays = ($toTimestamp - $fromTimestamp) / ManagementPanelTimeUtils::NUM_SECONDS_IN_DAY + 1; + $dateWhenTrackedForColumn = array_column($data, 'dateWhenTrackedFor'); + + for ($i = 0; $i < $rangeOfDays; $i++) { + $currentDayInRangeDate = $fromTimestamp + $i * ManagementPanelTimeUtils::NUM_SECONDS_IN_DAY; + if (array_search($currentDayInRangeDate, $dateWhenTrackedForColumn) === false) { + $data[] = ['numMinutes' => '0', 'dateWhenTrackedFor' => $currentDayInRangeDate]; + } + } + return $data; + } + + public function getDateTo(){ + return $this->dateTo; + } + + public function getDateFrom(){ + return $this->dateFrom; + } + + public function getChartJsonData() { + return $this->chartJsonData; + } + + public function getSelectedUserID(){ + return $this->userID; + } + private function getTimestampFromInput($dateInput) { + $dateInput = trim($dateInput); + $pieces = explode('/', $dateInput); + + $day = $pieces[1]; + $month = $pieces[0]; + $year = $pieces[2]; + + return ManagementPanelTimeUtils::getTimestamp($day, $month, $year); + } + + public function getRequest() { + return $this->request; + } +} diff --git a/src/applications/managementpanel/requesthandlers/ManagementPanelUserDateRequestHandler.php b/src/applications/managementpanel/requesthandlers/ManagementPanelUserDateRequestHandler.php new file mode 100644 index 0000000000..fb87e5bcc7 --- /dev/null +++ b/src/applications/managementpanel/requesthandlers/ManagementPanelUserDateRequestHandler.php @@ -0,0 +1,77 @@ +request = $request; + + $isSent = $request->getStr('isSent') == '1'; + + if ($isSent) { + + $this->user = new ManagementPanelUser($request->getStr('userID')); + $newDate = $request->getStr('newDate'); + + $newDateUserRegistration = $this->getTimestampFromInput($newDate); + + $this->setNewDate($this->user, $newDateUserRegistration); + + $this->responsePanel = $this->createResponsePanel(true, $newDate); + } + } + + private function setNewDate($user, $date) { + $userID = $user->getID(); + + $dao = new PhabricatorUser(); + $connection = id($dao)->establishConnection('w'); + $guard = AphrontWriteGuard::beginScopedUnguardedWrites(); + $dao->openTransaction(); + + queryfx($connection, 'UPDATE user SET dateCreated = %d WHERE id = %d', + $date, $userID); + + $dao->saveTransaction(); + unset($guard); + } + + private function createResponsePanel($success, $date) { + $severity = $success ? PHUIInfoView::SEVERITY_SUCCESS : PHUIInfoView::SEVERITY_ERROR; + $responseText = ''; + if ($success) { + $responseText = 'New date tracked '.$date; + } + else { + $responseText = 'Incorrect input'; + } + + $view = new PHUIInfoView(); + $view->setSeverity($severity); + $view->setErrors(array(pht($responseText))); + return $view; +} + + private function getTimestampFromInput($dateInput) { + $dateInput = trim($dateInput); + $pieces = explode('/', $dateInput); + + $day = $pieces[1]; + $month = $pieces[0]; + $year = $pieces[2]; + + return ManagementPanelTimeUtils::getTimestamp($day, $month, $year); + } + + public function getResponsePanel() { + return $this->responsePanel; + } + + public function getRequest() { + return $this->request; + } +} diff --git a/src/applications/managementpanel/requesthandlers/ManagementPanelUserRequestInfo.php b/src/applications/managementpanel/requesthandlers/ManagementPanelUserRequestInfo.php new file mode 100644 index 0000000000..51fd7dba1e --- /dev/null +++ b/src/applications/managementpanel/requesthandlers/ManagementPanelUserRequestInfo.php @@ -0,0 +1,25 @@ +request = $request; + } + + public function getUserID(){ + $selectUser = $this->request->getRequestData(); + $userIDSelected = -1; + + try{ + $userIDSelected = ManagementPanelStorageManager::getUserID($selectUser['add_person'][0]); + return $userIDSelected; + } + catch(Exception $e){ + + } + + return $userIDSelected; + } +} diff --git a/src/applications/maniphest/application/PhabricatorManiphestApplication.php b/src/applications/maniphest/application/PhabricatorManiphestApplication.php index 8ed20416bb..784974ec81 100644 --- a/src/applications/maniphest/application/PhabricatorManiphestApplication.php +++ b/src/applications/maniphest/application/PhabricatorManiphestApplication.php @@ -53,6 +53,7 @@ public function getRoutes() { $this->getEditRoutePattern('edit/') => 'ManiphestTaskEditController', 'subtask/(?P[1-9]\d*)/' => 'ManiphestTaskSubtaskController', + 'blocker/(?P[1-9]\d*)/' => 'ManiphestTaskBlockerController', ), 'graph/(?P[1-9]\d*)/' => 'ManiphestTaskGraphController', ), diff --git a/src/applications/maniphest/conduit/ManiphestConduitAPIMethod.php b/src/applications/maniphest/conduit/ManiphestConduitAPIMethod.php index 640e30fee5..0b62cfcfdc 100644 --- a/src/applications/maniphest/conduit/ManiphestConduitAPIMethod.php +++ b/src/applications/maniphest/conduit/ManiphestConduitAPIMethod.php @@ -226,7 +226,7 @@ protected function buildTaskInfoDictionaries(array $tasks) { $all_deps = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs($task_phids) - ->withEdgeTypes(array(ManiphestTaskDependsOnTaskEdgeType::EDGECONST)); + ->withEdgeTypes(array(ManiphestTaskDependsOnTaskEdgeType::EDGECONST, ManiphestTaskBlockerEdgeType::EDGECONST)); $all_deps->execute(); $result = array(); @@ -244,7 +244,7 @@ protected function buildTaskInfoDictionaries(array $tasks) { $task_deps = $all_deps->getDestinationPHIDs( array($task->getPHID()), - array(ManiphestTaskDependsOnTaskEdgeType::EDGECONST)); + array(ManiphestTaskDependsOnTaskEdgeType::EDGECONST, ManiphestTaskBlockerEdgeType::EDGECONST)); $result[$task->getPHID()] = array( 'id' => $task->getID(), diff --git a/src/applications/maniphest/controller/ManiphestController.php b/src/applications/maniphest/controller/ManiphestController.php index 970095009b..2632369102 100644 --- a/src/applications/maniphest/controller/ManiphestController.php +++ b/src/applications/maniphest/controller/ManiphestController.php @@ -40,7 +40,9 @@ protected function buildApplicationCrumbs() { final protected function newTaskGraphDropdownMenu( ManiphestTask $task, $has_parents, + $has_blocked_tasks, $has_subtasks, + $has_blockers, $include_standalone) { $viewer = $this->getViewer(); @@ -62,11 +64,23 @@ final protected function newTaskGraphDropdownMenu( ->setName(pht('Search Parent Tasks')) ->setDisabled(!$has_parents) ->setIcon('fa-chevron-circle-up')) + ->addAction( + id(new PhabricatorActionView()) + ->setHref($parents_uri) + ->setName(pht('Search Blocked Tasks')) + ->setDisabled(!$has_blocked_tasks) + ->setIcon('fa-chevron-circle-up')) ->addAction( id(new PhabricatorActionView()) ->setHref($subtasks_uri) ->setName(pht('Search Subtasks')) ->setDisabled(!$has_subtasks) + ->setIcon('fa-chevron-circle-down')) + ->addAction( + id(new PhabricatorActionView()) + ->setHref($subtasks_uri) + ->setName(pht('Search Blockers')) + ->setDisabled(!$has_blockers) ->setIcon('fa-chevron-circle-down')); if ($include_standalone) { diff --git a/src/applications/maniphest/controller/ManiphestReportController.php b/src/applications/maniphest/controller/ManiphestReportController.php index 6fc19ece98..4da1958ba0 100644 --- a/src/applications/maniphest/controller/ManiphestReportController.php +++ b/src/applications/maniphest/controller/ManiphestReportController.php @@ -186,8 +186,8 @@ public function renderBurn() { switch ($row['transactionType']) { case ManiphestTaskStatusTransaction::TRANSACTIONTYPE: // NOTE: Hack to avoid json_decode(). - $oldv = trim($row['oldValue'], '"'); - $newv = trim($row['newValue'], '"'); + $oldv = trim(phutil_string_cast($row['oldValue']), '"'); + $newv = trim(phutil_string_cast($row['newValue']), '"'); break; case ManiphestTaskMergedIntoTransaction::TRANSACTIONTYPE: // NOTE: Merging a task does not generate a "status" transaction. diff --git a/src/applications/maniphest/controller/ManiphestTaskBlockerController.php b/src/applications/maniphest/controller/ManiphestTaskBlockerController.php new file mode 100644 index 0000000000..f5fefe5909 --- /dev/null +++ b/src/applications/maniphest/controller/ManiphestTaskBlockerController.php @@ -0,0 +1,71 @@ +getViewer(); + $id = $request->getURIData('id'); + + $task = id(new ManiphestTaskQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->executeOne(); + if (!$task) { + return new Aphront404Response(); + } + + $cancel_uri = $task->getURI(); + + $edit_engine = id(new ManiphestEditEngine()) + ->setViewer($viewer) + ->setTargetObject($task); + + $subtype_map = $task->newEditEngineSubtypeMap(); + + $subtype_options = $subtype_map->getCreateFormsForSubtype( + $edit_engine, + $task); + + if (!$subtype_options) { + return $this->newDialog() + ->setTitle(pht('No Forms')) + ->appendParagraph( + pht( + 'You do not have access to any forms which can be used to '. + 'create a blocker task.')) + ->addCancelButton($cancel_uri, pht('Close')); + } + + $menu = id(new PHUIObjectItemListView()) + ->setUser($viewer) + ->setBig(true) + ->setFlush(true); + + foreach ($subtype_options as $form_key => $subtype_form) { + $subtype_key = $subtype_form->getSubtype(); + $subtype = $subtype_map->getSubtype($subtype_key); + + $subtask_uri = id(new PhutilURI("/task/edit/form/{$form_key}/")) + ->replaceQueryParam('blocked', $id) + ->replaceQueryParam('template', $id) + ->replaceQueryParam('status', ManiphestTaskStatus::getDefaultStatus()); + $subtask_uri = $this->getApplicationURI($subtask_uri); + + $item = id(new PHUIObjectItemView()) + ->setHeader($subtype_form->getDisplayName()) + ->setHref($subtask_uri) + ->setClickable(true) + ->setImageIcon($subtype->newIconView()) + ->addAttribute($subtype->getName()); + + $menu->addItem($item); + } + + return $this->newDialog() + ->setTitle(pht('Choose Subtype')) + ->appendChild($menu) + ->addCancelButton($cancel_uri); + } + +} diff --git a/src/applications/maniphest/controller/ManiphestTaskDetailController.php b/src/applications/maniphest/controller/ManiphestTaskDetailController.php index 8916ad26cf..9d0494d663 100644 --- a/src/applications/maniphest/controller/ManiphestTaskDetailController.php +++ b/src/applications/maniphest/controller/ManiphestTaskDetailController.php @@ -6,6 +6,98 @@ public function shouldAllowPublic() { return true; } + private function buildGraph(ManiphestTask $task, int $subtask_type) { + switch ($subtask_type) { + case ManiphestTaskDependsOnTaskEdgeType::EDGECONST: + $parent_type = ManiphestTaskDependedOnByTaskEdgeType::EDGECONST; + break; + + case ManiphestTaskBlockerEdgeType::EDGECONST: + $parent_type = ManiphestTaskBlockedEdgeType::EDGECONST; + break; + + default: + return null; + } + + $viewer = $this->getViewer(); + + $graph_limit = 200; + $overflow_message = null; + $task_graph = id(new ManiphestTaskGraph()) + ->setViewer($viewer) + ->setSubtaskType($subtask_type) + ->setSeedPHID($task->getPHID()) + ->setLimit($graph_limit) + ->loadGraph(); + + $allow_render_graph = false; + $graph_table = null; + $has_parents = null; + $has_subtasks = null; + + if (!$task_graph->isEmpty()) { + $parent_map = $task_graph->getEdges($parent_type); + $subtask_map = $task_graph->getEdges($subtask_type); + $parent_list = idx($parent_map, $task->getPHID(), array()); + $subtask_list = idx($subtask_map, $task->getPHID(), array()); + $has_parents = (bool)$parent_list; + $has_subtasks = (bool)$subtask_list; + + // First, get a count of direct parent tasks and subtasks. If there + // are too many of these, we just don't draw anything. You can use + // the search button to browse tasks with the search UI instead. + $direct_count = count($parent_list) + count($subtask_list); + + if ($direct_count > $graph_limit) { + $overflow_message = pht( + 'This task is directly connected to more than %s other tasks. '. + 'Use %s to browse parents or subtasks, or %s to show more of the '. + 'graph.', + new PhutilNumber($graph_limit), + phutil_tag('strong', array(), pht('Search...')), + phutil_tag('strong', array(), pht('View Standalone Graph'))); + } else { + // If there aren't too many direct tasks, but there are too many total + // tasks, we'll only render directly connected tasks. + if ($task_graph->isOverLimit()) { + $task_graph->setRenderOnlyAdjacentNodes(true); + + $overflow_message = pht( + 'This task is connected to more than %s other tasks. '. + 'Only direct parents and subtasks are shown here. Use '. + '%s to show more of the graph.', + new PhutilNumber($graph_limit), + phutil_tag('strong', array(), pht('View Standalone Graph'))); + } + + $graph_table = $task_graph->newGraphTable(); + } + + if ($overflow_message) { + $overflow_view = $this->newTaskGraphOverflowView( + $task, + $overflow_message, + true); + + $graph_table = array( + $overflow_view, + $graph_table, + ); + } + + $allow_render_graph = true; + } + + $result = array( + 'table' => $graph_table, + 'has_parents' => $has_parents, + 'has_subtasks' => $has_subtasks, + 'allow_render_graph' => $allow_render_graph + ); + return $result; + } + public function handleRequest(AphrontRequest $request) { $viewer = $this->getViewer(); $id = $request->getURIData('id'); @@ -79,80 +171,49 @@ public function handleRequest(AphrontRequest $request) { $related_tabs = array(); $graph_menu = null; + $new_tab = null; - $graph_limit = 200; - $overflow_message = null; - $task_graph = id(new ManiphestTaskGraph()) - ->setViewer($viewer) - ->setSeedPHID($task->getPHID()) - ->setLimit($graph_limit) - ->loadGraph(); - if (!$task_graph->isEmpty()) { - $parent_type = ManiphestTaskDependedOnByTaskEdgeType::EDGECONST; - $subtask_type = ManiphestTaskDependsOnTaskEdgeType::EDGECONST; - $parent_map = $task_graph->getEdges($parent_type); - $subtask_map = $task_graph->getEdges($subtask_type); - $parent_list = idx($parent_map, $task->getPHID(), array()); - $subtask_list = idx($subtask_map, $task->getPHID(), array()); - $has_parents = (bool)$parent_list; - $has_subtasks = (bool)$subtask_list; + $subtask_graph_map = $this->buildGraph($task, ManiphestTaskDependsOnTaskEdgeType::EDGECONST); + $subtask_graph_table = $subtask_graph_map['table']; + $has_parents = $subtask_graph_map['has_parents']; + $has_subtasks = $subtask_graph_map['has_subtasks']; - // First, get a count of direct parent tasks and subtasks. If there - // are too many of these, we just don't draw anything. You can use - // the search button to browse tasks with the search UI instead. - $direct_count = count($parent_list) + count($subtask_list); - - if ($direct_count > $graph_limit) { - $overflow_message = pht( - 'This task is directly connected to more than %s other tasks. '. - 'Use %s to browse parents or subtasks, or %s to show more of the '. - 'graph.', - new PhutilNumber($graph_limit), - phutil_tag('strong', array(), pht('Search...')), - phutil_tag('strong', array(), pht('View Standalone Graph'))); - - $graph_table = null; - } else { - // If there aren't too many direct tasks, but there are too many total - // tasks, we'll only render directly connected tasks. - if ($task_graph->isOverLimit()) { - $task_graph->setRenderOnlyAdjacentNodes(true); - - $overflow_message = pht( - 'This task is connected to more than %s other tasks. '. - 'Only direct parents and subtasks are shown here. Use '. - '%s to show more of the graph.', - new PhutilNumber($graph_limit), - phutil_tag('strong', array(), pht('View Standalone Graph'))); - } - - $graph_table = $task_graph->newGraphTable(); - } - - if ($overflow_message) { - $overflow_view = $this->newTaskGraphOverflowView( - $task, - $overflow_message, - true); - - $graph_table = array( - $overflow_view, - $graph_table, - ); - } + $blocker_graph_map = $this->buildGraph($task, ManiphestTaskBlockerEdgeType::EDGECONST); + $blocker_graph_table = $blocker_graph_map['table']; + $has_blocked_tasks = $blocker_graph_map['has_parents']; + $has_blockers = $blocker_graph_map['has_subtasks']; + if ($subtask_graph_map['allow_render_graph'] || $blocker_graph_map['allow_render_graph']) { $graph_menu = $this->newTaskGraphDropdownMenu( $task, $has_parents, + $has_blocked_tasks, $has_subtasks, + $has_blockers, true); - $related_tabs[] = id(new PHUITabView()) + $new_tab = id(new PHUITabView()) ->setName(pht('Task Graph')) - ->setKey('graph') - ->appendChild($graph_table); + ->setKey('graph'); } + if ($subtask_graph_map['allow_render_graph']) { + $subtask_header = id(new PHUIHeaderView()) + ->setHeader(pht('Subtasks')); + $new_tab + ->appendChild($subtask_header) + ->appendChild($subtask_graph_table); + } + + if ($blocker_graph_map['allow_render_graph']) { + $blocker_header = id(new PHUIHeaderView()) + ->setHeader(pht('Blockers')); + $new_tab + ->appendChild($blocker_header) + ->appendChild($blocker_graph_table); + } + + $related_tabs[] = $new_tab; $related_tabs[] = $this->newMocksTab($task, $query); $related_tabs[] = $this->newMentionsTab($task, $query); $related_tabs[] = $this->newDuplicatesTab($task, $query); @@ -249,36 +310,30 @@ private function buildHeaderView(ManiphestTask $task) { return $view; } - - private function buildCurtain( + private function buildSubtaskItem( ManiphestTask $task, - PhabricatorEditEngine $edit_engine) { - $viewer = $this->getViewer(); + PhabricatorEditEngine $edit_engine, + int $subtask_type) { + + switch ($subtask_type) { + case ManiphestTaskDependsOnTaskEdgeType::EDGECONST: + $subtask_query_param = 'parent'; + $view_name = 'Create Subtask'; + $subtask_controller = 'subtask'; + break; + + case ManiphestTaskBlockerEdgeType::EDGECONST: + $subtask_query_param = 'blocked'; + $view_name = 'Create Blocker'; + $subtask_controller = 'blocker'; + break; + + default: + return null; + } $id = $task->getID(); - $phid = $task->getPHID(); - - $can_edit = PhabricatorPolicyFilter::hasCapability( - $viewer, - $task, - PhabricatorPolicyCapability::CAN_EDIT); - - $can_interact = PhabricatorPolicyFilter::canInteract($viewer, $task); - - // We expect a policy dialog if you can't edit the task, and expect a - // lock override dialog if you can't interact with it. - $workflow_edit = (!$can_edit || !$can_interact); - - $curtain = $this->newCurtainView($task); - - $curtain->addAction( - id(new PhabricatorActionView()) - ->setName(pht('Edit Task')) - ->setIcon('fa-pencil') - ->setHref($this->getApplicationURI("/task/edit/{$id}/")) - ->setDisabled(!$can_edit) - ->setWorkflow($workflow_edit)); - + $subtype_map = $task->newEditEngineSubtypeMap(); $subtask_options = $subtype_map->getCreateFormsForSubtype( $edit_engine, @@ -291,14 +346,14 @@ private function buildCurtain( // The "subtask" controller handles the first case (no forms) and the // third case (more than one form). In the case of one form, we link // directly to the form. - $subtask_uri = "/task/subtask/{$id}/"; + $subtask_uri = "/task/{$subtask_controller}/{$id}/"; $subtask_workflow = true; if (count($subtask_options) == 1) { $subtask_form = head($subtask_options); $form_key = $subtask_form->getIdentifier(); $subtask_uri = id(new PhutilURI("/task/edit/form/{$form_key}/")) - ->replaceQueryParam('parent', $id) + ->replaceQueryParam($subtask_query_param, $id) ->replaceQueryParam('template', $id) ->replaceQueryParam('status', ManiphestTaskStatus::getDefaultStatus()); $subtask_workflow = false; @@ -306,21 +361,57 @@ private function buildCurtain( $subtask_uri = $this->getApplicationURI($subtask_uri); - $subtask_item = id(new PhabricatorActionView()) - ->setName(pht('Create Subtask')) + return id(new PhabricatorActionView()) + ->setName(pht($view_name)) ->setHref($subtask_uri) ->setIcon('fa-level-down') ->setDisabled(!$subtask_options) ->setWorkflow($subtask_workflow); + } + + private function buildCurtain( + ManiphestTask $task, + PhabricatorEditEngine $edit_engine) { + $viewer = $this->getViewer(); + + $id = $task->getID(); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $task, + PhabricatorPolicyCapability::CAN_EDIT); + + $can_interact = PhabricatorPolicyFilter::canInteract($viewer, $task); + + // We expect a policy dialog if you can't edit the task, and expect a + // lock override dialog if you can't interact with it. + $workflow_edit = (!$can_edit || !$can_interact); + + $curtain = $this->newCurtainView($task); + + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Edit Task')) + ->setIcon('fa-pencil') + ->setHref($this->getApplicationURI("/task/edit/{$id}/")) + ->setDisabled(!$can_edit) + ->setWorkflow($workflow_edit)); + $relationship_list = PhabricatorObjectRelationshipList::newForObject( $viewer, $task); + $subtask_item = $this->buildSubtaskItem($task, $edit_engine, ManiphestTaskDependsOnTaskEdgeType::EDGECONST); + $blocker_item = $this->buildSubtaskItem($task, $edit_engine, ManiphestTaskBlockerEdgeType::EDGECONST); + $submenu_actions = array( $subtask_item, ManiphestTaskHasParentRelationship::RELATIONSHIPKEY, ManiphestTaskHasSubtaskRelationship::RELATIONSHIPKEY, + $blocker_item, + ManiphestTaskHasBlockedRelationship::RELATIONSHIPKEY, + ManiphestTaskHasBlockerRelationship::RELATIONSHIPKEY, ManiphestTaskMergeInRelationship::RELATIONSHIPKEY, ManiphestTaskCloseAsDuplicateRelationship::RELATIONSHIPKEY, ); diff --git a/src/applications/maniphest/controller/ManiphestTaskGraphController.php b/src/applications/maniphest/controller/ManiphestTaskGraphController.php index f4655d1835..8ffce1902a 100644 --- a/src/applications/maniphest/controller/ManiphestTaskGraphController.php +++ b/src/applications/maniphest/controller/ManiphestTaskGraphController.php @@ -7,34 +7,36 @@ public function shouldAllowPublic() { return true; } - public function handleRequest(AphrontRequest $request) { - $viewer = $this->getViewer(); - $id = $request->getURIData('id'); - - $task = id(new ManiphestTaskQuery()) - ->setViewer($viewer) - ->withIDs(array($id)) - ->executeOne(); - if (!$task) { - return new Aphront404Response(); + private function buildGraph(ManiphestTask $task, int $subtask_type) { + switch ($subtask_type) { + case ManiphestTaskDependsOnTaskEdgeType::EDGECONST: + $parent_type = ManiphestTaskDependedOnByTaskEdgeType::EDGECONST; + break; + + case ManiphestTaskBlockerEdgeType::EDGECONST: + $parent_type = ManiphestTaskBlockedEdgeType::EDGECONST; + break; + + default: + return null; } - $crumbs = $this->buildApplicationCrumbs() - ->addTextCrumb($task->getMonogram(), $task->getURI()) - ->addTextCrumb(pht('Graph')) - ->setBorder(true); - + $viewer = $this->getViewer(); $graph_limit = 2000; $overflow_message = null; $task_graph = id(new ManiphestTaskGraph()) ->setViewer($viewer) + ->setSubtaskType($subtask_type) ->setSeedPHID($task->getPHID()) ->setLimit($graph_limit) ->setIsStandalone(true) ->loadGraph(); + + $graph_table = null; + $has_parents = null; + $has_subtasks = null; + if (!$task_graph->isEmpty()) { - $parent_type = ManiphestTaskDependedOnByTaskEdgeType::EDGECONST; - $subtask_type = ManiphestTaskDependsOnTaskEdgeType::EDGECONST; $parent_map = $task_graph->getEdges($parent_type); $subtask_map = $task_graph->getEdges($subtask_type); $parent_list = idx($parent_map, $task->getPHID(), array()); @@ -54,8 +56,6 @@ public function handleRequest(AphrontRequest $request) { 'or subtasks.', new PhutilNumber($graph_limit), phutil_tag('strong', array(), pht('Search...'))); - - $graph_table = null; } else { // If there aren't too many direct tasks, but there are too many total // tasks, we'll only render directly connected tasks. @@ -71,13 +71,19 @@ public function handleRequest(AphrontRequest $request) { $graph_table = $task_graph->newGraphTable(); } - $graph_menu = $this->newTaskGraphDropdownMenu( - $task, - $has_parents, - $has_subtasks, - false); + if ($overflow_message) { + $overflow_view = $this->newTaskGraphOverflowView( + $task, + $overflow_message, + false); + + $graph_table = array( + $overflow_view, + $graph_table, + ); + } + } else { - $graph_menu = null; $graph_table = null; $overflow_message = pht( @@ -85,29 +91,70 @@ public function handleRequest(AphrontRequest $request) { 'graph to draw.'); } - if ($overflow_message) { - $overflow_view = $this->newTaskGraphOverflowView( - $task, - $overflow_message, - false); + $result = array( + 'table' => $graph_table, + 'has_parents' => $has_parents, + 'has_subtasks' => $has_subtasks, + ); + return $result; - $graph_table = array( - $overflow_view, - $graph_table, - ); + } + + public function handleRequest(AphrontRequest $request) { + $viewer = $this->getViewer(); + $id = $request->getURIData('id'); + + $task = id(new ManiphestTaskQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->executeOne(); + if (!$task) { + return new Aphront404Response(); } + $crumbs = $this->buildApplicationCrumbs() + ->addTextCrumb($task->getMonogram(), $task->getURI()) + ->addTextCrumb(pht('Graph')) + ->setBorder(true); + $header = id(new PHUIHeaderView()) ->setHeader(pht('Task Graph')); + $subtask_graph_map = $this->buildGraph($task, ManiphestTaskDependsOnTaskEdgeType::EDGECONST); + $subtask_graph_table = $subtask_graph_map['table']; + $has_parents = $subtask_graph_map['has_parents']; + $has_subtasks = $subtask_graph_map['has_subtasks']; + + $blocker_graph_map = $this->buildGraph($task, ManiphestTaskBlockerEdgeType::EDGECONST); + $blocker_graph_table = $blocker_graph_map['table']; + $has_blocked_tasks = $blocker_graph_map['has_parents']; + $has_blockers = $blocker_graph_map['has_subtasks']; + + $graph_menu = $this->newTaskGraphDropdownMenu( + $task, + $has_parents, + $has_blocked_tasks, + $has_subtasks, + $has_blockers, + true); + if ($graph_menu) { $header->addActionLink($graph_menu); } + $subtask_header = id(new PHUIHeaderView()) + ->setHeader(pht('Subtasks')); + + $blocker_header = id(new PHUIHeaderView()) + ->setHeader(pht('Blockers')); + $tab_view = id(new PHUIObjectBoxView()) ->setHeader($header) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) - ->appendChild($graph_table); + ->appendChild($subtask_header) + ->appendChild($subtask_graph_table) + ->appendChild($blocker_header) + ->appendChild($blocker_graph_table); $view = id(new PHUITwoColumnView()) ->setFooter($tab_view); diff --git a/src/applications/maniphest/edge/ManiphestTaskBlockedEdgeType.php b/src/applications/maniphest/edge/ManiphestTaskBlockedEdgeType.php new file mode 100644 index 0000000000..87087af247 --- /dev/null +++ b/src/applications/maniphest/edge/ManiphestTaskBlockedEdgeType.php @@ -0,0 +1,113 @@ +setIsReorderable(false) ->setIsDefaultable(false) ->setIsLockable(false), + id(new PhabricatorHandlesEditField()) + ->setKey('blocked') + ->setLabel(pht('Blocked Task')) + ->setDescription(pht('Task to make blocked by this.')) + ->setConduitDescription(pht('Create as a blocker of another task.')) + ->setConduitTypeDescription(pht('PHID of the blocked task.')) + ->setAliases(array('parentPHID')) + ->setTransactionType(ManiphestTaskBlockedTransaction::TRANSACTIONTYPE) + ->setHandleParameterType(new ManiphestTaskListHTTPParameterType()) + ->setSingleValue(null) + ->setIsReorderable(false) + ->setIsDefaultable(false) + ->setIsLockable(false), id(new PhabricatorColumnsEditField()) ->setKey('column') ->setLabel(pht('Column')) diff --git a/src/applications/maniphest/relationship/ManiphestTaskHasBlockedRelationship.php b/src/applications/maniphest/relationship/ManiphestTaskHasBlockedRelationship.php new file mode 100644 index 0000000000..a5009a2334 --- /dev/null +++ b/src/applications/maniphest/relationship/ManiphestTaskHasBlockedRelationship.php @@ -0,0 +1,45 @@ +setSelectedFilter('open'); + } + +} diff --git a/src/applications/maniphest/relationship/ManiphestTaskHasBlockerRelationship.php b/src/applications/maniphest/relationship/ManiphestTaskHasBlockerRelationship.php new file mode 100644 index 0000000000..3ffc189e2e --- /dev/null +++ b/src/applications/maniphest/relationship/ManiphestTaskHasBlockerRelationship.php @@ -0,0 +1,45 @@ +setSelectedFilter('open'); + } + +} diff --git a/src/applications/maniphest/storage/ManiphestTask.php b/src/applications/maniphest/storage/ManiphestTask.php index 46dd8a1547..fbf7dc64a0 100644 --- a/src/applications/maniphest/storage/ManiphestTask.php +++ b/src/applications/maniphest/storage/ManiphestTask.php @@ -514,6 +514,8 @@ public function getConduitSearchAttachments() { return array( id(new PhabricatorBoardColumnsSearchEngineAttachment()) ->setAttachmentKey('columns'), + id(new PhabricatorRoleBoardColumnsSearchEngineAttachment()) + ->setAttachmentKey('role_columns'), ); } diff --git a/src/applications/maniphest/xaction/ManiphestTaskBlockedTransaction.php b/src/applications/maniphest/xaction/ManiphestTaskBlockedTransaction.php new file mode 100644 index 0000000000..c7a6344e80 --- /dev/null +++ b/src/applications/maniphest/xaction/ManiphestTaskBlockedTransaction.php @@ -0,0 +1,60 @@ +getPHID(); + + id(new PhabricatorEdgeEditor()) + ->addEdge($parent_phid, $parent_type, $task_phid) + ->save(); + } + + public function validateTransactions($object, array $xactions) { + $errors = array(); + + $with_effect = array(); + foreach ($xactions as $xaction) { + $task_phid = $xaction->getNewValue(); + if (!$task_phid) { + continue; + } + + $with_effect[] = $xaction; + + $task = id(new ManiphestTaskQuery()) + ->setViewer($this->getActor()) + ->withPHIDs(array($task_phid)) + ->executeOne(); + if (!$task) { + $errors[] = $this->newInvalidError( + pht( + 'Blocked task identifier "%s" does not identify a visible '. + 'task.', + $task_phid)); + } + } + + if ($with_effect && !$this->isNewObject()) { + $errors[] = $this->newInvalidError( + pht( + 'You can only select a blocked task when creating a '. + 'transaction for the first time.')); + } + + return $errors; + } +} diff --git a/src/applications/maniphest/xaction/ManiphestTaskPointsTransaction.php b/src/applications/maniphest/xaction/ManiphestTaskPointsTransaction.php index 2037a71127..cf02b1996f 100644 --- a/src/applications/maniphest/xaction/ManiphestTaskPointsTransaction.php +++ b/src/applications/maniphest/xaction/ManiphestTaskPointsTransaction.php @@ -77,7 +77,7 @@ public function validateTransactions($object, array $xactions) { foreach ($xactions as $xaction) { $new = $xaction->getNewValue(); - if (strlen($new) && !is_numeric($new)) { + if (strlen(phutil_string_cast($new)) && !is_numeric($new)) { $errors[] = $this->newInvalidError( pht('Points value must be numeric or empty.')); continue; diff --git a/src/applications/maniphest/xaction/ManiphestTaskTitleTransaction.php b/src/applications/maniphest/xaction/ManiphestTaskTitleTransaction.php index c01c263ba6..4deec5cb15 100644 --- a/src/applications/maniphest/xaction/ManiphestTaskTitleTransaction.php +++ b/src/applications/maniphest/xaction/ManiphestTaskTitleTransaction.php @@ -20,7 +20,7 @@ public function getActionStrength() { public function getActionName() { $old = $this->getOldValue(); - if (!strlen($old)) { + if ($old === null || $old === '') { return pht('Created'); } @@ -30,7 +30,7 @@ public function getActionName() { public function getTitle() { $old = $this->getOldValue(); - if (!strlen($old)) { + if ($old === null || $old === '') { return pht( '%s created this task.', $this->renderAuthor()); @@ -72,7 +72,7 @@ public function validateTransactions($object, array $xactions) { foreach ($xactions as $xaction) { $new = $xaction->getNewValue(); - if (!strlen($new)) { + if ($new === null || $new === '') { $errors[] = $this->newInvalidError( pht('Tasks must have a title.'), $xaction); diff --git a/src/applications/meta/controller/PhabricatorApplicationDetailViewController.php b/src/applications/meta/controller/PhabricatorApplicationDetailViewController.php index 8be6d73a1d..2161e51134 100644 --- a/src/applications/meta/controller/PhabricatorApplicationDetailViewController.php +++ b/src/applications/meta/controller/PhabricatorApplicationDetailViewController.php @@ -114,7 +114,7 @@ private function buildPropertySectionView( } $overview = $application->getOverview(); - if (strlen($overview)) { + if ($overview !== null && $overview !== '') { $overview = new PHUIRemarkupView($viewer, $overview); $properties->addSectionHeader( pht('Overview'), PHUIPropertyListView::ICON_SUMMARY); diff --git a/src/applications/metamta/management/PhabricatorMailManagementShowOutboundWorkflow.php b/src/applications/metamta/management/PhabricatorMailManagementShowOutboundWorkflow.php index f29a63c2eb..fd725bc4f7 100644 --- a/src/applications/metamta/management/PhabricatorMailManagementShowOutboundWorkflow.php +++ b/src/applications/metamta/management/PhabricatorMailManagementShowOutboundWorkflow.php @@ -203,7 +203,7 @@ public function execute(PhutilArgumentParser $args) { $info[] = null; $info[] = $this->newSectionHeader(pht('HTML BODY')); - if (strlen($message->getHTMLBody())) { + if (strlen(phutil_string_cast($message->getHTMLBody()))) { $info[] = $message->getHTMLBody(); $info[] = null; } else { diff --git a/src/applications/people/controller/PhabricatorPeopleProfileViewController.php b/src/applications/people/controller/PhabricatorPeopleProfileViewController.php index b929d980d5..0a6377104f 100644 --- a/src/applications/people/controller/PhabricatorPeopleProfileViewController.php +++ b/src/applications/people/controller/PhabricatorPeopleProfileViewController.php @@ -47,6 +47,7 @@ public function handleRequest(AphrontRequest $request) { ->appendChild($feed); $projects = $this->buildProjectsView($user); + $roles = $this->buildRolesView($user); $calendar = $this->buildCalendarDayView($user); $home = id(new PHUITwoColumnView()) @@ -61,6 +62,7 @@ public function handleRequest(AphrontRequest $request) { ->setSideColumn( array( $projects, + $roles, $calendar, )); @@ -168,6 +170,60 @@ private function buildProjectsView( return $box; } + private function buildRolesView( + PhabricatorUser $user) { + + $viewer = $this->getViewer(); + $roles = id(new PhabricatorRoleQuery()) + ->setViewer($viewer) + ->withMemberPHIDs(array($user->getPHID())) + ->needImages(true) + ->withStatuses( + array( + PhabricatorRoleStatus::STATUS_ACTIVE, + )) + ->execute(); + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Roles')); + + if (!empty($roles)) { + $limit = 5; + $render_phids = array_slice($roles, 0, $limit); + $list = id(new PhabricatorRoleListView()) + ->setUser($viewer) + ->setRoles($render_phids); + + if (count($roles) > $limit) { + $header_text = pht( + 'Roles (%s)', + phutil_count($roles)); + + $header = id(new PHUIHeaderView()) + ->setHeader($header_text) + ->addActionLink( + id(new PHUIButtonView()) + ->setTag('a') + ->setIcon('fa-list-ul') + ->setText(pht('View All')) + ->setHref('/role/?member='.$user->getPHID())); + + } + + } else { + $list = id(new PHUIInfoView()) + ->setSeverity(PHUIInfoView::SEVERITY_NODATA) + ->appendChild(pht('User does not belong to any roles.')); + } + + $box = id(new PHUIObjectBoxView()) + ->setHeader($header) + ->appendChild($list) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY); + + return $box; + } + private function buildCalendarDayView(PhabricatorUser $user) { $viewer = $this->getViewer(); $class = 'PhabricatorCalendarApplication'; diff --git a/src/applications/phriction/xaction/PhrictionDocumentContentTransaction.php b/src/applications/phriction/xaction/PhrictionDocumentContentTransaction.php index f5f1305279..42d5c50e93 100644 --- a/src/applications/phriction/xaction/PhrictionDocumentContentTransaction.php +++ b/src/applications/phriction/xaction/PhrictionDocumentContentTransaction.php @@ -21,10 +21,10 @@ public function validateTransactions($object, array $xactions) { // create an empty document. $content = $object->getContent()->getContent(); - if ($this->isEmptyTextTransaction($content, $xactions)) { - $errors[] = $this->newRequiredError( - pht('Documents must have content.')); - } + //if ($this->isEmptyTextTransaction($content, $xactions)) { + // $errors[] = $this->newRequiredError( + // pht('Documents must have content.')); + //} return $errors; } diff --git a/src/applications/phriction/xaction/PhrictionDocumentDraftTransaction.php b/src/applications/phriction/xaction/PhrictionDocumentDraftTransaction.php index 3fc9d63ca8..5160a36254 100644 --- a/src/applications/phriction/xaction/PhrictionDocumentDraftTransaction.php +++ b/src/applications/phriction/xaction/PhrictionDocumentDraftTransaction.php @@ -28,10 +28,10 @@ public function validateTransactions($object, array $xactions) { } $content = $object->getContent()->getContent(); - if ($this->isEmptyTextTransaction($content, $xactions)) { - $errors[] = $this->newRequiredError( - pht('Documents must have content.')); - } + //if ($this->isEmptyTextTransaction($content, $xactions)) { + //$errors[] = $this->newRequiredError( + //pht('Documents must have content.')); + //} return $errors; } diff --git a/src/applications/project/controller/PhabricatorProjectBoardReloadController.php b/src/applications/project/controller/PhabricatorProjectBoardReloadController.php index 6204671505..cec9b02218 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardReloadController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardReloadController.php @@ -12,7 +12,7 @@ public function handleRequest(AphrontRequest $request) { } $order = $request->getStr('order'); - if (!strlen($order)) { + if ($order === null || $order === '') { $order = PhabricatorProjectColumnNaturalOrder::ORDERKEY; } diff --git a/src/applications/project/phid/PhabricatorProjectProjectPHIDType.php b/src/applications/project/phid/PhabricatorProjectProjectPHIDType.php index 9247966d75..50ec92dc0d 100644 --- a/src/applications/project/phid/PhabricatorProjectProjectPHIDType.php +++ b/src/applications/project/phid/PhabricatorProjectProjectPHIDType.php @@ -43,7 +43,7 @@ public function loadHandles( $handle->setName($name); - if (strlen($slug)) { + if ($slug !== null && $slug !== '') { $handle->setObjectName('#'.$slug); $handle->setMailStampName('#'.$slug); $handle->setURI("/tag/{$slug}/"); diff --git a/src/applications/project/query/PhabricatorProjectSearchEngine.php b/src/applications/project/query/PhabricatorProjectSearchEngine.php index cb179c995f..a20d2b1d38 100644 --- a/src/applications/project/query/PhabricatorProjectSearchEngine.php +++ b/src/applications/project/query/PhabricatorProjectSearchEngine.php @@ -130,7 +130,7 @@ protected function buildCustomSearchFields() { protected function buildQueryFromParameters(array $map) { $query = $this->newQuery(); - if (strlen($map['name'])) { + if ($map['name'] !== null && $map['name'] !== '') { $tokens = PhabricatorTypeaheadDatasource::tokenizeString($map['name']); $query->withNameTokens($tokens); } diff --git a/src/applications/project/storage/PhabricatorProjectColumn.php b/src/applications/project/storage/PhabricatorProjectColumn.php index 49d7f28a9f..fa88872707 100644 --- a/src/applications/project/storage/PhabricatorProjectColumn.php +++ b/src/applications/project/storage/PhabricatorProjectColumn.php @@ -105,7 +105,7 @@ public function getDisplayName() { } $name = $this->getName(); - if (strlen($name)) { + if ($name !== null && $name !== '') { return $name; } diff --git a/src/applications/project/storage/PhabricatorProjectTrigger.php b/src/applications/project/storage/PhabricatorProjectTrigger.php index d7892dd7a1..4277bc10a8 100644 --- a/src/applications/project/storage/PhabricatorProjectTrigger.php +++ b/src/applications/project/storage/PhabricatorProjectTrigger.php @@ -53,7 +53,7 @@ public function setViewer(PhabricatorUser $user) { public function getDisplayName() { $name = $this->getName(); - if (strlen($name)) { + if ($name !== null && $name !== '') { return $name; } diff --git a/src/applications/project/typeahead/PhabricatorProjectDatasource.php b/src/applications/project/typeahead/PhabricatorProjectDatasource.php index 5be7ace110..661f6c1fff 100644 --- a/src/applications/project/typeahead/PhabricatorProjectDatasource.php +++ b/src/applications/project/typeahead/PhabricatorProjectDatasource.php @@ -84,10 +84,10 @@ public function loadResults() { } $slug = $proj->getPrimarySlug(); - if (!strlen($slug)) { + if ($slug === null || $slug === '') { foreach ($proj->getSlugs() as $slug_object) { $slug = $slug_object->getSlug(); - if (strlen($slug)) { + if ($slug !== null && $slug !== '') { break; } } @@ -133,7 +133,7 @@ public function loadResults() { ->setPriorityType('proj') ->setClosed($closed); - if (strlen($slug)) { + if ($slug !== null && $slug !== '') { $proj_result->setAutocomplete('#'.$slug); } diff --git a/src/applications/project/xaction/column/PhabricatorProjectColumnLimitTransaction.php b/src/applications/project/xaction/column/PhabricatorProjectColumnLimitTransaction.php index 8e91ccbe5d..99033d4653 100644 --- a/src/applications/project/xaction/column/PhabricatorProjectColumnLimitTransaction.php +++ b/src/applications/project/xaction/column/PhabricatorProjectColumnLimitTransaction.php @@ -10,7 +10,7 @@ public function generateOldValue($object) { } public function generateNewValue($object, $value) { - if (strlen($value)) { + if ($value !== null && $value !== '') { return (int)$value; } else { return null; diff --git a/src/applications/project/xaction/column/PhabricatorProjectColumnNameTransaction.php b/src/applications/project/xaction/column/PhabricatorProjectColumnNameTransaction.php index 4a7342dc5f..5c8745aa55 100644 --- a/src/applications/project/xaction/column/PhabricatorProjectColumnNameTransaction.php +++ b/src/applications/project/xaction/column/PhabricatorProjectColumnNameTransaction.php @@ -17,12 +17,12 @@ public function getTitle() { $old = $this->getOldValue(); $new = $this->getNewValue(); - if (!strlen($old)) { + if ($old === null || $old === '') { return pht( '%s named this column %s.', $this->renderAuthor(), $this->renderNewValue()); - } else if (strlen($new)) { + } else if ($new !== null && $new !== '') { return pht( '%s renamed this column from %s to %s.', $this->renderAuthor(), diff --git a/src/applications/roles/__tests__/PhabricatorRoleCoreTestCase.php b/src/applications/roles/__tests__/PhabricatorRoleCoreTestCase.php new file mode 100644 index 0000000000..fd0cd6b812 --- /dev/null +++ b/src/applications/roles/__tests__/PhabricatorRoleCoreTestCase.php @@ -0,0 +1,1694 @@ + true, + ); + } + + public function testViewRole() { + $user = $this->createUser(); + $user->save(); + + $user2 = $this->createUser(); + $user2->save(); + + $role = $this->createRole($user); + + $role = $this->refreshRole($role, $user, true); + + $this->joinRole($role, $user); + $role->setViewPolicy(PhabricatorPolicies::POLICY_USER); + $role->save(); + + $can_view = PhabricatorPolicyCapability::CAN_VIEW; + + // When the view policy is set to "users", any user can see the role. + $this->assertTrue((bool)$this->refreshRole($role, $user)); + $this->assertTrue((bool)$this->refreshRole($role, $user2)); + + + // When the view policy is set to "no one", members can still see the + // role. + $role->setViewPolicy(PhabricatorPolicies::POLICY_NOONE); + $role->save(); + + $this->assertTrue((bool)$this->refreshRole($role, $user)); + $this->assertFalse((bool)$this->refreshRole($role, $user2)); + } + + public function testApplicationPolicy() { + $user = $this->createUser() + ->save(); + + $role = $this->createRole($user); + + $this->assertTrue( + PhabricatorPolicyFilter::hasCapability( + $user, + $role, + PhabricatorPolicyCapability::CAN_VIEW)); + + // This object is visible so its handle should load normally. + $handle = id(new PhabricatorHandleQuery()) + ->setViewer($user) + ->withPHIDs(array($role->getPHID())) + ->executeOne(); + $this->assertEqual($role->getPHID(), $handle->getPHID()); + + // Change the "Can Use Application" policy for roles to "No One". This + // should cause filtering checks to fail even when they are executed + // directly rather than via a Query. + $env = PhabricatorEnv::beginScopedEnv(); + $env->overrideEnvConfig( + 'phabricator.application-settings', + array( + 'PHID-APPS-PhabricatorRoleApplication' => array( + 'policy' => array( + 'view' => PhabricatorPolicies::POLICY_NOONE, + ), + ), + )); + + // Application visibility is cached because it does not normally change + // over the course of a single request. Drop the cache so the next filter + // test uses the new visibility. + PhabricatorCaches::destroyRequestCache(); + + $this->assertFalse( + PhabricatorPolicyFilter::hasCapability( + $user, + $role, + PhabricatorPolicyCapability::CAN_VIEW)); + + // We should still be able to load a handle for the role, even if we + // can not see the application. + $handle = id(new PhabricatorHandleQuery()) + ->setViewer($user) + ->withPHIDs(array($role->getPHID())) + ->executeOne(); + + // The handle should load... + $this->assertEqual($role->getPHID(), $handle->getPHID()); + + // ...but be policy filtered. + $this->assertTrue($handle->getPolicyFiltered()); + + unset($env); + } + + public function testIsViewerMemberOrWatcher() { + $user1 = $this->createUser() + ->save(); + + $user2 = $this->createUser() + ->save(); + + $user3 = $this->createUser() + ->save(); + + $role1 = $this->createRole($user1); + $role1 = $this->refreshRole($role1, $user1); + + $this->joinRole($role1, $user1); + $this->joinRole($role1, $user3); + $this->watchRole($role1, $user3); + + $role1 = $this->refreshRole($role1, $user1); + + $this->assertTrue($role1->isUserMember($user1->getPHID())); + + $role1 = $this->refreshRole($role1, $user1, false, true); + + $this->assertTrue($role1->isUserMember($user1->getPHID())); + $this->assertFalse($role1->isUserWatcher($user1->getPHID())); + + $role1 = $this->refreshRole($role1, $user1, true, false); + + $this->assertTrue($role1->isUserMember($user1->getPHID())); + $this->assertFalse($role1->isUserMember($user2->getPHID())); + $this->assertTrue($role1->isUserMember($user3->getPHID())); + + $role1 = $this->refreshRole($role1, $user1, true, true); + + $this->assertTrue($role1->isUserMember($user1->getPHID())); + $this->assertFalse($role1->isUserMember($user2->getPHID())); + $this->assertTrue($role1->isUserMember($user3->getPHID())); + + $this->assertFalse($role1->isUserWatcher($user1->getPHID())); + $this->assertFalse($role1->isUserWatcher($user2->getPHID())); + $this->assertTrue($role1->isUserWatcher($user3->getPHID())); + } + + public function testEditRole() { + $user = $this->createUser(); + $user->save(); + + $user->setAllowInlineCacheGeneration(true); + + $role = $this->createRole($user); + + + // When edit and view policies are set to "user", anyone can edit. + $role->setViewPolicy(PhabricatorPolicies::POLICY_USER); + $role->setEditPolicy(PhabricatorPolicies::POLICY_USER); + $role->save(); + + $this->assertTrue($this->attemptRoleEdit($role, $user)); + + + // When edit policy is set to "no one", no one can edit. + $role->setEditPolicy(PhabricatorPolicies::POLICY_NOONE); + $role->save(); + + $caught = null; + try { + $this->attemptRoleEdit($role, $user); + } catch (Exception $ex) { + $caught = $ex; + } + $this->assertTrue($caught instanceof Exception); + } + + public function testAncestorMembers() { + $user1 = $this->createUser(); + $user1->save(); + + $user2 = $this->createUser(); + $user2->save(); + + $parent = $this->createRole($user1); + $child = $this->createRole($user1, $parent); + + $this->joinRole($child, $user1); + $this->joinRole($child, $user2); + + $role = id(new PhabricatorRoleQuery()) + ->setViewer($user1) + ->withPHIDs(array($child->getPHID())) + ->needAncestorMembers(true) + ->executeOne(); + + $members = array_fuse($role->getParentRole()->getMemberPHIDs()); + ksort($members); + + $expect = array_fuse( + array( + $user1->getPHID(), + $user2->getPHID(), + )); + ksort($expect); + + $this->assertEqual($expect, $members); + } + + public function testAncestryQueries() { + $user = $this->createUser(); + $user->save(); + + $ancestor = $this->createRole($user); + $parent = $this->createRole($user, $ancestor); + $child = $this->createRole($user, $parent); + + $roles = id(new PhabricatorRoleQuery()) + ->setViewer($user) + ->withAncestorRolePHIDs(array($ancestor->getPHID())) + ->execute(); + $this->assertEqual(2, count($roles)); + + $roles = id(new PhabricatorRoleQuery()) + ->setViewer($user) + ->withParentRolePHIDs(array($ancestor->getPHID())) + ->execute(); + $this->assertEqual(1, count($roles)); + $this->assertEqual( + $parent->getPHID(), + head($roles)->getPHID()); + + $roles = id(new PhabricatorRoleQuery()) + ->setViewer($user) + ->withAncestorRolePHIDs(array($ancestor->getPHID())) + ->withDepthBetween(2, null) + ->execute(); + $this->assertEqual(1, count($roles)); + $this->assertEqual( + $child->getPHID(), + head($roles)->getPHID()); + + $parent2 = $this->createRole($user, $ancestor); + $child2 = $this->createRole($user, $parent2); + $grandchild2 = $this->createRole($user, $child2); + + $roles = id(new PhabricatorRoleQuery()) + ->setViewer($user) + ->withAncestorRolePHIDs(array($ancestor->getPHID())) + ->execute(); + $this->assertEqual(5, count($roles)); + + $roles = id(new PhabricatorRoleQuery()) + ->setViewer($user) + ->withParentRolePHIDs(array($ancestor->getPHID())) + ->execute(); + $this->assertEqual(2, count($roles)); + + $roles = id(new PhabricatorRoleQuery()) + ->setViewer($user) + ->withAncestorRolePHIDs(array($ancestor->getPHID())) + ->withDepthBetween(2, null) + ->execute(); + $this->assertEqual(3, count($roles)); + + $roles = id(new PhabricatorRoleQuery()) + ->setViewer($user) + ->withAncestorRolePHIDs(array($ancestor->getPHID())) + ->withDepthBetween(3, null) + ->execute(); + $this->assertEqual(1, count($roles)); + + $roles = id(new PhabricatorRoleQuery()) + ->setViewer($user) + ->withPHIDs( + array( + $child->getPHID(), + $grandchild2->getPHID(), + )) + ->execute(); + $this->assertEqual(2, count($roles)); + } + + public function testMemberMaterialization() { + $material_type = PhabricatorRoleMaterializedMemberEdgeType::EDGECONST; + + $user = $this->createUser(); + $user->save(); + + $parent = $this->createRole($user); + $child = $this->createRole($user, $parent); + + $this->joinRole($child, $user); + + $parent_material = PhabricatorEdgeQuery::loadDestinationPHIDs( + $parent->getPHID(), + $material_type); + + $this->assertEqual( + array($user->getPHID()), + $parent_material); + } + + public function testMilestones() { + $user = $this->createUser(); + $user->save(); + + $parent = $this->createRole($user); + + $m1 = $this->createRole($user, $parent, true); + $m2 = $this->createRole($user, $parent, true); + $m3 = $this->createRole($user, $parent, true); + + $this->assertEqual(1, $m1->getMilestoneNumber()); + $this->assertEqual(2, $m2->getMilestoneNumber()); + $this->assertEqual(3, $m3->getMilestoneNumber()); + } + + public function testMilestoneMembership() { + $user = $this->createUser(); + $user->save(); + + $parent = $this->createRole($user); + $milestone = $this->createRole($user, $parent, true); + + $this->joinRole($parent, $user); + + $milestone = id(new PhabricatorRoleQuery()) + ->setViewer($user) + ->withPHIDs(array($milestone->getPHID())) + ->executeOne(); + + $this->assertTrue($milestone->isUserMember($user->getPHID())); + + $milestone = id(new PhabricatorRoleQuery()) + ->setViewer($user) + ->withPHIDs(array($milestone->getPHID())) + ->needMembers(true) + ->executeOne(); + + $this->assertEqual( + array($user->getPHID()), + $milestone->getMemberPHIDs()); + } + + public function testSameSlugAsName() { + // It should be OK to type the primary hashtag into "additional hashtags", + // even if the primary hashtag doesn't exist yet because you're creating + // or renaming the role. + + $user = $this->createUser(); + $user->save(); + + $role = $this->createRole($user); + + // In this first case, set the name and slugs at the same time. + $name = 'slugrole'; + + $xactions = array(); + $xactions[] = id(new PhabricatorRoleTransaction()) + ->setTransactionType(PhabricatorRoleNameTransaction::TRANSACTIONTYPE) + ->setNewValue($name); + $this->applyTransactions($role, $user, $xactions); + + $xactions = array(); + $xactions[] = id(new PhabricatorRoleTransaction()) + ->setTransactionType(PhabricatorRoleSlugsTransaction::TRANSACTIONTYPE) + ->setNewValue(array($name)); + $this->applyTransactions($role, $user, $xactions); + + $role = id(new PhabricatorRoleQuery()) + ->setViewer($user) + ->withPHIDs(array($role->getPHID())) + ->needSlugs(true) + ->executeOne(); + + $slugs = $role->getSlugs(); + $slugs = mpull($slugs, 'getSlug'); + + $this->assertTrue(in_array($name, $slugs)); + + // In this second case, set the name first and then the slugs separately. + $name2 = 'slugrole2'; + $xactions = array(); + + $xactions[] = id(new PhabricatorRoleTransaction()) + ->setTransactionType(PhabricatorRoleNameTransaction::TRANSACTIONTYPE) + ->setNewValue($name2); + + $xactions[] = id(new PhabricatorRoleTransaction()) + ->setTransactionType(PhabricatorRoleSlugsTransaction::TRANSACTIONTYPE) + ->setNewValue(array($name2)); + + $this->applyTransactions($role, $user, $xactions); + + $role = id(new PhabricatorRoleQuery()) + ->setViewer($user) + ->withPHIDs(array($role->getPHID())) + ->needSlugs(true) + ->executeOne(); + + $slugs = $role->getSlugs(); + $slugs = mpull($slugs, 'getSlug'); + + $this->assertTrue(in_array($name2, $slugs)); + } + + public function testDuplicateSlugs() { + // Creating a role with multiple duplicate slugs should succeed. + + $user = $this->createUser(); + $user->save(); + + $role = $this->createRole($user); + + $input = 'duplicate'; + + $xactions = array(); + + $xactions[] = id(new PhabricatorRoleTransaction()) + ->setTransactionType(PhabricatorRoleSlugsTransaction::TRANSACTIONTYPE) + ->setNewValue(array($input, $input)); + + $this->applyTransactions($role, $user, $xactions); + + $role = id(new PhabricatorRoleQuery()) + ->setViewer($user) + ->withPHIDs(array($role->getPHID())) + ->needSlugs(true) + ->executeOne(); + + $slugs = $role->getSlugs(); + $slugs = mpull($slugs, 'getSlug'); + + $this->assertTrue(in_array($input, $slugs)); + } + + public function testNormalizeSlugs() { + // When a user creates a role with slug "XxX360n0sc0perXxX", normalize + // it before writing it. + + $user = $this->createUser(); + $user->save(); + + $role = $this->createRole($user); + + $input = 'NoRmAlIzE'; + $expect = 'normalize'; + + $xactions = array(); + + $xactions[] = id(new PhabricatorRoleTransaction()) + ->setTransactionType(PhabricatorRoleSlugsTransaction::TRANSACTIONTYPE) + ->setNewValue(array($input)); + + $this->applyTransactions($role, $user, $xactions); + + $role = id(new PhabricatorRoleQuery()) + ->setViewer($user) + ->withPHIDs(array($role->getPHID())) + ->needSlugs(true) + ->executeOne(); + + $slugs = $role->getSlugs(); + $slugs = mpull($slugs, 'getSlug'); + + $this->assertTrue(in_array($expect, $slugs)); + + + // If another user tries to add the same slug in denormalized form, it + // should be caught and fail, even though the database version of the slug + // is normalized. + + $role2 = $this->createRole($user); + + $xactions = array(); + + $xactions[] = id(new PhabricatorRoleTransaction()) + ->setTransactionType(PhabricatorRoleSlugsTransaction::TRANSACTIONTYPE) + ->setNewValue(array($input)); + + $caught = null; + try { + $this->applyTransactions($role2, $user, $xactions); + } catch (PhabricatorApplicationTransactionValidationException $ex) { + $caught = $ex; + } + + $this->assertTrue((bool)$caught); + } + + public function testRoleMembersVisibility() { + // This is primarily testing that you can create a role and set the + // visibility or edit policy to "Role Members" immediately. + + $user1 = $this->createUser(); + $user1->save(); + + $user2 = $this->createUser(); + $user2->save(); + + $role = PhabricatorRole::initializeNewRole($user1); + $name = pht('Test Role %d', mt_rand()); + + $xactions = array(); + + $xactions[] = id(new PhabricatorRoleTransaction()) + ->setTransactionType(PhabricatorRoleNameTransaction::TRANSACTIONTYPE) + ->setNewValue($name); + + $xactions[] = id(new PhabricatorRoleTransaction()) + ->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY) + ->setNewValue( + id(new PhabricatorRoleMembersPolicyRule()) + ->getObjectPolicyFullKey()); + + $edge_type = PhabricatorRoleRoleHasMemberEdgeType::EDGECONST; + + $xactions[] = id(new PhabricatorRoleTransaction()) + ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) + ->setMetadataValue('edge:type', $edge_type) + ->setNewValue( + array( + '=' => array($user1->getPHID() => $user1->getPHID()), + )); + + $this->applyTransactions($role, $user1, $xactions); + + $this->assertTrue((bool)$this->refreshRole($role, $user1)); + $this->assertFalse((bool)$this->refreshRole($role, $user2)); + + $this->leaveRole($role, $user1); + + $this->assertFalse((bool)$this->refreshRole($role, $user1)); + } + + public function testParentRole() { + $user = $this->createUser(); + $user->save(); + + $parent = $this->createRole($user); + $child = $this->createRole($user, $parent); + + $this->assertTrue(true); + + $child = $this->refreshRole($child, $user); + + $this->assertEqual( + $parent->getPHID(), + $child->getParentRole()->getPHID()); + + $this->assertEqual(1, (int)$child->getRoleDepth()); + + $this->assertFalse( + $child->isUserMember($user->getPHID())); + + $this->assertFalse( + $child->getParentRole()->isUserMember($user->getPHID())); + + $this->joinRole($child, $user); + + $child = $this->refreshRole($child, $user); + + $this->assertTrue( + $child->isUserMember($user->getPHID())); + + $this->assertTrue( + $child->getParentRole()->isUserMember($user->getPHID())); + + + // Test that hiding a parent hides the child. + + $user2 = $this->createUser(); + $user2->save(); + + // Second user can see the role for now. + $this->assertTrue((bool)$this->refreshRole($child, $user2)); + + // Hide the parent. + $this->setViewPolicy($parent, $user, $user->getPHID()); + + // First user (who can see the parent because they are a member of + // the child) can see the role. + $this->assertTrue((bool)$this->refreshRole($child, $user)); + + // Second user can not, because they can't see the parent. + $this->assertFalse((bool)$this->refreshRole($child, $user2)); + } + + public function testSlugMaps() { + // When querying by slugs, slugs should be normalized and the mapping + // should be reported correctly. + $user = $this->createUser(); + $user->save(); + + $name = 'queryslugrole'; + $name2 = 'QUERYslugROLE'; + $slug = 'queryslugextra'; + $slug2 = 'QuErYSlUgExTrA'; + + $role = PhabricatorRole::initializeNewRole($user); + + $xactions = array(); + + $xactions[] = id(new PhabricatorRoleTransaction()) + ->setTransactionType(PhabricatorRoleNameTransaction::TRANSACTIONTYPE) + ->setNewValue($name); + + $xactions[] = id(new PhabricatorRoleTransaction()) + ->setTransactionType(PhabricatorRoleSlugsTransaction::TRANSACTIONTYPE) + ->setNewValue(array($slug)); + + $this->applyTransactions($role, $user, $xactions); + + $role_query = id(new PhabricatorRoleQuery()) + ->setViewer($user) + ->withSlugs(array($name)); + $role_query->execute(); + $map = $role_query->getSlugMap(); + + $this->assertEqual( + array( + $name => $role->getPHID(), + ), + ipull($map, 'rolePHID')); + + $role_query = id(new PhabricatorRoleQuery()) + ->setViewer($user) + ->withSlugs(array($slug)); + $role_query->execute(); + $map = $role_query->getSlugMap(); + + $this->assertEqual( + array( + $slug => $role->getPHID(), + ), + ipull($map, 'rolePHID')); + + $role_query = id(new PhabricatorRoleQuery()) + ->setViewer($user) + ->withSlugs(array($name, $slug, $name2, $slug2)); + $role_query->execute(); + $map = $role_query->getSlugMap(); + + $expect = array( + $name => $role->getPHID(), + $slug => $role->getPHID(), + $name2 => $role->getPHID(), + $slug2 => $role->getPHID(), + ); + + $actual = ipull($map, 'rolePHID'); + + ksort($expect); + ksort($actual); + + $this->assertEqual($expect, $actual); + + $expect = array( + $name => $name, + $slug => $slug, + $name2 => $name, + $slug2 => $slug, + ); + + $actual = ipull($map, 'slug'); + + ksort($expect); + ksort($actual); + + $this->assertEqual($expect, $actual); + } + + public function testJoinLeaveRole() { + $user = $this->createUser(); + $user->save(); + + $role = $this->createRoleWithNewAuthor(); + + $role = $this->refreshRole($role, $user, true); + $this->assertTrue( + (bool)$role, + pht( + 'Assumption that roles are default visible '. + 'to any user when created.')); + + $this->assertFalse( + $role->isUserMember($user->getPHID()), + pht('Arbitrary user not member of role.')); + + // Join the role. + $this->joinRole($role, $user); + + $role = $this->refreshRole($role, $user, true); + $this->assertTrue((bool)$role); + + $this->assertTrue( + $role->isUserMember($user->getPHID()), + pht('Join works.')); + + + // Join the role again. + $this->joinRole($role, $user); + + $role = $this->refreshRole($role, $user, true); + $this->assertTrue((bool)$role); + + $this->assertTrue( + $role->isUserMember($user->getPHID()), + pht('Joining an already-joined role is a no-op.')); + + + // Leave the role. + $this->leaveRole($role, $user); + + $role = $this->refreshRole($role, $user, true); + $this->assertTrue((bool)$role); + + $this->assertFalse( + $role->isUserMember($user->getPHID()), + pht('Leave works.')); + + + // Leave the role again. + $this->leaveRole($role, $user); + + $role = $this->refreshRole($role, $user, true); + $this->assertTrue((bool)$role); + + $this->assertFalse( + $role->isUserMember($user->getPHID()), + pht('Leaving an already-left role is a no-op.')); + + + // If a user can't edit or join a role, joining fails. + $role->setEditPolicy(PhabricatorPolicies::POLICY_NOONE); + $role->setJoinPolicy(PhabricatorPolicies::POLICY_NOONE); + $role->save(); + + $role = $this->refreshRole($role, $user, true); + $caught = null; + try { + $this->joinRole($role, $user); + } catch (Exception $ex) { + $caught = $ex; + } + $this->assertTrue($ex instanceof Exception); + + + // If a user can edit a role, they can join. + $role->setEditPolicy(PhabricatorPolicies::POLICY_USER); + $role->setJoinPolicy(PhabricatorPolicies::POLICY_NOONE); + $role->save(); + + $role = $this->refreshRole($role, $user, true); + $this->joinRole($role, $user); + $role = $this->refreshRole($role, $user, true); + $this->assertTrue( + $role->isUserMember($user->getPHID()), + pht('Join allowed with edit permission.')); + $this->leaveRole($role, $user); + + + // If a user can join a role, they can join, even if they can't edit. + $role->setEditPolicy(PhabricatorPolicies::POLICY_NOONE); + $role->setJoinPolicy(PhabricatorPolicies::POLICY_USER); + $role->save(); + + $role = $this->refreshRole($role, $user, true); + $this->joinRole($role, $user); + $role = $this->refreshRole($role, $user, true); + $this->assertTrue( + $role->isUserMember($user->getPHID()), + pht('Join allowed with join permission.')); + + + // A user can leave a role even if they can't edit it or join. + $role->setEditPolicy(PhabricatorPolicies::POLICY_NOONE); + $role->setJoinPolicy(PhabricatorPolicies::POLICY_NOONE); + $role->save(); + + $role = $this->refreshRole($role, $user, true); + $this->leaveRole($role, $user); + $role = $this->refreshRole($role, $user, true); + $this->assertFalse( + $role->isUserMember($user->getPHID()), + pht('Leave allowed without any permission.')); + } + + + public function testComplexConstraints() { + $user = $this->createUser(); + $user->save(); + + $engineering = $this->createRole($user); + $engineering_scan = $this->createRole($user, $engineering); + $engineering_warp = $this->createRole($user, $engineering); + + $exploration = $this->createRole($user); + $exploration_diplomacy = $this->createRole($user, $exploration); + + $task_engineering = $this->newTask( + $user, + array($engineering), + pht('Engineering Only')); + + $task_exploration = $this->newTask( + $user, + array($exploration), + pht('Exploration Only')); + + $task_warp_explore = $this->newTask( + $user, + array($engineering_warp, $exploration), + pht('Warp to New Planet')); + + $task_diplomacy_scan = $this->newTask( + $user, + array($engineering_scan, $exploration_diplomacy), + pht('Scan Diplomat')); + + $task_diplomacy = $this->newTask( + $user, + array($exploration_diplomacy), + pht('Diplomatic Meeting')); + + $task_warp_scan = $this->newTask( + $user, + array($engineering_scan, $engineering_warp), + pht('Scan Warp Drives')); + + $this->assertQueryByRoles( + $user, + array( + $task_engineering, + $task_warp_explore, + $task_diplomacy_scan, + $task_warp_scan, + ), + array($engineering), + pht('All Engineering')); + + $this->assertQueryByRoles( + $user, + array( + $task_diplomacy_scan, + $task_warp_scan, + ), + array($engineering_scan), + pht('All Scan')); + + $this->assertQueryByRoles( + $user, + array( + $task_warp_explore, + $task_diplomacy_scan, + ), + array($engineering, $exploration), + pht('Engineering + Exploration')); + + // This is testing that a query for "Parent" and "Parent > Child" works + // properly. + $this->assertQueryByRoles( + $user, + array( + $task_diplomacy_scan, + $task_warp_scan, + ), + array($engineering, $engineering_scan), + pht('Engineering + Scan')); + } + + public function testTagAncestryConflicts() { + $user = $this->createUser(); + $user->save(); + + $stonework = $this->createRole($user); + $stonework_masonry = $this->createRole($user, $stonework); + $stonework_sculpting = $this->createRole($user, $stonework); + + $task = $this->newTask($user, array()); + $this->assertEqual(array(), $this->getTaskRoles($task)); + + $this->addRoleTags($user, $task, array($stonework->getPHID())); + $this->assertEqual( + array( + $stonework->getPHID(), + ), + $this->getTaskRoles($task)); + + // Adding a descendant should remove the parent. + $this->addRoleTags($user, $task, array($stonework_masonry->getPHID())); + $this->assertEqual( + array( + $stonework_masonry->getPHID(), + ), + $this->getTaskRoles($task)); + + // Adding an ancestor should remove the descendant. + $this->addRoleTags($user, $task, array($stonework->getPHID())); + $this->assertEqual( + array( + $stonework->getPHID(), + ), + $this->getTaskRoles($task)); + + // Adding two tags in the same hierarchy which are not mutual ancestors + // should remove the ancestor but otherwise work fine. + $this->addRoleTags( + $user, + $task, + array( + $stonework_masonry->getPHID(), + $stonework_sculpting->getPHID(), + )); + + $expect = array( + $stonework_masonry->getPHID(), + $stonework_sculpting->getPHID(), + ); + sort($expect); + + $this->assertEqual($expect, $this->getTaskRoles($task)); + } + + public function testTagMilestoneConflicts() { + $user = $this->createUser(); + $user->save(); + + $stonework = $this->createRole($user); + $stonework_1 = $this->createRole($user, $stonework, true); + $stonework_2 = $this->createRole($user, $stonework, true); + + $task = $this->newTask($user, array()); + $this->assertEqual(array(), $this->getTaskRoles($task)); + + $this->addRoleTags($user, $task, array($stonework->getPHID())); + $this->assertEqual( + array( + $stonework->getPHID(), + ), + $this->getTaskRoles($task)); + + // Adding a milesone should remove the parent. + $this->addRoleTags($user, $task, array($stonework_1->getPHID())); + $this->assertEqual( + array( + $stonework_1->getPHID(), + ), + $this->getTaskRoles($task)); + + // Adding the parent should remove the milestone. + $this->addRoleTags($user, $task, array($stonework->getPHID())); + $this->assertEqual( + array( + $stonework->getPHID(), + ), + $this->getTaskRoles($task)); + + // First, add one milestone. + $this->addRoleTags($user, $task, array($stonework_1->getPHID())); + // Now, adding a second milestone should remove the first milestone. + $this->addRoleTags($user, $task, array($stonework_2->getPHID())); + $this->assertEqual( + array( + $stonework_2->getPHID(), + ), + $this->getTaskRoles($task)); + } + + public function testBoardMoves() { + $user = $this->createUser(); + $user->save(); + + $board = $this->createRole($user); + + $backlog = $this->addColumn($user, $board, 0); + $column = $this->addColumn($user, $board, 1); + + // New tasks should appear in the backlog. + $task1 = $this->newTask($user, array($board)); + $expect = array( + $backlog->getPHID(), + ); + $this->assertColumns($expect, $user, $board, $task1); + + // Moving a task should move it to the destination column. + $this->moveToColumn($user, $board, $task1, $backlog, $column); + $expect = array( + $column->getPHID(), + ); + $this->assertColumns($expect, $user, $board, $task1); + + // Same thing again, with a new task. + $task2 = $this->newTask($user, array($board)); + $expect = array( + $backlog->getPHID(), + ); + $this->assertColumns($expect, $user, $board, $task2); + + // Move it, too. + $this->moveToColumn($user, $board, $task2, $backlog, $column); + $expect = array( + $column->getPHID(), + ); + $this->assertColumns($expect, $user, $board, $task2); + + // Now the stuff should be in the column, in order, with the more recently + // moved task on top. + $expect = array( + $task2->getPHID(), + $task1->getPHID(), + ); + $label = pht('Simple move'); + $this->assertTasksInColumn($expect, $user, $board, $column, $label); + + // Move the second task after the first task. + $options = array( + 'afterPHIDs' => array($task1->getPHID()), + ); + $this->moveToColumn($user, $board, $task2, $column, $column, $options); + $expect = array( + $task1->getPHID(), + $task2->getPHID(), + ); + $label = pht('With afterPHIDs'); + $this->assertTasksInColumn($expect, $user, $board, $column, $label); + + // Move the second task before the first task. + $options = array( + 'beforePHIDs' => array($task1->getPHID()), + ); + $this->moveToColumn($user, $board, $task2, $column, $column, $options); + $expect = array( + $task2->getPHID(), + $task1->getPHID(), + ); + $label = pht('With beforePHIDs'); + $this->assertTasksInColumn($expect, $user, $board, $column, $label); + } + + public function testMilestoneMoves() { + $user = $this->createUser(); + $user->save(); + + $board = $this->createRole($user); + + $backlog = $this->addColumn($user, $board, 0); + + // Create a task into the backlog. + $task = $this->newTask($user, array($board)); + $expect = array( + $backlog->getPHID(), + ); + $this->assertColumns($expect, $user, $board, $task); + + $milestone = $this->createRole($user, $board, true); + + $this->addRoleTags($user, $task, array($milestone->getPHID())); + + // We just want the side effect of looking at the board: creation of the + // milestone column. + $this->loadColumns($user, $board, $task); + + $column = id(new PhabricatorRoleColumnQuery()) + ->setViewer($user) + ->withRolePHIDs(array($board->getPHID())) + ->withProxyPHIDs(array($milestone->getPHID())) + ->executeOne(); + + $this->assertTrue((bool)$column); + + // Moving the task to the milestone should have moved it to the milestone + // column. + $expect = array( + $column->getPHID(), + ); + $this->assertColumns($expect, $user, $board, $task); + + + // Move the task within the "Milestone" column. This should not affect + // the roles the task is tagged with. See T10912. + $task_a = $task; + + $task_b = $this->newTask($user, array($backlog)); + $this->moveToColumn($user, $board, $task_b, $backlog, $column); + + $a_options = array( + 'beforePHID' => $task_b->getPHID(), + ); + + $b_options = array( + 'beforePHID' => $task_a->getPHID(), + ); + + $old_roles = $this->getTaskRoles($task); + + // Move the target task to the top. + $this->moveToColumn($user, $board, $task_a, $column, $column, $a_options); + $new_roles = $this->getTaskRoles($task_a); + $this->assertEqual($old_roles, $new_roles); + + // Move the other task. + $this->moveToColumn($user, $board, $task_b, $column, $column, $b_options); + $new_roles = $this->getTaskRoles($task_a); + $this->assertEqual($old_roles, $new_roles); + + // Move the target task again. + $this->moveToColumn($user, $board, $task_a, $column, $column, $a_options); + $new_roles = $this->getTaskRoles($task_a); + $this->assertEqual($old_roles, $new_roles); + + + // Add the parent role to the task. This should move it out of the + // milestone column and into the parent's backlog. + $this->addRoleTags($user, $task, array($board->getPHID())); + $expect_columns = array( + $backlog->getPHID(), + ); + $this->assertColumns($expect_columns, $user, $board, $task); + + $new_roles = $this->getTaskRoles($task); + $expect_roles = array( + $board->getPHID(), + ); + $this->assertEqual($expect_roles, $new_roles); + } + + public function testColumnExtendedPolicies() { + $user = $this->createUser(); + $user->save(); + + $board = $this->createRole($user); + $column = $this->addColumn($user, $board, 0); + + // At first, the user should be able to view and edit the column. + $column = $this->refreshColumn($user, $column); + $this->assertTrue((bool)$column); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $user, + $column, + PhabricatorPolicyCapability::CAN_EDIT); + $this->assertTrue($can_edit); + + // Now, set the role edit policy to "Members of Role". This should + // disable editing. + $members_policy = id(new PhabricatorRoleMembersPolicyRule()) + ->getObjectPolicyFullKey(); + $board->setEditPolicy($members_policy)->save(); + + $column = $this->refreshColumn($user, $column); + $this->assertTrue((bool)$column); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $user, + $column, + PhabricatorPolicyCapability::CAN_EDIT); + $this->assertFalse($can_edit); + + // Now, join the role. This should make the column editable again. + $this->joinRole($board, $user); + + $column = $this->refreshColumn($user, $column); + $this->assertTrue((bool)$column); + + // This test has been failing randomly in a way that doesn't reproduce + // on any host, so add some extra assertions to try to nail it down. + $board = $this->refreshRole($board, $user, true); + $this->assertTrue((bool)$board); + $this->assertTrue($board->isUserMember($user->getPHID())); + + $can_view = PhabricatorPolicyFilter::hasCapability( + $user, + $column, + PhabricatorPolicyCapability::CAN_VIEW); + $this->assertTrue($can_view); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $user, + $column, + PhabricatorPolicyCapability::CAN_EDIT); + $this->assertTrue($can_edit); + } + + public function testRolePolicyRules() { + $author = $this->generateNewTestUser(); + + $role_a = PhabricatorRole::initializeNewRole($author) + ->setName('Policy A') + ->save(); + $role_b = PhabricatorRole::initializeNewRole($author) + ->setName('Policy B') + ->save(); + + $user_none = $this->generateNewTestUser(); + $user_any = $this->generateNewTestUser(); + $user_all = $this->generateNewTestUser(); + + $this->joinRole($role_a, $user_any); + $this->joinRole($role_a, $user_all); + $this->joinRole($role_b, $user_all); + + $any_policy = id(new PhabricatorPolicy()) + ->setRules( + array( + array( + 'action' => PhabricatorPolicy::ACTION_ALLOW, + 'rule' => 'PhabricatorRolesPolicyRule', + 'value' => array( + $role_a->getPHID(), + $role_b->getPHID(), + ), + ), + )) + ->save(); + + $all_policy = id(new PhabricatorPolicy()) + ->setRules( + array( + array( + 'action' => PhabricatorPolicy::ACTION_ALLOW, + 'rule' => 'PhabricatorRolesAllPolicyRule', + 'value' => array( + $role_a->getPHID(), + $role_b->getPHID(), + ), + ), + )) + ->save(); + + $any_task = ManiphestTask::initializeNewTask($author) + ->setViewPolicy($any_policy->getPHID()) + ->save(); + + $all_task = ManiphestTask::initializeNewTask($author) + ->setViewPolicy($all_policy->getPHID()) + ->save(); + + $map = array( + array( + pht('Role policy rule; user in no roles'), + $user_none, + false, + false, + ), + array( + pht('Role policy rule; user in some roles'), + $user_any, + true, + false, + ), + array( + pht('Role policy rule; user in all roles'), + $user_all, + true, + true, + ), + ); + + foreach ($map as $test_case) { + list($label, $user, $expect_any, $expect_all) = $test_case; + + $can_any = PhabricatorPolicyFilter::hasCapability( + $user, + $any_task, + PhabricatorPolicyCapability::CAN_VIEW); + + $can_all = PhabricatorPolicyFilter::hasCapability( + $user, + $all_task, + PhabricatorPolicyCapability::CAN_VIEW); + + $this->assertEqual($expect_any, $can_any, pht('%s / Any', $label)); + $this->assertEqual($expect_all, $can_all, pht('%s / All', $label)); + } + } + + + private function moveToColumn( + PhabricatorUser $viewer, + PhabricatorRole $board, + ManiphestTask $task, + PhabricatorRoleColumn $src, + PhabricatorRoleColumn $dst, + $options = null) { + + $xactions = array(); + + if (!$options) { + $options = array(); + } + + $value = array( + 'columnPHID' => $dst->getPHID(), + ) + $options; + + $xactions[] = id(new ManiphestTransaction()) + ->setTransactionType(PhabricatorTransactions::TYPE_COLUMNS) + ->setNewValue(array($value)); + + $editor = id(new ManiphestTransactionEditor()) + ->setActor($viewer) + ->setContentSource($this->newContentSource()) + ->setContinueOnNoEffect(true) + ->applyTransactions($task, $xactions); + } + + private function assertColumns( + array $expect, + PhabricatorUser $viewer, + PhabricatorRole $board, + ManiphestTask $task) { + $column_phids = $this->loadColumns($viewer, $board, $task); + $this->assertEqual($expect, $column_phids); + } + + private function loadColumns( + PhabricatorUser $viewer, + PhabricatorRole $board, + ManiphestTask $task) { + $engine = id(new PhabricatorRoleBoardLayoutEngine()) + ->setViewer($viewer) + ->setBoardPHIDs(array($board->getPHID())) + ->setObjectPHIDs( + array( + $task->getPHID(), + )) + ->executeLayout(); + + $columns = $engine->getObjectColumns($board->getPHID(), $task->getPHID()); + $column_phids = mpull($columns, 'getPHID'); + $column_phids = array_values($column_phids); + + return $column_phids; + } + + private function assertTasksInColumn( + array $expect, + PhabricatorUser $viewer, + PhabricatorRole $board, + PhabricatorRoleColumn $column, + $label = null) { + + $engine = id(new PhabricatorRoleBoardLayoutEngine()) + ->setViewer($viewer) + ->setBoardPHIDs(array($board->getPHID())) + ->setObjectPHIDs($expect) + ->executeLayout(); + + $object_phids = $engine->getColumnObjectPHIDs( + $board->getPHID(), + $column->getPHID()); + $object_phids = array_values($object_phids); + + $this->assertEqual($expect, $object_phids, $label); + } + + private function addColumn( + PhabricatorUser $viewer, + PhabricatorRole $role, + $sequence) { + + $role->setHasWorkboard(1)->save(); + + return PhabricatorRoleColumn::initializeNewColumn($viewer) + ->setSequence(0) + ->setProperty('isDefault', ($sequence == 0)) + ->setRolePHID($role->getPHID()) + ->save(); + } + + private function getTaskRoles(ManiphestTask $task) { + $role_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( + $task->getPHID(), + PhabricatorRoleObjectHasRoleEdgeType::EDGECONST); + + sort($role_phids); + + return $role_phids; + } + + private function attemptRoleEdit( + PhabricatorRole $role, + PhabricatorUser $user, + $skip_refresh = false) { + + $role = $this->refreshRole($role, $user, true); + + $new_name = $role->getName().' '.mt_rand(); + + $params = array( + 'objectIdentifier' => $role->getID(), + 'transactions' => array( + array( + 'type' => 'name', + 'value' => $new_name, + ), + ), + ); + + id(new ConduitCall('role.edit', $params)) + ->setUser($user) + ->execute(); + + return true; + } + + + private function addRoleTags( + PhabricatorUser $viewer, + ManiphestTask $task, + array $phids) { + + $xactions = array(); + + $xactions[] = id(new ManiphestTransaction()) + ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) + ->setMetadataValue( + 'edge:type', + PhabricatorRoleObjectHasRoleEdgeType::EDGECONST) + ->setNewValue( + array( + '+' => array_fuse($phids), + )); + + $editor = id(new ManiphestTransactionEditor()) + ->setActor($viewer) + ->setContentSource($this->newContentSource()) + ->setContinueOnNoEffect(true) + ->applyTransactions($task, $xactions); + } + + private function newTask( + PhabricatorUser $viewer, + array $roles, + $name = null) { + + $task = ManiphestTask::initializeNewTask($viewer); + + if ($name === null || $name === '') { + $name = pht('Test Task'); + } + + $xactions = array(); + + $xactions[] = id(new ManiphestTransaction()) + ->setTransactionType(ManiphestTaskTitleTransaction::TRANSACTIONTYPE) + ->setNewValue($name); + + if ($roles) { + $xactions[] = id(new ManiphestTransaction()) + ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) + ->setMetadataValue( + 'edge:type', + PhabricatorRoleObjectHasRoleEdgeType::EDGECONST) + ->setNewValue( + array( + '=' => array_fuse(mpull($roles, 'getPHID')), + )); + } + + $editor = id(new ManiphestTransactionEditor()) + ->setActor($viewer) + ->setContentSource($this->newContentSource()) + ->setContinueOnNoEffect(true) + ->applyTransactions($task, $xactions); + + return $task; + } + + private function assertQueryByRoles( + PhabricatorUser $viewer, + array $expect, + array $roles, + $label = null) { + + $datasource = id(new PhabricatorRoleLogicalDatasource()) + ->setViewer($viewer); + + $role_phids = mpull($roles, 'getPHID'); + $constraints = $datasource->evaluateTokens($role_phids); + + $query = id(new ManiphestTaskQuery()) + ->setViewer($viewer); + + $query->withEdgeLogicConstraints( + PhabricatorRoleObjectHasRoleEdgeType::EDGECONST, + $constraints); + + $tasks = $query->execute(); + + $expect_phids = mpull($expect, 'getTitle', 'getPHID'); + ksort($expect_phids); + + $actual_phids = mpull($tasks, 'getTitle', 'getPHID'); + ksort($actual_phids); + + $this->assertEqual($expect_phids, $actual_phids, $label); + } + + private function refreshRole( + PhabricatorRole $role, + PhabricatorUser $viewer, + $need_members = false, + $need_watchers = false) { + + $results = id(new PhabricatorRoleQuery()) + ->setViewer($viewer) + ->needMembers($need_members) + ->needWatchers($need_watchers) + ->withIDs(array($role->getID())) + ->execute(); + + if ($results) { + return head($results); + } else { + return null; + } + } + + private function refreshColumn( + PhabricatorUser $viewer, + PhabricatorRoleColumn $column) { + + $results = id(new PhabricatorRoleColumnQuery()) + ->setViewer($viewer) + ->withIDs(array($column->getID())) + ->execute(); + + if ($results) { + return head($results); + } else { + return null; + } + } + + private function createRole( + PhabricatorUser $user, + PhabricatorRole $parent = null, + $is_milestone = false) { + + $role = PhabricatorRole::initializeNewRole($user, $parent); + + $name = pht('Test Role %d', mt_rand()); + + $xactions = array(); + + $xactions[] = id(new PhabricatorRoleTransaction()) + ->setTransactionType(PhabricatorRoleNameTransaction::TRANSACTIONTYPE) + ->setNewValue($name); + + if ($parent) { + if ($is_milestone) { + $xactions[] = id(new PhabricatorRoleTransaction()) + ->setTransactionType( + PhabricatorRoleMilestoneTransaction::TRANSACTIONTYPE) + ->setNewValue($parent->getPHID()); + } else { + $xactions[] = id(new PhabricatorRoleTransaction()) + ->setTransactionType( + PhabricatorRoleParentTransaction::TRANSACTIONTYPE) + ->setNewValue($parent->getPHID()); + } + } + + $this->applyTransactions($role, $user, $xactions); + + // Force these values immediately; they are normally updated by the + // index engine. + if ($parent) { + if ($is_milestone) { + $parent->setHasMilestones(1)->save(); + } else { + $parent->setHasSubroles(1)->save(); + } + } + + return $role; + } + + private function setViewPolicy( + PhabricatorRole $role, + PhabricatorUser $user, + $policy) { + + $xactions = array(); + + $xactions[] = id(new PhabricatorRoleTransaction()) + ->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY) + ->setNewValue($policy); + + $this->applyTransactions($role, $user, $xactions); + + return $role; + } + + private function createRoleWithNewAuthor() { + $author = $this->createUser(); + $author->save(); + + $role = $this->createRole($author); + + return $role; + } + + private function createUser() { + $rand = mt_rand(); + + $user = new PhabricatorUser(); + $user->setUsername('unittestuser'.$rand); + $user->setRealName(pht('Unit Test User %d', $rand)); + + return $user; + } + + private function joinRole( + PhabricatorRole $role, + PhabricatorUser $user) { + return $this->joinOrLeaveRole($role, $user, '+'); + } + + private function leaveRole( + PhabricatorRole $role, + PhabricatorUser $user) { + return $this->joinOrLeaveRole($role, $user, '-'); + } + + private function watchRole( + PhabricatorRole $role, + PhabricatorUser $user) { + return $this->watchOrUnwatchRole($role, $user, '+'); + } + + private function unwatchRole( + PhabricatorRole $role, + PhabricatorUser $user) { + return $this->watchOrUnwatchRole($role, $user, '-'); + } + + private function joinOrLeaveRole( + PhabricatorRole $role, + PhabricatorUser $user, + $operation) { + return $this->applyRoleEdgeTransaction( + $role, + $user, + $operation, + PhabricatorRoleRoleHasMemberEdgeType::EDGECONST); + } + + private function watchOrUnwatchRole( + PhabricatorRole $role, + PhabricatorUser $user, + $operation) { + return $this->applyRoleEdgeTransaction( + $role, + $user, + $operation, + PhabricatorObjectHasWatcherEdgeType::EDGECONST); + } + + private function applyRoleEdgeTransaction( + PhabricatorRole $role, + PhabricatorUser $user, + $operation, + $edge_type) { + + $spec = array( + $operation => array($user->getPHID() => $user->getPHID()), + ); + + $xactions = array(); + $xactions[] = id(new PhabricatorRoleTransaction()) + ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) + ->setMetadataValue('edge:type', $edge_type) + ->setNewValue($spec); + + $this->applyTransactions($role, $user, $xactions); + + return $role; + } + + private function applyTransactions( + PhabricatorRole $role, + PhabricatorUser $user, + array $xactions) { + + $editor = id(new PhabricatorRoleTransactionEditor()) + ->setActor($user) + ->setContentSource($this->newContentSource()) + ->setContinueOnNoEffect(true) + ->applyTransactions($role, $xactions); + } + + +} diff --git a/src/applications/roles/application/PhabricatorRoleApplication.php b/src/applications/roles/application/PhabricatorRoleApplication.php new file mode 100644 index 0000000000..659f0f345a --- /dev/null +++ b/src/applications/roles/application/PhabricatorRoleApplication.php @@ -0,0 +1,173 @@ + array( + '(?:query/(?P[^/]+)/)?' => 'PhabricatorRoleListController', + 'filter/(?P[^/]+)/' => 'PhabricatorRoleListController', + 'archive/(?P[1-9]\d*)/' + => 'PhabricatorRoleArchiveController', + 'lock/(?P[1-9]\d*)/' + => 'PhabricatorRoleLockController', + 'members/(?P[1-9]\d*)/' + => 'PhabricatorRoleMembersViewController', + 'members/(?P[1-9]\d*)/add/' + => 'PhabricatorRoleMembersAddController', + '(?Pmembers|watchers)/(?P[1-9]\d*)/remove/' + => 'PhabricatorRoleMembersRemoveController', + 'profile/(?P[1-9]\d*)/' + => 'PhabricatorRoleProfileController', + 'view/(?P[1-9]\d*)/' + => 'PhabricatorRoleViewController', + 'picture/(?P[1-9]\d*)/' + => 'PhabricatorRoleEditPictureController', + $this->getEditRoutePattern('edit/') + => 'PhabricatorRoleEditController', + '(?P[1-9]\d*)/item/' => $this->getProfileMenuRouting( + 'PhabricatorRoleMenuItemController'), + 'subroles/(?P[1-9]\d*)/' + => 'PhabricatorRoleSubrolesController', + 'board/(?P[1-9]\d*)/'. + '(?:query/(?P[^/]+)/)?' + => 'PhabricatorRoleBoardViewController', + 'move/(?P[1-9]\d*)/' => 'PhabricatorRoleMoveController', + 'cover/' => 'PhabricatorRoleCoverController', + 'reports/(?P[1-9]\d*)/' => + 'PhabricatorRoleReportsController', + 'board/(?P[1-9]\d*)/' => array( + 'edit/(?:(?P\d+)/)?' + => 'PhabricatorRoleColumnEditController', + 'hide/(?:(?P\d+)/)?' + => 'PhabricatorRoleColumnHideController', + 'column/(?:(?P\d+)/)?' + => 'PhabricatorRoleColumnDetailController', + 'viewquery/(?P\d+)/' + => 'PhabricatorRoleColumnViewQueryController', + 'bulk/(?P\d+)/' + => 'PhabricatorRoleColumnBulkEditController', + 'bulkmove/(?P\d+)/(?Prole|column)/' + => 'PhabricatorRoleColumnBulkMoveController', + 'import/' + => 'PhabricatorRoleBoardImportController', + 'reorder/' + => 'PhabricatorRoleBoardReorderController', + 'disable/' + => 'PhabricatorRoleBoardDisableController', + 'manage/' + => 'PhabricatorRoleBoardManageController', + 'background/' + => 'PhabricatorRoleBoardBackgroundController', + 'default/(?P[^/]+)/' + => 'PhabricatorRoleBoardDefaultController', + 'filter/(?:query/(?P[^/]+)/)?' + => 'PhabricatorRoleBoardFilterController', + 'reload/' + => 'PhabricatorRoleBoardReloadController', + ), + 'column/' => array( + 'remove/(?P\d+)/' => + 'PhabricatorRoleColumnRemoveTriggerController', + ), + 'trigger/' => array( + $this->getQueryRoutePattern() => + 'PhabricatorRoleTriggerListController', + '(?P[1-9]\d*)/' => + 'PhabricatorRoleTriggerViewController', + $this->getEditRoutePattern('edit/') => + 'PhabricatorRoleTriggerEditController', + ), + 'update/(?P[1-9]\d*)/(?P[^/]+)/' + => 'PhabricatorRoleUpdateController', + 'manage/(?P[1-9]\d*)/' => 'PhabricatorRoleManageController', + '(?Pwatch|unwatch)/(?P[1-9]\d*)/' + => 'PhabricatorRoleWatchController', + 'silence/(?P[1-9]\d*)/' + => 'PhabricatorRoleSilenceController', + 'warning/(?P[1-9]\d*)/' + => 'PhabricatorRoleSubroleWarningController', + ), + ); + } + + protected function getCustomCapabilities() { + return array( + RoleCreateRolesCapability::CAPABILITY => array(), + RoleCanLockRolesCapability::CAPABILITY => array( + 'default' => PhabricatorPolicies::POLICY_ADMIN, + ), + RoleDefaultViewCapability::CAPABILITY => array( + 'caption' => pht('Default view policy for newly created roles.'), + 'template' => PhabricatorRoleRolePHIDType::TYPECONST, + 'capability' => PhabricatorPolicyCapability::CAN_VIEW, + ), + RoleDefaultEditCapability::CAPABILITY => array( + 'caption' => pht('Default edit policy for newly created roles.'), + 'template' => PhabricatorRoleRolePHIDType::TYPECONST, + 'capability' => PhabricatorPolicyCapability::CAN_EDIT, + ), + RoleDefaultJoinCapability::CAPABILITY => array( + 'caption' => pht('Default join policy for newly created roles.'), + 'template' => PhabricatorRoleRolePHIDType::TYPECONST, + 'capability' => PhabricatorPolicyCapability::CAN_JOIN, + ), + ); + } + + public function getApplicationSearchDocumentTypes() { + return array( + PhabricatorRoleRolePHIDType::TYPECONST, + ); + } + + public function getApplicationOrder() { + return 0.150; + } + + public function getHelpDocumentationArticles(PhabricatorUser $viewer) { + return array( + array( + 'name' => pht('Roles User Guide'), + 'href' => PhabricatorEnv::getDoclink('Roles User Guide'), + ), + ); + } + +} diff --git a/src/applications/roles/capability/RoleCanLockRolesCapability.php b/src/applications/roles/capability/RoleCanLockRolesCapability.php new file mode 100644 index 0000000000..e159d7b805 --- /dev/null +++ b/src/applications/roles/capability/RoleCanLockRolesCapability.php @@ -0,0 +1,16 @@ +setEngineParameter('rolePHIDs', $role_phids); + } + + protected function newChart(PhabricatorFactChart $chart, array $map) { + $viewer = $this->getViewer(); + + $map = $map + array( + 'rolePHIDs' => array(), + ); + + if ($map['rolePHIDs']) { + $roles = id(new PhabricatorRoleQuery()) + ->setViewer($viewer) + ->withPHIDs($map['rolePHIDs']) + ->execute(); + $role_phids = mpull($roles, 'getPHID'); + } else { + $role_phids = array(); + } + + $role_phid = head($role_phids); + + $functions = array(); + $stacks = array(); + + $function = $this->newFunction( + array( + 'accumulate', + array( + 'compose', + array('fact', 'tasks.open-count.assign.role', $role_phid), + array('min', 0), + ), + )); + + $function->getFunctionLabel() + ->setKey('moved-in') + ->setName(pht('Tasks Moved Into Role')) + ->setColor('rgba(128, 128, 200, 1)') + ->setFillColor('rgba(128, 128, 200, 0.15)'); + + $functions[] = $function; + + $function = $this->newFunction( + array( + 'accumulate', + array( + 'compose', + array('fact', 'tasks.open-count.status.role', $role_phid), + array('min', 0), + ), + )); + + $function->getFunctionLabel() + ->setKey('reopened') + ->setName(pht('Tasks Reopened')) + ->setColor('rgba(128, 128, 200, 1)') + ->setFillColor('rgba(128, 128, 200, 0.15)'); + + $functions[] = $function; + + $function = $this->newFunction( + array( + 'accumulate', + array('fact', 'tasks.open-count.create.role', $role_phid), + )); + + $function->getFunctionLabel() + ->setKey('created') + ->setName(pht('Tasks Created')) + ->setColor('rgba(0, 0, 200, 1)') + ->setFillColor('rgba(0, 0, 200, 0.15)'); + + $functions[] = $function; + + $function = $this->newFunction( + array( + 'accumulate', + array( + 'compose', + array('fact', 'tasks.open-count.status.role', $role_phid), + array('max', 0), + ), + )); + + $function->getFunctionLabel() + ->setKey('closed') + ->setName(pht('Tasks Closed')) + ->setColor('rgba(0, 200, 0, 1)') + ->setFillColor('rgba(0, 200, 0, 0.15)'); + + $functions[] = $function; + + $function = $this->newFunction( + array( + 'accumulate', + array( + 'compose', + array('fact', 'tasks.open-count.assign.role', $role_phid), + array('max', 0), + ), + )); + + $function->getFunctionLabel() + ->setKey('moved-out') + ->setName(pht('Tasks Moved Out of Role')) + ->setColor('rgba(128, 200, 128, 1)') + ->setFillColor('rgba(128, 200, 128, 0.15)'); + + $functions[] = $function; + + $stacks[] = array('created', 'reopened', 'moved-in'); + $stacks[] = array('closed', 'moved-out'); + + $datasets = array(); + + $dataset = id(new PhabricatorChartStackedAreaDataset()) + ->setFunctions($functions) + ->setStacks($stacks); + + $datasets[] = $dataset; + $chart->attachDatasets($datasets); + } + +} diff --git a/src/applications/roles/chart/PhabricatorRoleBurndownChartEngine.php b/src/applications/roles/chart/PhabricatorRoleBurndownChartEngine.php new file mode 100644 index 0000000000..7f0a2d61ad --- /dev/null +++ b/src/applications/roles/chart/PhabricatorRoleBurndownChartEngine.php @@ -0,0 +1,111 @@ +setEngineParameter('rolePHIDs', $role_phids); + } + + protected function newChart(PhabricatorFactChart $chart, array $map) { + $viewer = $this->getViewer(); + + $map = $map + array( + 'rolePHIDs' => array(), + ); + + if ($map['rolePHIDs']) { + $roles = id(new PhabricatorRoleQuery()) + ->setViewer($viewer) + ->withPHIDs($map['rolePHIDs']) + ->execute(); + $role_phids = mpull($roles, 'getPHID'); + } else { + $role_phids = array(); + } + + $functions = array(); + if ($role_phids) { + $open_function = $this->newFunction( + array( + 'accumulate', + array( + 'sum', + $this->newFactSum( + 'tasks.open-count.create.role', $role_phids), + $this->newFactSum( + 'tasks.open-count.status.role', $role_phids), + $this->newFactSum( + 'tasks.open-count.assign.role', $role_phids), + ), + )); + + $closed_function = $this->newFunction( + array( + 'accumulate', + $this->newFactSum('tasks.open-count.status.role', $role_phids), + )); + } else { + $open_function = $this->newFunction( + array( + 'accumulate', + array( + 'sum', + array('fact', 'tasks.open-count.create'), + array('fact', 'tasks.open-count.status'), + ), + )); + + $closed_function = $this->newFunction( + array( + 'accumulate', + array('fact', 'tasks.open-count.status'), + )); + } + + $open_function->getFunctionLabel() + ->setKey('open') + ->setName(pht('Open Tasks')) + ->setColor('rgba(0, 0, 200, 1)') + ->setFillColor('rgba(0, 0, 200, 0.15)'); + + $closed_function->getFunctionLabel() + ->setKey('closed') + ->setName(pht('Closed Tasks')) + ->setColor('rgba(0, 200, 0, 1)') + ->setFillColor('rgba(0, 200, 0, 0.15)'); + + $datasets = array(); + + $dataset = id(new PhabricatorChartStackedAreaDataset()) + ->setFunctions( + array( + $open_function, + $closed_function, + )) + ->setStacks( + array( + array('open'), + array('closed'), + )); + + $datasets[] = $dataset; + $chart->attachDatasets($datasets); + } + + private function newFactSum($fact_key, array $phids) { + $result = array(); + $result[] = 'sum'; + + foreach ($phids as $phid) { + $result[] = array('fact', $fact_key, $phid); + } + + return $result; + } + +} diff --git a/src/applications/roles/command/ProjectAddRolesEmailCommand.php b/src/applications/roles/command/ProjectAddRolesEmailCommand.php new file mode 100644 index 0000000000..cc72479e73 --- /dev/null +++ b/src/applications/roles/command/ProjectAddRolesEmailCommand.php @@ -0,0 +1,70 @@ +setViewer($viewer) + ->setAllowedTypes( + array( + PhabricatorRoleRolePHIDType::TYPECONST, + )) + ->setObjectList(implode(' ', $argv)) + ->setAllowPartialResults(true) + ->execute(); + + $xactions = array(); + + $type_role = PhabricatorRoleObjectHasRoleEdgeType::EDGECONST; + $xactions[] = $object->getApplicationTransactionTemplate() + ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) + ->setMetadataValue('edge:type', $type_role) + ->setNewValue( + array( + '+' => array_fuse($role_phids), + )); + + return $xactions; + } + +} diff --git a/src/applications/roles/conduit/RoleColumnSearchConduitAPIMethod.php b/src/applications/roles/conduit/RoleColumnSearchConduitAPIMethod.php new file mode 100644 index 0000000000..c280aefd0b --- /dev/null +++ b/src/applications/roles/conduit/RoleColumnSearchConduitAPIMethod.php @@ -0,0 +1,18 @@ +buildRoleInfoDictionaries(array($role)); + return idx($results, $role->getPHID()); + } + + protected function buildRoleInfoDictionaries(array $roles) { + assert_instances_of($roles, 'PhabricatorRole'); + if (!$roles) { + return array(); + } + + $result = array(); + foreach ($roles as $role) { + + $member_phids = $role->getMemberPHIDs(); + $member_phids = array_values($member_phids); + + $role_slugs = $role->getSlugs(); + $role_slugs = array_values(mpull($role_slugs, 'getSlug')); + + $role_icon = $role->getDisplayIconKey(); + + $result[$role->getPHID()] = array( + 'id' => $role->getID(), + 'phid' => $role->getPHID(), + 'name' => $role->getName(), + 'profileImagePHID' => $role->getProfileImagePHID(), + 'icon' => $role_icon, + 'color' => $role->getColor(), + 'members' => $member_phids, + 'slugs' => $role_slugs, + 'dateCreated' => $role->getDateCreated(), + 'dateModified' => $role->getDateModified(), + ); + } + + return $result; + } + +} diff --git a/src/applications/roles/conduit/RoleCreateConduitAPIMethod.php b/src/applications/roles/conduit/RoleCreateConduitAPIMethod.php new file mode 100644 index 0000000000..629240c4da --- /dev/null +++ b/src/applications/roles/conduit/RoleCreateConduitAPIMethod.php @@ -0,0 +1,94 @@ + 'required string', + 'members' => 'optional list', + 'icon' => 'optional string', + 'color' => 'optional string', + 'tags' => 'optional list', + ); + } + + protected function defineReturnType() { + return 'dict'; + } + + protected function execute(ConduitAPIRequest $request) { + $user = $request->getUser(); + + $this->requireApplicationCapability( + RoleCreateRolesCapability::CAPABILITY, + $user); + + $role = PhabricatorRole::initializeNewRole($user); + $type_name = PhabricatorRoleNameTransaction::TRANSACTIONTYPE; + $members = $request->getValue('members'); + $xactions = array(); + + $xactions[] = id(new PhabricatorRoleTransaction()) + ->setTransactionType($type_name) + ->setNewValue($request->getValue('name')); + + if ($request->getValue('icon')) { + $xactions[] = id(new PhabricatorRoleTransaction()) + ->setTransactionType( + PhabricatorRoleIconTransaction::TRANSACTIONTYPE) + ->setNewValue($request->getValue('icon')); + } + + if ($request->getValue('color')) { + $xactions[] = id(new PhabricatorRoleTransaction()) + ->setTransactionType( + PhabricatorRoleColorTransaction::TRANSACTIONTYPE) + ->setNewValue($request->getValue('color')); + } + + if ($request->getValue('tags')) { + $xactions[] = id(new PhabricatorRoleTransaction()) + ->setTransactionType( + PhabricatorRoleSlugsTransaction::TRANSACTIONTYPE) + ->setNewValue($request->getValue('tags')); + } + + $xactions[] = id(new PhabricatorRoleTransaction()) + ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) + ->setMetadataValue( + 'edge:type', + PhabricatorRoleRoleHasMemberEdgeType::EDGECONST) + ->setNewValue( + array( + '+' => array_fuse($members), + )); + + $editor = id(new PhabricatorRoleTransactionEditor()) + ->setActor($user) + ->setContinueOnNoEffect(true) + ->setContentSource($request->newContentSource()); + + $editor->applyTransactions($role, $xactions); + + return $this->buildRoleInfoDictionary($role); + } + +} diff --git a/src/applications/roles/conduit/RoleEditConduitAPIMethod.php b/src/applications/roles/conduit/RoleEditConduitAPIMethod.php new file mode 100644 index 0000000000..2292341f7d --- /dev/null +++ b/src/applications/roles/conduit/RoleEditConduitAPIMethod.php @@ -0,0 +1,19 @@ +formatStringConstants($statuses); + + return array( + 'ids' => 'optional list', + 'names' => 'optional list', + 'phids' => 'optional list', + 'slugs' => 'optional list', + 'icons' => 'optional list', + 'colors' => 'optional list', + 'status' => 'optional '.$status_const, + + 'members' => 'optional list', + + 'limit' => 'optional int', + 'offset' => 'optional int', + ); + } + + protected function defineReturnType() { + return 'list'; + } + + protected function execute(ConduitAPIRequest $request) { + $query = new PhabricatorRoleQuery(); + $query->setViewer($request->getUser()); + $query->needMembers(true); + $query->needSlugs(true); + + $ids = $request->getValue('ids'); + if ($ids) { + $query->withIDs($ids); + } + + $names = $request->getValue('names'); + if ($names) { + $query->withNames($names); + } + + $status = $request->getValue('status'); + if ($status) { + $query->withStatus($status); + } + + $phids = $request->getValue('phids'); + if ($phids) { + $query->withPHIDs($phids); + } + + $slugs = $request->getValue('slugs'); + if ($slugs) { + $query->withSlugs($slugs); + } + + $request->getValue('icons'); + if ($request->getValue('icons')) { + $icons = array(); + $query->withIcons($icons); + } + + $colors = $request->getValue('colors'); + if ($colors) { + $query->withColors($colors); + } + + $members = $request->getValue('members'); + if ($members) { + $query->withMemberPHIDs($members); + } + + $limit = $request->getValue('limit'); + if ($limit) { + $query->setLimit($limit); + } + + $offset = $request->getValue('offset'); + if ($offset) { + $query->setOffset($offset); + } + + $pager = $this->newPager($request); + $results = $query->executeWithCursorPager($pager); + $roles = $this->buildRoleInfoDictionaries($results); + + // TODO: This is pretty hideous. + $slug_map = array(); + if ($slugs) { + foreach ($slugs as $slug) { + $normal = PhabricatorSlug::normalizeProjectSlug($slug); + foreach ($roles as $role) { + if (in_array($normal, $role['slugs'])) { + $slug_map[$slug] = $role['phid']; + } + } + } + } + + $result = array( + 'data' => $roles, + 'slugMap' => $slug_map, + ); + + return $this->addPagerResults($result, $pager); + } + +} diff --git a/src/applications/roles/conduit/RoleSearchConduitAPIMethod.php b/src/applications/roles/conduit/RoleSearchConduitAPIMethod.php new file mode 100644 index 0000000000..df68bff9e4 --- /dev/null +++ b/src/applications/roles/conduit/RoleSearchConduitAPIMethod.php @@ -0,0 +1,24 @@ + $query->getSlugMap(), + ); + } + +} diff --git a/src/applications/roles/config/PhabricatorRoleColorsConfigType.php b/src/applications/roles/config/PhabricatorRoleColorsConfigType.php new file mode 100644 index 0000000000..4d4cb2218f --- /dev/null +++ b/src/applications/roles/config/PhabricatorRoleColorsConfigType.php @@ -0,0 +1,14 @@ +deformat(pht(<< Icons and Images}. + +Configure a list of icon specifications. Each icon specification should be +a dictionary, which may contain these keys: + + - `key` //Required string.// Internal key identifying the icon. + - `name` //Required string.// Human-readable icon name. + - `icon` //Required string.// Specifies which actual icon image to use. + - `image` //Optional string.// Selects a default image. Select an image from + `resources/builtins/roles/`. + - `default` //Optional bool.// Selects a default icon. Exactly one icon must + be selected as the default. + - `disabled` //Optional bool.// If true, this icon will no longer be + available for selection when creating or editing roles. + - `special` //Optional string.// Marks an icon as a special icon: + - `milestone` This is the icon for milestones. Exactly one icon must be + selected as the milestone icon. + +You can look at the default configuration below for an example of a valid +configuration. +EOTEXT + )); + + $default_colors = PhabricatorRoleIconSet::getDefaultColorMap(); + $colors_type = 'role.colors'; + + $colors_description = $this->deformat(pht(<< true, + ); + + foreach ($default_fields as $key => $enabled) { + $default_fields[$key] = array( + 'disabled' => !$enabled, + ); + } + + $custom_field_type = 'custom:PhabricatorCustomFieldConfigOptionType'; + + + $subtype_type = 'roles.subtypes'; + $subtype_default_key = PhabricatorEditEngineSubtype::SUBTYPE_DEFAULT; + $subtype_example = array( + array( + 'key' => $subtype_default_key, + 'name' => pht('Role'), + ), + array( + 'key' => 'team', + 'name' => pht('Team'), + ), + ); + $subtype_example = id(new PhutilJSON())->encodeAsList($subtype_example); + + $subtype_default = array( + array( + 'key' => $subtype_default_key, + 'name' => pht('Role'), + ), + ); + + $subtype_description = $this->deformat(pht(<<newOption('roles.custom-field-definitions', 'wild', array()) + ->setSummary(pht('Custom Roles fields.')) + ->setDescription( + pht( + 'Array of custom fields for Roles.')) + ->addExample( + '{"mycompany:motto": {"name": "Role Motto", '. + '"type": "text"}}', + pht('Valid Setting')), + $this->newOption('roles.fields', $custom_field_type, $default_fields) + ->setCustomData(id(new PhabricatorRole())->getCustomFieldBaseClass()) + ->setDescription(pht('Select and reorder role fields.')), + $this->newOption('roles.icons', $icons_type, $default_icons) + ->setSummary(pht('Adjust role icons.')) + ->setDescription($icons_description), + $this->newOption('roles.colors', $colors_type, $default_colors) + ->setSummary(pht('Adjust role colors.')) + ->setDescription($colors_description), + $this->newOption('roles.subtypes', $subtype_type, $subtype_default) + ->setSummary(pht('Define role subtypes.')) + ->setDescription($subtype_description) + ->addExample($subtype_example, pht('Simple Subtypes')), + + ); + } + +} diff --git a/src/applications/roles/config/PhabricatorRoleIconsConfigType.php b/src/applications/roles/config/PhabricatorRoleIconsConfigType.php new file mode 100644 index 0000000000..84ea98e93e --- /dev/null +++ b/src/applications/roles/config/PhabricatorRoleIconsConfigType.php @@ -0,0 +1,14 @@ + pht('Active'), + self::STATUS_ARCHIVED => pht('Archived'), + ); + + return idx($map, coalesce($status, '?'), pht('Unknown')); + } + + public static function getStatusMap() { + return array( + self::STATUS_ACTIVE => pht('Active'), + self::STATUS_ARCHIVED => pht('Archived'), + ); + } + +} diff --git a/src/applications/roles/constants/PhabricatorRoleWorkboardBackgroundColor.php b/src/applications/roles/constants/PhabricatorRoleWorkboardBackgroundColor.php new file mode 100644 index 0000000000..e0f6fef747 --- /dev/null +++ b/src/applications/roles/constants/PhabricatorRoleWorkboardBackgroundColor.php @@ -0,0 +1,124 @@ + '', + 'name' => pht('Use Parent Background (Default)'), + 'special' => 'parent', + 'icon' => 'fa-chevron-circle-up', + 'group' => 'basic', + ), + array( + 'key' => 'none', + 'name' => pht('No Background'), + 'special' => 'none', + 'icon' => 'fa-ban', + 'group' => 'basic', + ), + array( + 'key' => 'red', + 'name' => pht('Red'), + ), + array( + 'key' => 'orange', + 'name' => pht('Orange'), + ), + array( + 'key' => 'yellow', + 'name' => pht('Yellow'), + ), + array( + 'key' => 'green', + 'name' => pht('Green'), + ), + array( + 'key' => 'blue', + 'name' => pht('Blue'), + ), + array( + 'key' => 'indigo', + 'name' => pht('Indigo'), + ), + array( + 'key' => 'violet', + 'name' => pht('Violet'), + ), + array( + 'key' => 'sky', + 'name' => pht('Sky'), + ), + array( + 'key' => 'pink', + 'name' => pht('Pink'), + ), + array( + 'key' => 'fire', + 'name' => pht('Fire'), + ), + array( + 'key' => 'grey', + 'name' => pht('Grey'), + ), + array( + 'key' => 'gradient-red', + 'name' => pht('Ripe Peach'), + ), + array( + 'key' => 'gradient-orange', + 'name' => pht('Ripe Orange'), + ), + array( + 'key' => 'gradient-yellow', + 'name' => pht('Ripe Mango'), + ), + array( + 'key' => 'gradient-green', + 'name' => pht('Shallows'), + ), + array( + 'key' => 'gradient-blue', + 'name' => pht('Reef'), + ), + array( + 'key' => 'gradient-bluegrey', + 'name' => pht('Depths'), + ), + array( + 'key' => 'gradient-indigo', + 'name' => pht('This One Is Purple'), + ), + array( + 'key' => 'gradient-violet', + 'name' => pht('Unripe Plum'), + ), + array( + 'key' => 'gradient-sky', + 'name' => pht('Blue Sky'), + ), + array( + 'key' => 'gradient-pink', + 'name' => pht('Intensity'), + ), + array( + 'key' => 'gradient-grey', + 'name' => pht('Into The Expanse'), + ), + ); + + foreach ($options as $key => $option) { + if (empty($option['group'])) { + if (preg_match('/^gradient/', $option['key'])) { + $option['group'] = 'gradient'; + } else { + $option['group'] = 'solid'; + } + } + $options[$key] = $option; + } + + return ipull($options, null, 'key'); + } +} diff --git a/src/applications/roles/controller/PhabricatorRoleArchiveController.php b/src/applications/roles/controller/PhabricatorRoleArchiveController.php new file mode 100644 index 0000000000..912e255945 --- /dev/null +++ b/src/applications/roles/controller/PhabricatorRoleArchiveController.php @@ -0,0 +1,69 @@ +getViewer(); + $id = $request->getURIData('id'); + + $role = id(new PhabricatorRoleQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$role) { + return new Aphront404Response(); + } + + $edit_uri = $this->getApplicationURI('manage/'.$role->getID().'/'); + + if ($request->isFormPost()) { + if ($role->isArchived()) { + $new_status = PhabricatorRoleStatus::STATUS_ACTIVE; + } else { + $new_status = PhabricatorRoleStatus::STATUS_ARCHIVED; + } + + $xactions = array(); + + $xactions[] = id(new PhabricatorRoleTransaction()) + ->setTransactionType( + PhabricatorRoleStatusTransaction::TRANSACTIONTYPE) + ->setNewValue($new_status); + + id(new PhabricatorRoleTransactionEditor()) + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true) + ->applyTransactions($role, $xactions); + + return id(new AphrontRedirectResponse())->setURI($edit_uri); + } + + if ($role->isArchived()) { + $title = pht('Really activate role?'); + $body = pht('This role will become active again.'); + $button = pht('Activate Role'); + } else { + $title = pht('Really archive role?'); + $body = pht('This role will be moved to the archive.'); + $button = pht('Archive Role'); + } + + $dialog = id(new AphrontDialogView()) + ->setUser($viewer) + ->setTitle($title) + ->appendChild($body) + ->addCancelButton($edit_uri) + ->addSubmitButton($button); + + return id(new AphrontDialogResponse())->setDialog($dialog); + } + +} diff --git a/src/applications/roles/controller/PhabricatorRoleBoardBackgroundController.php b/src/applications/roles/controller/PhabricatorRoleBoardBackgroundController.php new file mode 100644 index 0000000000..facdb5ee6a --- /dev/null +++ b/src/applications/roles/controller/PhabricatorRoleBoardBackgroundController.php @@ -0,0 +1,172 @@ +getUser(); + $board_id = $request->getURIData('roleID'); + + $board = id(new PhabricatorRoleQuery()) + ->setViewer($viewer) + ->withIDs(array($board_id)) + ->needImages(true) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$board) { + return new Aphront404Response(); + } + + if (!$board->getHasWorkboard()) { + return new Aphront404Response(); + } + + $this->setRole($board); + $id = $board->getID(); + + $view_uri = $this->getApplicationURI("board/{$id}/"); + $manage_uri = $this->getApplicationURI("board/{$id}/manage/"); + + if ($request->isFormPost()) { + $background_key = $request->getStr('backgroundKey'); + + $xactions = array(); + + $xactions[] = id(new PhabricatorRoleTransaction()) + ->setTransactionType( + PhabricatorRoleWorkboardBackgroundTransaction::TRANSACTIONTYPE) + ->setNewValue($background_key); + + id(new PhabricatorRoleTransactionEditor()) + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true) + ->applyTransactions($board, $xactions); + + return id(new AphrontRedirectResponse()) + ->setURI($view_uri); + } + + $nav = $this->newNavigation( + $board, + PhabricatorRole::ITEM_WORKBOARD); + + $crumbs = id($this->buildApplicationCrumbs()) + ->addTextCrumb(pht('Workboard'), $board->getWorkboardURI()) + ->addTextCrumb(pht('Manage'), $manage_uri) + ->addTextCrumb(pht('Background Color')); + + $form = id(new AphrontFormView()) + ->setUser($viewer); + + $group_info = array( + 'basic' => array( + 'label' => pht('Basics'), + ), + 'solid' => array( + 'label' => pht('Solid Colors'), + ), + 'gradient' => array( + 'label' => pht('Gradients'), + ), + ); + + $groups = array(); + $options = PhabricatorRoleWorkboardBackgroundColor::getOptions(); + $option_groups = igroup($options, 'group'); + + require_celerity_resource('people-profile-css'); + require_celerity_resource('phui-workboard-color-css'); + Javelin::initBehavior('phabricator-tooltips', array()); + + foreach ($group_info as $group_key => $spec) { + $buttons = array(); + + $available_options = idx($option_groups, $group_key, array()); + foreach ($available_options as $option) { + $buttons[] = $this->renderOptionButton($option); + } + + $form->appendControl( + id(new AphrontFormMarkupControl()) + ->setLabel($spec['label']) + ->setValue($buttons)); + } + + // NOTE: Each button is its own form, so we can't wrap them in a normal + // form. + $layout_view = $form->buildLayoutView(); + + $form_box = id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Edit Background Color')) + ->appendChild($layout_view); + + return $this->newPage() + ->setTitle( + array( + pht('Edit Background Color'), + $board->getDisplayName(), + )) + ->setCrumbs($crumbs) + ->setNavigation($nav) + ->appendChild($form_box); + } + + private function renderOptionButton(array $option) { + $viewer = $this->getViewer(); + + $icon = idx($option, 'icon'); + if ($icon) { + $preview_class = null; + $preview_content = id(new PHUIIconView()) + ->setIcon($icon, 'lightbluetext'); + } else { + $preview_class = 'phui-workboard-'.$option['key']; + $preview_content = null; + } + + $preview = phutil_tag( + 'div', + array( + 'class' => 'phui-workboard-color-preview '.$preview_class, + ), + $preview_content); + + $button = javelin_tag( + 'button', + array( + 'class' => 'button-grey profile-image-button', + 'sigil' => 'has-tooltip', + 'meta' => array( + 'tip' => $option['name'], + 'size' => 300, + ), + ), + $preview); + + $input = phutil_tag( + 'input', + array( + 'type' => 'hidden', + 'name' => 'backgroundKey', + 'value' => $option['key'], + )); + + return phabricator_form( + $viewer, + array( + 'class' => 'profile-image-form', + 'method' => 'POST', + ), + array( + $button, + $input, + )); + } + +} diff --git a/src/applications/roles/controller/PhabricatorRoleBoardController.php b/src/applications/roles/controller/PhabricatorRoleBoardController.php new file mode 100644 index 0000000000..5658ee7980 --- /dev/null +++ b/src/applications/roles/controller/PhabricatorRoleBoardController.php @@ -0,0 +1,36 @@ +viewState === null) { + $this->viewState = $this->newViewState(); + } + + return $this->viewState; + } + + private function newViewState() { + $role = $this->getRole(); + $request = $this->getRequest(); + + return id(new PhabricatorRoleWorkboardViewState()) + ->setRole($role) + ->readFromRequest($request); + } + + final protected function newWorkboardDialog() { + $dialog = $this->newDialog(); + + $state = $this->getViewState(); + foreach ($state->getQueryParameters() as $key => $value) { + $dialog->addHiddenInput($key, $value); + } + + return $dialog; + } + +} diff --git a/src/applications/roles/controller/PhabricatorRoleBoardDefaultController.php b/src/applications/roles/controller/PhabricatorRoleBoardDefaultController.php new file mode 100644 index 0000000000..1b3beacbed --- /dev/null +++ b/src/applications/roles/controller/PhabricatorRoleBoardDefaultController.php @@ -0,0 +1,81 @@ +getViewer(); + + $response = $this->loadRoleForEdit(); + if ($response) { + return $response; + } + + $role = $this->getRole(); + $state = $this->getViewState(); + $board_uri = $state->newWorkboardURI(); + $remove_param = null; + + $target = $request->getURIData('target'); + switch ($target) { + case 'filter': + $title = pht('Set Board Default Filter'); + $body = pht( + 'Make the current filter the new default filter for this board? '. + 'All users will see the new filter as the default when they view '. + 'the board.'); + $button = pht('Save Default Filter'); + + $xaction_value = $state->getQueryKey(); + $xaction_type = PhabricatorRoleFilterTransaction::TRANSACTIONTYPE; + + $remove_param = 'filter'; + break; + case 'sort': + $title = pht('Set Board Default Order'); + $body = pht( + 'Make the current sort order the new default order for this board? '. + 'All users will see the new order as the default when they view '. + 'the board.'); + $button = pht('Save Default Order'); + + $xaction_value = $state->getOrder(); + $xaction_type = PhabricatorRoleSortTransaction::TRANSACTIONTYPE; + + $remove_param = 'order'; + break; + default: + return new Aphront404Response(); + } + + $id = $role->getID(); + + if ($request->isFormPost()) { + $xactions = array(); + + $xactions[] = id(new PhabricatorRoleTransaction()) + ->setTransactionType($xaction_type) + ->setNewValue($xaction_value); + + id(new PhabricatorRoleTransactionEditor()) + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true) + ->applyTransactions($role, $xactions); + + // If the parameter we just modified is present in the query string, + // throw it away so the user is redirected back to the default view of + // the board, allowing them to see the new default behavior. + $board_uri->removeQueryParam($remove_param); + + return id(new AphrontRedirectResponse())->setURI($board_uri); + } + + return $this->newWorkboardDialog() + ->setTitle($title) + ->appendChild($body) + ->addCancelButton($board_uri) + ->addSubmitButton($title); + } +} diff --git a/src/applications/roles/controller/PhabricatorRoleBoardDisableController.php b/src/applications/roles/controller/PhabricatorRoleBoardDisableController.php new file mode 100644 index 0000000000..ae0bd9b914 --- /dev/null +++ b/src/applications/roles/controller/PhabricatorRoleBoardDisableController.php @@ -0,0 +1,62 @@ +getUser(); + $role_id = $request->getURIData('roleID'); + + $role = id(new PhabricatorRoleQuery()) + ->setViewer($viewer) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->withIDs(array($role_id)) + ->executeOne(); + if (!$role) { + return new Aphront404Response(); + } + + if (!$role->getHasWorkboard()) { + return new Aphront404Response(); + } + + $this->setRole($role); + $id = $role->getID(); + + $board_uri = $this->getApplicationURI("board/{$id}/"); + + if ($request->isFormPost()) { + $xactions = array(); + + $xactions[] = id(new PhabricatorRoleTransaction()) + ->setTransactionType( + PhabricatorRoleWorkboardTransaction::TRANSACTIONTYPE) + ->setNewValue(0); + + id(new PhabricatorRoleTransactionEditor()) + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true) + ->applyTransactions($role, $xactions); + + return id(new AphrontRedirectResponse()) + ->setURI($board_uri); + } + + return $this->newDialog() + ->setTitle(pht('Disable Workboard')) + ->appendParagraph( + pht( + 'Disabling a workboard hides the board. Objects on the board '. + 'will no longer be annotated with column names in other '. + 'applications. You can restore the workboard later.')) + ->addCancelButton($board_uri) + ->addSubmitButton(pht('Disable Workboard')); + } + +} diff --git a/src/applications/roles/controller/PhabricatorRoleBoardFilterController.php b/src/applications/roles/controller/PhabricatorRoleBoardFilterController.php new file mode 100644 index 0000000000..b22f55e0bd --- /dev/null +++ b/src/applications/roles/controller/PhabricatorRoleBoardFilterController.php @@ -0,0 +1,56 @@ +getViewer(); + + $response = $this->loadRole(); + if ($response) { + return $response; + } + + $role = $this->getRole(); + $state = $this->getViewState(); + $board_uri = $state->newWorkboardURI(); + + $search_engine = $state->getSearchEngine(); + + $is_submit = $request->isFormPost(); + + if ($is_submit) { + $saved_query = $search_engine->buildSavedQueryFromRequest($request); + $search_engine->saveQuery($saved_query); + } else { + $saved_query = $state->getSavedQuery(); + if (!$saved_query) { + return new Aphront404Response(); + } + } + + $filter_form = id(new AphrontFormView()) + ->setUser($viewer); + + $search_engine->buildSearchForm($filter_form, $saved_query); + + $errors = $search_engine->getErrors(); + + if ($is_submit && !$errors) { + $query_key = $saved_query->getQueryKey(); + + $state->setQueryKey($query_key); + $board_uri = $state->newWorkboardURI(); + + return id(new AphrontRedirectResponse())->setURI($board_uri); + } + + return $this->newWorkboardDialog() + ->setWidth(AphrontDialogView::WIDTH_FULL) + ->setTitle(pht('Advanced Filter')) + ->appendChild($filter_form->buildLayoutView()) + ->setErrors($errors) + ->addSubmitButton(pht('Apply Filter')) + ->addCancelButton($board_uri); + } +} diff --git a/src/applications/roles/controller/PhabricatorRoleBoardImportController.php b/src/applications/roles/controller/PhabricatorRoleBoardImportController.php new file mode 100644 index 0000000000..b0c3281ae3 --- /dev/null +++ b/src/applications/roles/controller/PhabricatorRoleBoardImportController.php @@ -0,0 +1,113 @@ +getViewer(); + $role_id = $request->getURIData('roleID'); + + $role = id(new PhabricatorRoleQuery()) + ->setViewer($viewer) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->withIDs(array($role_id)) + ->executeOne(); + if (!$role) { + return new Aphront404Response(); + } + $this->setRole($role); + + $role_id = $role->getID(); + $board_uri = $this->getApplicationURI("board/{$role_id}/"); + + // See PHI1025. We only want to prevent the import if the board already has + // real columns. If it has proxy columns (for example, for milestones) you + // can still import columns from another board. + $columns = id(new PhabricatorRoleColumnQuery()) + ->setViewer($viewer) + ->withRolePHIDs(array($role->getPHID())) + ->withIsProxyColumn(false) + ->execute(); + if ($columns) { + return $this->newDialog() + ->setTitle(pht('Workboard Already Has Columns')) + ->appendParagraph( + pht( + 'You can not import columns into this workboard because it '. + 'already has columns. You can only import into an empty '. + 'workboard.')) + ->addCancelButton($board_uri); + } + + if ($request->isFormPost()) { + $import_phid = $request->getArr('importRolePHID'); + $import_phid = reset($import_phid); + + $import_columns = id(new PhabricatorRoleColumnQuery()) + ->setViewer($viewer) + ->withRolePHIDs(array($import_phid)) + ->withIsProxyColumn(false) + ->execute(); + if (!$import_columns) { + return $this->newDialog() + ->setTitle(pht('Source Workboard Has No Columns')) + ->appendParagraph( + pht( + 'You can not import columns from that workboard because it has '. + 'no importable columns.')) + ->addCancelButton($board_uri); + } + + $table = id(new PhabricatorRoleColumn()) + ->openTransaction(); + foreach ($import_columns as $import_column) { + if ($import_column->isHidden()) { + continue; + } + + $new_column = PhabricatorRoleColumn::initializeNewColumn($viewer) + ->setSequence($import_column->getSequence()) + ->setRolePHID($role->getPHID()) + ->setName($import_column->getName()) + ->setProperties($import_column->getProperties()) + ->save(); + } + $xactions = array(); + $xactions[] = id(new PhabricatorRoleTransaction()) + ->setTransactionType( + PhabricatorRoleWorkboardTransaction::TRANSACTIONTYPE) + ->setNewValue(1); + + id(new PhabricatorRoleTransactionEditor()) + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true) + ->applyTransactions($role, $xactions); + + $table->saveTransaction(); + + return id(new AphrontRedirectResponse())->setURI($board_uri); + } + + $role_selector = id(new AphrontFormTokenizerControl()) + ->setName('importRolePHID') + ->setUser($viewer) + ->setDatasource(id(new PhabricatorRoleDatasource()) + ->setParameters(array('mustHaveColumns' => true)) + ->setLimit(1)); + + return $this->newDialog() + ->setTitle(pht('Import Columns')) + ->setWidth(AphrontDialogView::WIDTH_FORM) + ->appendParagraph(pht('Choose a role to import columns from:')) + ->appendChild($role_selector) + ->addCancelButton($board_uri) + ->addSubmitButton(pht('Import')); + } + +} diff --git a/src/applications/roles/controller/PhabricatorRoleBoardManageController.php b/src/applications/roles/controller/PhabricatorRoleBoardManageController.php new file mode 100644 index 0000000000..e3e6723e4d --- /dev/null +++ b/src/applications/roles/controller/PhabricatorRoleBoardManageController.php @@ -0,0 +1,144 @@ +getViewer(); + $board_id = $request->getURIData('roleID'); + + $board = id(new PhabricatorRoleQuery()) + ->setViewer($viewer) + ->withIDs(array($board_id)) + ->needImages(true) + ->executeOne(); + if (!$board) { + return new Aphront404Response(); + } + $this->setRole($board); + + // Perform layout of no tasks to load and populate the columns in the + // correct order. + $layout_engine = id(new PhabricatorRoleBoardLayoutEngine()) + ->setViewer($viewer) + ->setBoardPHIDs(array($board->getPHID())) + ->setObjectPHIDs(array()) + ->setFetchAllBoards(true) + ->executeLayout(); + + $columns = $layout_engine->getColumns($board->getPHID()); + + $board_id = $board->getID(); + + $header = $this->buildHeaderView($board); + $curtain = $this->buildCurtainView($board); + + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb(pht('Workboard'), $board->getWorkboardURI()); + $crumbs->addTextCrumb(pht('Manage')); + $crumbs->setBorder(true); + + $nav = $this->newNavigation( + $board, + PhabricatorRole::ITEM_WORKBOARD); + $columns_list = $this->buildColumnsList($board, $columns); + + require_celerity_resource('project-view-css'); + + $view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->addClass('project-view-home') + ->addClass('project-view-people-home') + ->setCurtain($curtain) + ->setMainColumn($columns_list); + + $title = array( + pht('Manage Workboard'), + $board->getDisplayName(), + ); + + return $this->newPage() + ->setTitle($title) + ->setNavigation($nav) + ->setCrumbs($crumbs) + ->appendChild($view); + } + + private function buildHeaderView(PhabricatorRole $board) { + $viewer = $this->getViewer(); + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Workboard: %s', $board->getDisplayName())) + ->setUser($viewer); + + return $header; + } + + private function buildCurtainView(PhabricatorRole $board) { + $viewer = $this->getViewer(); + $id = $board->getID(); + + $curtain = $this->newCurtainView(); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $board, + PhabricatorPolicyCapability::CAN_EDIT); + + $disable_uri = $this->getApplicationURI("board/{$id}/disable/"); + + $curtain->addAction( + id(new PhabricatorActionView()) + ->setIcon('fa-ban') + ->setName(pht('Disable Workboard')) + ->setHref($disable_uri) + ->setDisabled(!$can_edit) + ->setWorkflow(true)); + + return $curtain; + } + + private function buildColumnsList( + PhabricatorRole $board, + array $columns) { + assert_instances_of($columns, 'PhabricatorRoleColumn'); + + $board_id = $board->getID(); + + $view = id(new PHUIObjectItemListView()) + ->setNoDataString(pht('This board has no columns.')); + + foreach ($columns as $column) { + $column_id = $column->getID(); + + $proxy = $column->getProxy(); + if ($proxy && !$proxy->isMilestone()) { + continue; + } + + $detail_uri = "/role/board/{$board_id}/column/{$column_id}/"; + + $item = id(new PHUIObjectItemView()) + ->setHeader($column->getDisplayName()) + ->setHref($detail_uri); + + if ($column->isHidden()) { + $item->setDisabled(true); + $item->addAttribute(pht('Hidden')); + $item->setImageIcon('fa-columns grey'); + } else { + $item->addAttribute(pht('Visible')); + $item->setImageIcon('fa-columns'); + } + + $view->addItem($item); + } + + return id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Columns')) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->setObjectList($view); + } + + +} diff --git a/src/applications/roles/controller/PhabricatorRoleBoardReloadController.php b/src/applications/roles/controller/PhabricatorRoleBoardReloadController.php new file mode 100644 index 0000000000..f06e6a6da7 --- /dev/null +++ b/src/applications/roles/controller/PhabricatorRoleBoardReloadController.php @@ -0,0 +1,73 @@ +getViewer(); + + $response = $this->loadRole(); + if ($response) { + return $response; + } + + $order = $request->getStr('order'); + if ($order === null || $order === '') { + $order = PhabricatorRoleColumnNaturalOrder::ORDERKEY; + } + + $ordering = PhabricatorRoleColumnOrder::getOrderByKey($order); + $ordering = id(clone $ordering) + ->setViewer($viewer); + + $role = $this->getRole(); + $state = $this->getViewState(); + $board_uri = $state->newWorkboardURI(); + + $layout_engine = $state->getLayoutEngine(); + + $board_phid = $role->getPHID(); + + $objects = $state->getObjects(); + $objects = mpull($objects, null, 'getPHID'); + + try { + $client_state = $request->getStr('state'); + $client_state = phutil_json_decode($client_state); + } catch (PhutilJSONParserException $ex) { + $client_state = array(); + } + + // Figure out which objects need to be updated: either the client has an + // out-of-date version of them (objects which have been edited); or they + // exist on the client but not on the server (objects which have been + // removed from the board); or they exist on the server but not on the + // client (objects which have been added to the board). + + $update_objects = array(); + foreach ($objects as $object_phid => $object) { + + // TODO: For now, this is always hard-coded. + $object_version = 2; + + $client_version = idx($client_state, $object_phid, 0); + if ($object_version > $client_version) { + $update_objects[$object_phid] = $object; + } + } + + $update_phids = array_keys($update_objects); + $visible_phids = array_keys($client_state); + + $engine = id(new PhabricatorRoleBoardResponseEngine()) + ->setViewer($viewer) + ->setBoardPHID($board_phid) + ->setOrdering($ordering) + ->setObjects($objects) + ->setUpdatePHIDs($update_phids) + ->setVisiblePHIDs($visible_phids); + + return $engine->buildResponse(); + } + +} diff --git a/src/applications/roles/controller/PhabricatorRoleBoardReorderController.php b/src/applications/roles/controller/PhabricatorRoleBoardReorderController.php new file mode 100644 index 0000000000..9d0534a530 --- /dev/null +++ b/src/applications/roles/controller/PhabricatorRoleBoardReorderController.php @@ -0,0 +1,145 @@ +getViewer(); + $roleid = $request->getURIData('roleID'); + + $role = id(new PhabricatorRoleQuery()) + ->setViewer($viewer) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->withIDs(array($roleid)) + ->executeOne(); + if (!$role) { + return new Aphront404Response(); + } + + $this->setRole($role); + $role_id = $role->getID(); + + $view_uri = $this->getApplicationURI("board/{$role_id}/"); + $reorder_uri = $this->getApplicationURI("board/{$role_id}/reorder/"); + + if ($request->isFormPost()) { + // User clicked "Done", make sure the page reloads to show the new + // column order. + return id(new AphrontRedirectResponse())->setURI($view_uri); + } + + $columns = id(new PhabricatorRoleColumnQuery()) + ->setViewer($viewer) + ->withRolePHIDs(array($role->getPHID())) + ->execute(); + $columns = msort($columns, 'getSequence'); + + $column_phid = $request->getStr('columnPHID'); + if ($column_phid && $request->validateCSRF()) { + + $columns = mpull($columns, null, 'getPHID'); + if (empty($columns[$column_phid])) { + return new Aphront404Response(); + } + + $target_column = $columns[$column_phid]; + $new_sequence = $request->getInt('sequence'); + if ($new_sequence < 0) { + return new Aphront404Response(); + } + + // TODO: For now, we're not recording any transactions here. We probably + // should, but this sort of edit is extremely trivial. + + // Resequence the columns so that the moved column has the correct + // sequence number. Move columns after it up one place in the sequence. + $new_map = array(); + foreach ($columns as $phid => $column) { + $value = $column->getSequence(); + if ($column->getPHID() == $column_phid) { + $value = $new_sequence; + } else if ($column->getSequence() >= $new_sequence) { + $value = $value + 1; + } + $new_map[$phid] = $value; + } + + // Sort the columns into their new ordering. + asort($new_map); + + // Now, compact the ordering and adjust any columns that need changes. + $role->openTransaction(); + $sequence = 0; + foreach ($new_map as $phid => $ignored) { + $new_value = $sequence++; + $cur_value = $columns[$phid]->getSequence(); + if ($new_value != $cur_value) { + $columns[$phid]->setSequence($new_value)->save(); + } + } + $role->saveTransaction(); + + return id(new AphrontAjaxResponse())->setContent( + array( + 'sequenceMap' => mpull($columns, 'getSequence', 'getPHID'), + )); + } + + $list_id = celerity_generate_unique_node_id(); + + $list = id(new PHUIObjectItemListView()) + ->setUser($viewer) + ->setID($list_id) + ->setFlush(true) + ->setDrag(true); + + foreach ($columns as $column) { + // Don't allow milestone columns to be reordered. + $proxy = $column->getProxy(); + if ($proxy && $proxy->isMilestone()) { + continue; + } + + // At least for now, don't show subrole column. + if ($proxy) { + continue; + } + + $item = id(new PHUIObjectItemView()) + ->setHeader($column->getDisplayName()) + ->addIcon($column->getHeaderIcon(), $column->getDisplayType()); + + if ($column->isHidden()) { + $item->setDisabled(true); + } + + $item->setGrippable(true); + $item->addSigil('board-column'); + $item->setMetadata( + array( + 'columnPHID' => $column->getPHID(), + 'columnSequence' => $column->getSequence(), + )); + + $list->addItem($item); + } + + Javelin::initBehavior( + 'reorder-columns', + array( + 'listID' => $list_id, + 'reorderURI' => $reorder_uri, + )); + + return $this->newDialog() + ->setTitle(pht('Reorder Columns')) + ->setWidth(AphrontDialogView::WIDTH_FORM) + ->appendChild($list) + ->addSubmitButton(pht('Done')); + } + +} diff --git a/src/applications/roles/controller/PhabricatorRoleBoardViewController.php b/src/applications/roles/controller/PhabricatorRoleBoardViewController.php new file mode 100644 index 0000000000..877e4d9598 --- /dev/null +++ b/src/applications/roles/controller/PhabricatorRoleBoardViewController.php @@ -0,0 +1,1040 @@ +getUser(); + + $response = $this->loadRole(); + if ($response) { + return $response; + } + + $role = $this->getRole(); + $state = $this->getViewState(); + $board_uri = $role->getWorkboardURI(); + + $search_engine = $state->getSearchEngine(); + $query_key = $state->getQueryKey(); + $saved = $state->getSavedQuery(); + if (!$saved) { + return new Aphront404Response(); + } + + if ($saved->getID()) { + $custom_query = $saved; + } else { + $custom_query = null; + } + + $layout_engine = $state->getLayoutEngine(); + + $board_phid = $role->getPHID(); + $columns = $layout_engine->getColumns($board_phid); + if (!$columns || !$role->getHasWorkboard()) { + $has_normal_columns = false; + + foreach ($columns as $column) { + if (!$column->getProxyPHID()) { + $has_normal_columns = true; + break; + } + } + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $role, + PhabricatorPolicyCapability::CAN_EDIT); + + if (!$has_normal_columns) { + if (!$can_edit) { + $content = $this->buildNoAccessContent($role); + } else { + $content = $this->buildInitializeContent($role); + } + } else { + if (!$can_edit) { + $content = $this->buildDisabledContent($role); + } else { + $content = $this->buildEnableContent($role); + } + } + + if ($content instanceof AphrontResponse) { + return $content; + } + + $nav = $this->newNavigation( + $role, + PhabricatorRole::ITEM_WORKBOARD); + + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb(pht('Workboard')); + + return $this->newPage() + ->setTitle( + array( + $role->getDisplayName(), + pht('Workboard'), + )) + ->setNavigation($nav) + ->setCrumbs($crumbs) + ->appendChild($content); + } + + $tasks = $state->getObjects(); + + $task_can_edit_map = id(new PhabricatorPolicyFilter()) + ->setViewer($viewer) + ->requireCapabilities(array(PhabricatorPolicyCapability::CAN_EDIT)) + ->apply($tasks); + + $board_id = celerity_generate_unique_node_id(); + + $board = id(new PHUIWorkboardView()) + ->setUser($viewer) + ->setID($board_id) + ->addSigil('jx-workboard') + ->setMetadata( + array( + 'boardPHID' => $role->getPHID(), + )); + + $visible_columns = array(); + $column_phids = array(); + $visible_phids = array(); + foreach ($columns as $column) { + if (!$state->getShowHidden()) { + if ($column->isHidden()) { + continue; + } + } + + $proxy = $column->getProxy(); + if ($proxy && !$proxy->isMilestone()) { + // TODO: For now, don't show subrole columns because we can't + // handle tasks with multiple positions yet. + continue; + } + + $task_phids = $layout_engine->getColumnObjectPHIDs( + $board_phid, + $column->getPHID()); + + $column_tasks = array_select_keys($tasks, $task_phids); + $column_phid = $column->getPHID(); + + $visible_columns[$column_phid] = $column; + $column_phids[$column_phid] = $column_tasks; + + foreach ($column_tasks as $phid => $task) { + $visible_phids[$phid] = $phid; + } + } + + $container_phids = $state->getBoardContainerPHIDs(); + + $rendering_engine = id(new PhabricatorRoleBoardRenderingEngine()) + ->setViewer($viewer) + ->setObjects(array_select_keys($tasks, $visible_phids)) + ->setEditMap($task_can_edit_map) + ->setExcludedRolePHIDs($container_phids); + + $templates = array(); + $all_tasks = array(); + $column_templates = array(); + $sounds = array(); + foreach ($visible_columns as $column_phid => $column) { + $column_tasks = $column_phids[$column_phid]; + + $panel = id(new PHUIWorkpanelView()) + ->setHeader($column->getDisplayName()) + ->setSubHeader($column->getDisplayType()) + ->addSigil('workpanel'); + + $proxy = $column->getProxy(); + if ($proxy) { + $proxy_id = $proxy->getID(); + $href = $this->getApplicationURI("view/{$proxy_id}/"); + $panel->setHref($href); + } + + $header_icon = $column->getHeaderIcon(); + if ($header_icon) { + $panel->setHeaderIcon($header_icon); + } + + $display_class = $column->getDisplayClass(); + if ($display_class) { + $panel->addClass($display_class); + } + + if ($column->isHidden()) { + $panel->addClass('role-panel-hidden'); + } + + $column_menu = $this->buildColumnMenu($role, $column); + $panel->addHeaderAction($column_menu); + + if ($column->canHaveTrigger()) { + $trigger = $column->getTrigger(); + if ($trigger) { + $trigger->setViewer($viewer); + } + + $trigger_menu = $this->buildTriggerMenu($column); + $panel->addHeaderAction($trigger_menu); + } + + $count_tag = id(new PHUITagView()) + ->setType(PHUITagView::TYPE_SHADE) + ->setColor(PHUITagView::COLOR_BLUE) + ->addSigil('column-points') + ->setName( + javelin_tag( + 'span', + array( + 'sigil' => 'column-points-content', + ), + pht('-'))) + ->setStyle('display: none'); + + $panel->setHeaderTag($count_tag); + + $cards = id(new PHUIObjectItemListView()) + ->setUser($viewer) + ->setFlush(true) + ->setAllowEmptyList(true) + ->addSigil('role-column') + ->setItemClass('phui-workcard') + ->setMetadata( + array( + 'columnPHID' => $column->getPHID(), + 'pointLimit' => $column->getPointLimit(), + )); + + $card_phids = array(); + foreach ($column_tasks as $task) { + $object_phid = $task->getPHID(); + + $card = $rendering_engine->renderCard($object_phid); + $templates[$object_phid] = hsprintf('%s', $card->getItem()); + $card_phids[] = $object_phid; + + $all_tasks[$object_phid] = $task; + } + + $panel->setCards($cards); + $board->addPanel($panel); + + $drop_effects = $column->getDropEffects(); + $drop_effects = mpull($drop_effects, 'toDictionary'); + + $preview_effect = null; + if ($column->canHaveTrigger()) { + $trigger = $column->getTrigger(); + if ($trigger) { + $preview_effect = $trigger->getPreviewEffect() + ->toDictionary(); + + foreach ($trigger->getSoundEffects() as $sound) { + $sounds[] = $sound; + } + } + } + + $column_templates[] = array( + 'columnPHID' => $column_phid, + 'effects' => $drop_effects, + 'cardPHIDs' => $card_phids, + 'triggerPreviewEffect' => $preview_effect, + ); + } + + $order_key = $state->getOrder(); + + $ordering_map = PhabricatorRoleColumnOrder::getEnabledOrders(); + $ordering = id(clone $ordering_map[$order_key]) + ->setViewer($viewer); + + $headers = $ordering->getHeadersForObjects($all_tasks); + $headers = mpull($headers, 'toDictionary'); + + $vectors = $ordering->getSortVectorsForObjects($all_tasks); + $vector_map = array(); + foreach ($vectors as $task_phid => $vector) { + $vector_map[$task_phid][$order_key] = $vector; + } + + $header_keys = $ordering->getHeaderKeysForObjects($all_tasks); + + $order_maps = array(); + $order_maps[] = $ordering->toDictionary(); + + $properties = array(); + foreach ($all_tasks as $task) { + $properties[$task->getPHID()] = + PhabricatorRoleBoardResponseEngine::newTaskProperties($task); + } + + $behavior_config = array( + 'moveURI' => $this->getApplicationURI('move/'.$role->getID().'/'), + 'uploadURI' => '/file/dropupload/', + 'coverURI' => $this->getApplicationURI('cover/'), + 'reloadURI' => phutil_string_cast($state->newWorkboardURI('reload/')), + 'chunkThreshold' => PhabricatorFileStorageEngine::getChunkThreshold(), + 'pointsEnabled' => ManiphestTaskPoints::getIsEnabled(), + + 'boardPHID' => $role->getPHID(), + 'order' => $state->getOrder(), + 'orders' => $order_maps, + 'headers' => $headers, + 'headerKeys' => $header_keys, + 'templateMap' => $templates, + 'orderMaps' => $vector_map, + 'propertyMaps' => $properties, + 'columnTemplates' => $column_templates, + + 'boardID' => $board_id, + 'rolePHID' => $role->getPHID(), + 'preloadSounds' => $sounds, + ); + $this->initBehavior('project-boards', $behavior_config); + + $sort_menu = $this->buildSortMenu( + $viewer, + $role, + $state->getOrder(), + $ordering_map); + + $filter_menu = $this->buildFilterMenu( + $viewer, + $role, + $custom_query, + $search_engine, + $query_key); + + $manage_menu = $this->buildManageMenu($role, $state->getShowHidden()); + + $header_link = phutil_tag( + 'a', + array( + 'href' => $this->getApplicationURI('profile/'.$role->getID().'/'), + ), + $role->getName()); + + $board_box = id(new PHUIBoxView()) + ->appendChild($board) + ->addClass('role-board-wrapper'); + + $nav = $this->newNavigation( + $role, + PhabricatorRole::ITEM_WORKBOARD); + + $divider = id(new PHUIListItemView()) + ->setType(PHUIListItemView::TYPE_DIVIDER); + $fullscreen = $this->buildFullscreenMenu(); + + $crumbs = $this->newWorkboardCrumbs(); + $crumbs->addTextCrumb(pht('Workboard')); + $crumbs->setBorder(true); + + $crumbs->addAction($sort_menu); + $crumbs->addAction($filter_menu); + $crumbs->addAction($divider); + $crumbs->addAction($manage_menu); + $crumbs->addAction($fullscreen); + + $page = $this->newPage() + ->setTitle( + array( + $role->getDisplayName(), + pht('Workboard'), + )) + ->setPageObjectPHIDs(array($role->getPHID())) + ->setShowFooter(false) + ->setNavigation($nav) + ->setCrumbs($crumbs) + ->addQuicksandConfig( + array( + 'boardConfig' => $behavior_config, + )) + ->appendChild( + array( + $board_box, + )); + + $background = $role->getDisplayWorkboardBackgroundColor(); + require_celerity_resource('phui-workboard-color-css'); + if ($background !== null) { + $background_color_class = "phui-workboard-{$background}"; + + $page->addClass('phui-workboard-color'); + $page->addClass($background_color_class); + } else { + $page->addClass('phui-workboard-no-color'); + } + + return $page; + } + + private function buildSortMenu( + PhabricatorUser $viewer, + PhabricatorRole $role, + $sort_key, + array $ordering_map) { + + $state = $this->getViewState(); + $base_uri = $state->newWorkboardURI(); + + $items = array(); + foreach ($ordering_map as $key => $ordering) { + // TODO: It would be desirable to build a real "PHUIIconView" here, but + // the pathway for threading that through all the view classes ends up + // being fairly complex, since some callers read the icon out of other + // views. For now, just stick with a string. + $ordering_icon = $ordering->getMenuIconIcon(); + $ordering_name = $ordering->getDisplayName(); + + $is_selected = ($key === $sort_key); + if ($is_selected) { + $active_name = $ordering_name; + $active_icon = $ordering_icon; + } + + $item = id(new PhabricatorActionView()) + ->setIcon($ordering_icon) + ->setSelected($is_selected) + ->setName($ordering_name); + + $uri = $base_uri->alter('order', $key); + $item->setHref($uri); + + $items[] = $item; + } + + $id = $role->getID(); + + $save_uri = $state->newWorkboardURI('default/sort/'); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $role, + PhabricatorPolicyCapability::CAN_EDIT); + + $items[] = id(new PhabricatorActionView()) + ->setType(PhabricatorActionView::TYPE_DIVIDER); + + $items[] = id(new PhabricatorActionView()) + ->setIcon('fa-floppy-o') + ->setName(pht('Save as Default')) + ->setHref($save_uri) + ->setWorkflow(true) + ->setDisabled(!$can_edit); + + $sort_menu = id(new PhabricatorActionListView()) + ->setUser($viewer); + foreach ($items as $item) { + $sort_menu->addAction($item); + } + + $sort_button = id(new PHUIListItemView()) + ->setName($active_name) + ->setIcon($active_icon) + ->setHref('#') + ->addSigil('boards-dropdown-menu') + ->setMetadata( + array( + 'items' => hsprintf('%s', $sort_menu), + )); + + return $sort_button; + } + + private function buildFilterMenu( + PhabricatorUser $viewer, + PhabricatorRole $role, + $custom_query, + PhabricatorApplicationSearchEngine $engine, + $query_key) { + + $state = $this->getViewState(); + + $named = array( + 'open' => pht('Open Tasks'), + 'all' => pht('All Tasks'), + ); + + if ($viewer->isLoggedIn()) { + $named['assigned'] = pht('Assigned to Me'); + } + + if ($custom_query) { + $named[$custom_query->getQueryKey()] = pht('Custom Filter'); + } + + $items = array(); + foreach ($named as $key => $name) { + $is_selected = ($key == $query_key); + if ($is_selected) { + $active_filter = $name; + } + + $is_custom = false; + if ($custom_query) { + $is_custom = ($key == $custom_query->getQueryKey()); + } + + $item = id(new PhabricatorActionView()) + ->setIcon('fa-search') + ->setSelected($is_selected) + ->setName($name); + + if ($is_custom) { + // When you're using a custom filter already and you select "Custom + // Filter", you get a dialog back to let you edit the filter. This is + // equivalent to selecting "Advanced Filter..." to configure a new + // filter. + $filter_uri = $state->newWorkboardURI('filter/'); + $item->setWorkflow(true); + } else { + $filter_uri = urisprintf('query/%s/', $key); + $filter_uri = $state->newWorkboardURI($filter_uri); + $filter_uri->removeQueryParam('filter'); + } + + $item->setHref($filter_uri); + + $items[] = $item; + } + + $id = $role->getID(); + + $filter_uri = $state->newWorkboardURI('filter/'); + + $items[] = id(new PhabricatorActionView()) + ->setIcon('fa-cog') + ->setHref($filter_uri) + ->setWorkflow(true) + ->setName(pht('Advanced Filter...')); + + $save_uri = $state->newWorkboardURI('default/filter/'); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $role, + PhabricatorPolicyCapability::CAN_EDIT); + + $items[] = id(new PhabricatorActionView()) + ->setType(PhabricatorActionView::TYPE_DIVIDER); + + $items[] = id(new PhabricatorActionView()) + ->setIcon('fa-floppy-o') + ->setName(pht('Save as Default')) + ->setHref($save_uri) + ->setWorkflow(true) + ->setDisabled(!$can_edit); + + $filter_menu = id(new PhabricatorActionListView()) + ->setUser($viewer); + foreach ($items as $item) { + $filter_menu->addAction($item); + } + + $filter_button = id(new PHUIListItemView()) + ->setName($active_filter) + ->setIcon('fa-search') + ->setHref('#') + ->addSigil('boards-dropdown-menu') + ->setMetadata( + array( + 'items' => hsprintf('%s', $filter_menu), + )); + + return $filter_button; + } + + private function buildManageMenu( + PhabricatorRole $role, + $show_hidden) { + + $request = $this->getRequest(); + $viewer = $request->getUser(); + $state = $this->getViewState(); + + $id = $role->getID(); + + $manage_uri = $this->getApplicationURI("board/{$id}/manage/"); + $add_uri = $this->getApplicationURI("board/{$id}/edit/"); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $role, + PhabricatorPolicyCapability::CAN_EDIT); + + $manage_items = array(); + + $manage_items[] = id(new PhabricatorActionView()) + ->setIcon('fa-plus') + ->setName(pht('Add Column')) + ->setHref($add_uri) + ->setDisabled(!$can_edit) + ->setWorkflow(true); + + $reorder_uri = $this->getApplicationURI("board/{$id}/reorder/"); + $manage_items[] = id(new PhabricatorActionView()) + ->setIcon('fa-exchange') + ->setName(pht('Reorder Columns')) + ->setHref($reorder_uri) + ->setDisabled(!$can_edit) + ->setWorkflow(true); + + if ($show_hidden) { + $hidden_uri = $state->newWorkboardURI() + ->removeQueryParam('hidden'); + $hidden_icon = 'fa-eye-slash'; + $hidden_text = pht('Hide Hidden Columns'); + } else { + $hidden_uri = $state->newWorkboardURI() + ->replaceQueryParam('hidden', 'true'); + $hidden_icon = 'fa-eye'; + $hidden_text = pht('Show Hidden Columns'); + } + + $manage_items[] = id(new PhabricatorActionView()) + ->setIcon($hidden_icon) + ->setName($hidden_text) + ->setHref($hidden_uri); + + $manage_items[] = id(new PhabricatorActionView()) + ->setType(PhabricatorActionView::TYPE_DIVIDER); + + $background_uri = $this->getApplicationURI("board/{$id}/background/"); + $manage_items[] = id(new PhabricatorActionView()) + ->setIcon('fa-paint-brush') + ->setName(pht('Change Background Color')) + ->setHref($background_uri) + ->setDisabled(!$can_edit) + ->setWorkflow(false); + + $manage_uri = $this->getApplicationURI("board/{$id}/manage/"); + $manage_items[] = id(new PhabricatorActionView()) + ->setIcon('fa-gear') + ->setName(pht('Manage Workboard')) + ->setHref($manage_uri); + + $manage_menu = id(new PhabricatorActionListView()) + ->setUser($viewer); + foreach ($manage_items as $item) { + $manage_menu->addAction($item); + } + + $manage_button = id(new PHUIListItemView()) + ->setIcon('fa-cog') + ->setHref('#') + ->addSigil('boards-dropdown-menu') + ->addSigil('has-tooltip') + ->setMetadata( + array( + 'tip' => pht('Manage'), + 'align' => 'S', + 'items' => hsprintf('%s', $manage_menu), + )); + + return $manage_button; + } + + private function buildFullscreenMenu() { + + $up = id(new PHUIListItemView()) + ->setIcon('fa-arrows-alt') + ->setHref('#') + ->addClass('phui-workboard-expand-icon') + ->addSigil('jx-toggle-class') + ->addSigil('has-tooltip') + ->setMetaData(array( + 'tip' => pht('Fullscreen'), + 'map' => array( + 'phabricator-standard-page' => 'phui-workboard-fullscreen', + ), + )); + + return $up; + } + + private function buildColumnMenu( + PhabricatorRole $role, + PhabricatorRoleColumn $column) { + + $request = $this->getRequest(); + $viewer = $request->getUser(); + $state = $this->getViewState(); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $role, + PhabricatorPolicyCapability::CAN_EDIT); + + $column_items = array(); + + if ($column->getProxyPHID()) { + $default_phid = $column->getProxyPHID(); + } else { + $default_phid = $column->getRolePHID(); + } + + $specs = id(new ManiphestEditEngine()) + ->setViewer($viewer) + ->newCreateActionSpecifications(array()); + + foreach ($specs as $spec) { + $column_items[] = id(new PhabricatorActionView()) + ->setIcon($spec['icon']) + ->setName($spec['name']) + ->setHref($spec['uri']) + ->setDisabled($spec['disabled']) + ->addSigil('column-add-task') + ->setMetadata( + array( + 'createURI' => $spec['uri'], + 'columnPHID' => $column->getPHID(), + 'boardPHID' => $role->getPHID(), + 'rolePHID' => $default_phid, + )); + } + + $column_items[] = id(new PhabricatorActionView()) + ->setType(PhabricatorActionView::TYPE_DIVIDER); + + $query_uri = urisprintf('viewquery/%d/', $column->getID()); + $query_uri = $state->newWorkboardURI($query_uri); + + $column_items[] = id(new PhabricatorActionView()) + ->setName(pht('View Tasks as Query')) + ->setIcon('fa-search') + ->setHref($query_uri); + + $column_move_uri = urisprintf('bulkmove/%d/column/', $column->getID()); + $column_move_uri = $state->newWorkboardURI($column_move_uri); + + $column_items[] = id(new PhabricatorActionView()) + ->setIcon('fa-arrows-h') + ->setName(pht('Move Tasks to Column...')) + ->setHref($column_move_uri) + ->setWorkflow(true); + + $role_move_uri = urisprintf('bulkmove/%d/role/', $column->getID()); + $role_move_uri = $state->newWorkboardURI($role_move_uri); + + $column_items[] = id(new PhabricatorActionView()) + ->setIcon('fa-arrows') + ->setName(pht('Move Tasks to Role...')) + ->setHref($role_move_uri) + ->setWorkflow(true); + + $bulk_edit_uri = urisprintf('bulk/%d/', $column->getID()); + $bulk_edit_uri = $state->newWorkboardURI($bulk_edit_uri); + + $can_bulk_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + PhabricatorApplication::getByClass('PhabricatorManiphestApplication'), + ManiphestBulkEditCapability::CAPABILITY); + + $column_items[] = id(new PhabricatorActionView()) + ->setIcon('fa-pencil-square-o') + ->setName(pht('Bulk Edit Tasks...')) + ->setHref($bulk_edit_uri) + ->setDisabled(!$can_bulk_edit); + + $column_items[] = id(new PhabricatorActionView()) + ->setType(PhabricatorActionView::TYPE_DIVIDER); + + + $edit_uri = 'board/'.$role->getID().'/edit/'.$column->getID().'/'; + $column_items[] = id(new PhabricatorActionView()) + ->setName(pht('Edit Column')) + ->setIcon('fa-pencil') + ->setHref($this->getApplicationURI($edit_uri)) + ->setDisabled(!$can_edit) + ->setWorkflow(true); + + $can_hide = ($can_edit && !$column->isDefaultColumn()); + + $hide_uri = urisprintf('hide/%d/', $column->getID()); + $hide_uri = $state->newWorkboardURI($hide_uri); + + if (!$column->isHidden()) { + $column_items[] = id(new PhabricatorActionView()) + ->setName(pht('Hide Column')) + ->setIcon('fa-eye-slash') + ->setHref($hide_uri) + ->setDisabled(!$can_hide) + ->setWorkflow(true); + } else { + $column_items[] = id(new PhabricatorActionView()) + ->setName(pht('Show Column')) + ->setIcon('fa-eye') + ->setHref($hide_uri) + ->setDisabled(!$can_hide) + ->setWorkflow(true); + } + + $column_menu = id(new PhabricatorActionListView()) + ->setUser($viewer); + foreach ($column_items as $item) { + $column_menu->addAction($item); + } + + $column_button = id(new PHUIIconView()) + ->setIcon('fa-pencil') + ->setHref('#') + ->addSigil('boards-dropdown-menu') + ->setMetadata( + array( + 'items' => hsprintf('%s', $column_menu), + )); + + return $column_button; + } + + private function buildTriggerMenu(PhabricatorRoleColumn $column) { + $viewer = $this->getViewer(); + $trigger = $column->getTrigger(); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $column, + PhabricatorPolicyCapability::CAN_EDIT); + + $trigger_items = array(); + if (!$trigger) { + $set_uri = $this->getApplicationURI( + new PhutilURI( + 'trigger/edit/', + array( + 'columnPHID' => $column->getPHID(), + ))); + + $trigger_items[] = id(new PhabricatorActionView()) + ->setIcon('fa-cogs') + ->setName(pht('New Trigger...')) + ->setHref($set_uri) + ->setDisabled(!$can_edit); + } else { + $trigger_items[] = id(new PhabricatorActionView()) + ->setIcon('fa-cogs') + ->setName(pht('View Trigger')) + ->setHref($trigger->getURI()) + ->setDisabled(!$can_edit); + } + + $remove_uri = $this->getApplicationURI( + new PhutilURI( + urisprintf( + 'column/remove/%d/', + $column->getID()))); + + $trigger_items[] = id(new PhabricatorActionView()) + ->setIcon('fa-times') + ->setName(pht('Remove Trigger')) + ->setHref($remove_uri) + ->setWorkflow(true) + ->setDisabled(!$can_edit || !$trigger); + + $trigger_menu = id(new PhabricatorActionListView()) + ->setUser($viewer); + foreach ($trigger_items as $item) { + $trigger_menu->addAction($item); + } + + if ($trigger) { + $trigger_icon = 'fa-cogs'; + } else { + $trigger_icon = 'fa-cogs grey'; + } + + $trigger_button = id(new PHUIIconView()) + ->setIcon($trigger_icon) + ->setHref('#') + ->addSigil('boards-dropdown-menu') + ->addSigil('trigger-preview') + ->setMetadata( + array( + 'items' => hsprintf('%s', $trigger_menu), + 'columnPHID' => $column->getPHID(), + )); + + return $trigger_button; + } + + private function buildInitializeContent(PhabricatorRole $role) { + $request = $this->getRequest(); + $viewer = $this->getViewer(); + + $type = $request->getStr('initialize-type'); + + $id = $role->getID(); + + $profile_uri = $this->getApplicationURI("profile/{$id}/"); + $board_uri = $this->getApplicationURI("board/{$id}/"); + $import_uri = $this->getApplicationURI("board/{$id}/import/"); + + $set_default = $request->getBool('default'); + if ($set_default) { + $this + ->getProfileMenuEngine() + ->adjustDefault(PhabricatorRole::ITEM_WORKBOARD); + } + + if ($request->isFormPost()) { + if ($type == 'backlog-only') { + $column = PhabricatorRoleColumn::initializeNewColumn($viewer) + ->setSequence(0) + ->setProperty('isDefault', true) + ->setRolePHID($role->getPHID()) + ->save(); + + $xactions = array(); + $xactions[] = id(new PhabricatorRoleTransaction()) + ->setTransactionType( + PhabricatorRoleWorkboardTransaction::TRANSACTIONTYPE) + ->setNewValue(1); + + id(new PhabricatorRoleTransactionEditor()) + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true) + ->applyTransactions($role, $xactions); + + return id(new AphrontRedirectResponse()) + ->setURI($board_uri); + } else { + return id(new AphrontRedirectResponse()) + ->setURI($import_uri); + } + } + + // TODO: Tailor this UI if the role is already a parent role. We + // should not offer options for creating a parent role workboard, since + // they can't have their own columns. + + $new_selector = id(new AphrontFormRadioButtonControl()) + ->setLabel(pht('Columns')) + ->setName('initialize-type') + ->setValue('backlog-only') + ->addButton( + 'backlog-only', + pht('New Empty Board'), + pht('Create a new board with just a backlog column.')) + ->addButton( + 'import', + pht('Import Columns'), + pht('Import board columns from another role.')); + + $default_checkbox = id(new AphrontFormCheckboxControl()) + ->setLabel(pht('Make Default')) + ->addCheckbox( + 'default', + 1, + pht('Make the workboard the default view for this role.'), + true); + + $form = id(new AphrontFormView()) + ->setUser($viewer) + ->addHiddenInput('initialize', 1) + ->appendRemarkupInstructions( + pht('The workboard for this role has not been created yet.')) + ->appendControl($new_selector) + ->appendControl($default_checkbox) + ->appendControl( + id(new AphrontFormSubmitControl()) + ->addCancelButton($profile_uri) + ->setValue(pht('Create Workboard'))); + + $box = id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Create Workboard')) + ->setForm($form); + + return $box; + } + + private function buildNoAccessContent(PhabricatorRole $role) { + $viewer = $this->getViewer(); + + $id = $role->getID(); + + $profile_uri = $this->getApplicationURI("profile/{$id}/"); + + return $this->newDialog() + ->setTitle(pht('Unable to Create Workboard')) + ->appendParagraph( + pht( + 'The workboard for this role has not been created yet, '. + 'but you do not have permission to create it. Only users '. + 'who can edit this role can create a workboard for it.')) + ->addCancelButton($profile_uri); + } + + + private function buildEnableContent(PhabricatorRole $role) { + $request = $this->getRequest(); + $viewer = $this->getViewer(); + + $id = $role->getID(); + $profile_uri = $this->getApplicationURI("profile/{$id}/"); + $board_uri = $this->getApplicationURI("board/{$id}/"); + + if ($request->isFormPost()) { + $xactions = array(); + + $xactions[] = id(new PhabricatorRoleTransaction()) + ->setTransactionType( + PhabricatorRoleWorkboardTransaction::TRANSACTIONTYPE) + ->setNewValue(1); + + id(new PhabricatorRoleTransactionEditor()) + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true) + ->applyTransactions($role, $xactions); + + return id(new AphrontRedirectResponse()) + ->setURI($board_uri); + } + + return $this->newDialog() + ->setTitle(pht('Workboard Disabled')) + ->addHiddenInput('initialize', 1) + ->appendParagraph( + pht( + 'This workboard has been disabled, but can be restored to its '. + 'former glory.')) + ->addCancelButton($profile_uri) + ->addSubmitButton(pht('Enable Workboard')); + } + + private function buildDisabledContent(PhabricatorRole $role) { + $viewer = $this->getViewer(); + + $id = $role->getID(); + + $profile_uri = $this->getApplicationURI("profile/{$id}/"); + + return $this->newDialog() + ->setTitle(pht('Workboard Disabled')) + ->appendParagraph( + pht( + 'This workboard has been disabled, and you do not have permission '. + 'to enable it. Only users who can edit this role can restore '. + 'the workboard.')) + ->addCancelButton($profile_uri); + } + +} diff --git a/src/applications/roles/controller/PhabricatorRoleColumnBulkEditController.php b/src/applications/roles/controller/PhabricatorRoleColumnBulkEditController.php new file mode 100644 index 0000000000..931e29bd09 --- /dev/null +++ b/src/applications/roles/controller/PhabricatorRoleColumnBulkEditController.php @@ -0,0 +1,72 @@ +getViewer(); + + $response = $this->loadRole(); + if ($response) { + return $response; + } + + $role = $this->getRole(); + $state = $this->getViewState(); + $board_uri = $state->newWorkboardURI(); + + $layout_engine = $state->getLayoutEngine(); + + $board_phid = $role->getPHID(); + $columns = $layout_engine->getColumns($board_phid); + $columns = mpull($columns, null, 'getID'); + + $column_id = $request->getURIData('columnID'); + $bulk_column = idx($columns, $column_id); + if (!$bulk_column) { + return new Aphront404Response(); + } + + $bulk_task_phids = $layout_engine->getColumnObjectPHIDs( + $board_phid, + $bulk_column->getPHID()); + + $tasks = $state->getObjects(); + + $bulk_tasks = array_select_keys($tasks, $bulk_task_phids); + + $bulk_tasks = id(new PhabricatorPolicyFilter()) + ->setViewer($viewer) + ->requireCapabilities(array(PhabricatorPolicyCapability::CAN_EDIT)) + ->apply($bulk_tasks); + + if (!$bulk_tasks) { + return $this->newDialog() + ->setTitle(pht('No Editable Tasks')) + ->appendParagraph( + pht( + 'The selected column contains no visible tasks which you '. + 'have permission to edit.')) + ->addCancelButton($board_uri); + } + + // Create a saved query to hold the working set. This allows us to get + // around URI length limitations with a long "?ids=..." query string. + // For details, see T10268. + $search_engine = id(new ManiphestTaskSearchEngine()) + ->setViewer($viewer); + + $saved_query = $search_engine->newSavedQuery(); + $saved_query->setParameter('ids', mpull($bulk_tasks, 'getID')); + $search_engine->saveQuery($saved_query); + + $query_key = $saved_query->getQueryKey(); + + $bulk_uri = new PhutilURI("/maniphest/bulk/query/{$query_key}/"); + $bulk_uri->replaceQueryParam('board', $role->getID()); + + return id(new AphrontRedirectResponse()) + ->setURI($bulk_uri); + } + +} diff --git a/src/applications/roles/controller/PhabricatorRoleColumnBulkMoveController.php b/src/applications/roles/controller/PhabricatorRoleColumnBulkMoveController.php new file mode 100644 index 0000000000..9a5285c5e5 --- /dev/null +++ b/src/applications/roles/controller/PhabricatorRoleColumnBulkMoveController.php @@ -0,0 +1,301 @@ +getViewer(); + + $response = $this->loadRole(); + if ($response) { + return $response; + } + + // See T13316. If we're operating in "column" mode, we're going to skip + // the prompt for a role and just have the user select a target column. + // In "role" mode, we prompt them for a role first. + $is_column_mode = ($request->getURIData('mode') === 'column'); + + $src_role = $this->getRole(); + $state = $this->getViewState(); + $board_uri = $state->newWorkboardURI(); + + $layout_engine = $state->getLayoutEngine(); + + $board_phid = $src_role->getPHID(); + $columns = $layout_engine->getColumns($board_phid); + $columns = mpull($columns, null, 'getID'); + + $column_id = $request->getURIData('columnID'); + $src_column = idx($columns, $column_id); + if (!$src_column) { + return new Aphront404Response(); + } + + $move_task_phids = $layout_engine->getColumnObjectPHIDs( + $board_phid, + $src_column->getPHID()); + + $tasks = $state->getObjects(); + + $move_tasks = array_select_keys($tasks, $move_task_phids); + + $move_tasks = id(new PhabricatorPolicyFilter()) + ->setViewer($viewer) + ->requireCapabilities(array(PhabricatorPolicyCapability::CAN_EDIT)) + ->apply($move_tasks); + + if (!$move_tasks) { + return $this->newDialog() + ->setTitle(pht('No Movable Tasks')) + ->appendParagraph( + pht( + 'The selected column contains no visible tasks which you '. + 'have permission to move.')) + ->addCancelButton($board_uri); + } + + $dst_role_phid = null; + $dst_role = null; + $has_role = false; + if ($is_column_mode) { + $has_role = true; + $dst_role_phid = $src_role->getPHID(); + } else { + if ($request->isFormOrHiSecPost()) { + $has_role = $request->getStr('hasRole'); + if ($has_role) { + // We may read this from a tokenizer input as an array, or from a + // hidden input as a string. + $dst_role_phid = head($request->getArr('dstRolePHID')); + if (!$dst_role_phid) { + $dst_role_phid = $request->getStr('dstRolePHID'); + } + } + } + } + + $errors = array(); + $hidden = array(); + + if ($has_role) { + if (!$dst_role_phid) { + $errors[] = pht('Choose a role to move tasks to.'); + } else { + $dst_role = id(new PhabricatorRoleQuery()) + ->setViewer($viewer) + ->withPHIDs(array($dst_role_phid)) + ->executeOne(); + if (!$dst_role) { + $errors[] = pht('Choose a valid role to move tasks to.'); + } + + if (!$dst_role->getHasWorkboard()) { + $errors[] = pht('You must choose a role with a workboard.'); + $dst_role = null; + } + } + } + + if ($dst_role) { + $same_role = ($src_role->getID() === $dst_role->getID()); + + $layout_engine = id(new PhabricatorRoleBoardLayoutEngine()) + ->setViewer($viewer) + ->setBoardPHIDs(array($dst_role->getPHID())) + ->setFetchAllBoards(true) + ->executeLayout(); + + $dst_columns = $layout_engine->getColumns($dst_role->getPHID()); + $dst_columns = mpull($dst_columns, null, 'getPHID'); + + // Prevent moves to milestones or subroles by selecting their + // columns, since the implications aren't obvious and this doesn't + // work the same way as normal column moves. + foreach ($dst_columns as $key => $dst_column) { + if ($dst_column->getProxyPHID()) { + unset($dst_columns[$key]); + } + } + + $has_column = false; + $dst_column = null; + + // If we're performing a move on the same board, default the + // control value to the current column. + if ($same_role) { + $dst_column_phid = $src_column->getPHID(); + } else { + $dst_column_phid = null; + } + + if ($request->isFormOrHiSecPost()) { + $has_column = $request->getStr('hasColumn'); + if ($has_column) { + $dst_column_phid = $request->getStr('dstColumnPHID'); + } + } + + if ($has_column) { + $dst_column = idx($dst_columns, $dst_column_phid); + if (!$dst_column) { + $errors[] = pht('Choose a column to move tasks to.'); + } else { + if ($dst_column->isHidden()) { + $errors[] = pht('You can not move tasks to a hidden column.'); + $dst_column = null; + } else if ($dst_column->getPHID() === $src_column->getPHID()) { + $errors[] = pht('You can not move tasks from a column to itself.'); + $dst_column = null; + } + } + } + + if ($dst_column) { + foreach ($move_tasks as $move_task) { + $xactions = array(); + + // If we're switching roles, get out of the old role first + // and move to the new role. + if (!$same_role) { + $xactions[] = id(new ManiphestTransaction()) + ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) + ->setMetadataValue( + 'edge:type', + PhabricatorRoleObjectHasRoleEdgeType::EDGECONST) + ->setNewValue( + array( + '-' => array( + $src_role->getPHID() => $src_role->getPHID(), + ), + '+' => array( + $dst_role->getPHID() => $dst_role->getPHID(), + ), + )); + } + + $xactions[] = id(new ManiphestTransaction()) + ->setTransactionType(PhabricatorTransactions::TYPE_COLUMNS) + ->setNewValue( + array( + array( + 'columnPHID' => $dst_column->getPHID(), + ), + )); + + $editor = id(new ManiphestTransactionEditor()) + ->setActor($viewer) + ->setContinueOnMissingFields(true) + ->setContinueOnNoEffect(true) + ->setContentSourceFromRequest($request) + ->setCancelURI($board_uri); + + $editor->applyTransactions($move_task, $xactions); + } + + // If we did a move on the same workboard, redirect and preserve the + // state parameters. If we moved to a different workboard, go there + // with clean default state. + if ($same_role) { + $done_uri = $board_uri; + } else { + $done_uri = $dst_role->getWorkboardURI(); + } + + return id(new AphrontRedirectResponse())->setURI($done_uri); + } + + $title = pht('Move Tasks to Column'); + + $form = id(new AphrontFormView()) + ->setViewer($viewer); + + // If we're moving between roles, add a reminder about which role + // you selected in the previous step. + if (!$is_column_mode) { + $form->appendControl( + id(new AphrontFormStaticControl()) + ->setLabel(pht('Role')) + ->setValue($dst_role->getDisplayName())); + } + + $column_options = array( + 'visible' => array(), + 'hidden' => array(), + ); + + $any_hidden = false; + foreach ($dst_columns as $column) { + if (!$column->isHidden()) { + $group = 'visible'; + } else { + $group = 'hidden'; + } + + $phid = $column->getPHID(); + $display_name = $column->getDisplayName(); + + $column_options[$group][$phid] = $display_name; + } + + if ($column_options['hidden']) { + $column_options = array( + pht('Visible Columns') => $column_options['visible'], + pht('Hidden Columns') => $column_options['hidden'], + ); + } else { + $column_options = $column_options['visible']; + } + + $form->appendControl( + id(new AphrontFormSelectControl()) + ->setName('dstColumnPHID') + ->setLabel(pht('Move to Column')) + ->setValue($dst_column_phid) + ->setOptions($column_options)); + + $submit = pht('Move Tasks'); + + $hidden['dstRolePHID'] = $dst_role->getPHID(); + $hidden['hasColumn'] = true; + $hidden['hasRole'] = true; + } else { + $title = pht('Move Tasks to Role'); + + if ($dst_role_phid) { + $dst_role_phid_value = array($dst_role_phid); + } else { + $dst_role_phid_value = array(); + } + + $form = id(new AphrontFormView()) + ->setViewer($viewer) + ->appendControl( + id(new AphrontFormTokenizerControl()) + ->setName('dstRolePHID') + ->setLimit(1) + ->setLabel(pht('Move to Role')) + ->setValue($dst_role_phid_value) + ->setDatasource(new PhabricatorRoleDatasource())); + + $submit = pht('Continue'); + + $hidden['hasRole'] = true; + } + + $dialog = $this->newWorkboardDialog() + ->setWidth(AphrontDialogView::WIDTH_FORM) + ->setTitle($title) + ->setErrors($errors) + ->appendForm($form) + ->addSubmitButton($submit) + ->addCancelButton($board_uri); + + foreach ($hidden as $key => $value) { + $dialog->addHiddenInput($key, $value); + } + + return $dialog; + } + +} diff --git a/src/applications/roles/controller/PhabricatorRoleColumnDetailController.php b/src/applications/roles/controller/PhabricatorRoleColumnDetailController.php new file mode 100644 index 0000000000..14e5584cfb --- /dev/null +++ b/src/applications/roles/controller/PhabricatorRoleColumnDetailController.php @@ -0,0 +1,113 @@ +getViewer(); + $id = $request->getURIData('id'); + $role_id = $request->getURIData('roleID'); + + $role = id(new PhabricatorRoleQuery()) + ->setViewer($viewer) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + )) + ->withIDs(array($role_id)) + ->needImages(true) + ->executeOne(); + if (!$role) { + return new Aphront404Response(); + } + $this->setRole($role); + + $role_id = $role->getID(); + + $column = id(new PhabricatorRoleColumnQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + )) + ->executeOne(); + if (!$column) { + return new Aphront404Response(); + } + + $timeline = $this->buildTransactionTimeline( + $column, + new PhabricatorRoleColumnTransactionQuery()); + $timeline->setShouldTerminate(true); + + $title = $column->getDisplayName(); + + $header = $this->buildHeaderView($column); + $properties = $this->buildPropertyView($column); + + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb(pht('Workboard'), $role->getWorkboardURI()); + $crumbs->addTextCrumb(pht('Column: %s', $title)); + $crumbs->setBorder(true); + + $nav = $this->newNavigation( + $role, + PhabricatorRole::ITEM_WORKBOARD); + require_celerity_resource('project-view-css'); + + $view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->addClass('project-view-home') + ->addClass('project-view-people-home') + ->setMainColumn(array( + $properties, + $timeline, + )); + + return $this->newPage() + ->setTitle($title) + ->setNavigation($nav) + ->setCrumbs($crumbs) + ->appendChild($view); + } + + private function buildHeaderView(PhabricatorRoleColumn $column) { + $viewer = $this->getViewer(); + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Column: %s', $column->getDisplayName())) + ->setUser($viewer); + + if ($column->isHidden()) { + $header->setStatus('fa-ban', 'dark', pht('Hidden')); + } + + return $header; + } + + private function buildPropertyView( + PhabricatorRoleColumn $column) { + $viewer = $this->getViewer(); + + $properties = id(new PHUIPropertyListView()) + ->setUser($viewer) + ->setObject($column); + + $limit = $column->getPointLimit(); + if ($limit === null) { + $limit_text = pht('No Limit'); + } else { + $limit_text = $limit; + } + $properties->addProperty(pht('Point Limit'), $limit_text); + + $box = id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Details')) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->appendChild($properties); + + return $box; + } + +} diff --git a/src/applications/roles/controller/PhabricatorRoleColumnEditController.php b/src/applications/roles/controller/PhabricatorRoleColumnEditController.php new file mode 100644 index 0000000000..7d29819f8d --- /dev/null +++ b/src/applications/roles/controller/PhabricatorRoleColumnEditController.php @@ -0,0 +1,143 @@ +getViewer(); + $id = $request->getURIData('id'); + $role_id = $request->getURIData('roleID'); + + $role = id(new PhabricatorRoleQuery()) + ->setViewer($viewer) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->withIDs(array($role_id)) + ->needImages(true) + ->executeOne(); + + if (!$role) { + return new Aphront404Response(); + } + $this->setRole($role); + + $is_new = ($id ? false : true); + + if (!$is_new) { + $column = id(new PhabricatorRoleColumnQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$column) { + return new Aphront404Response(); + } + } else { + $column = PhabricatorRoleColumn::initializeNewColumn($viewer); + } + + $e_name = null; + $e_limit = null; + + $v_limit = $column->getPointLimit(); + $v_name = $column->getName(); + + $validation_exception = null; + $view_uri = $role->getWorkboardURI(); + + if ($request->isFormPost()) { + $v_name = $request->getStr('name'); + $v_limit = $request->getStr('limit'); + + if ($is_new) { + $column->setRolePHID($role->getPHID()); + $column->attachRole($role); + + $columns = id(new PhabricatorRoleColumnQuery()) + ->setViewer($viewer) + ->withRolePHIDs(array($role->getPHID())) + ->execute(); + + $new_sequence = 1; + if ($columns) { + $values = mpull($columns, 'getSequence'); + $new_sequence = max($values) + 1; + } + $column->setSequence($new_sequence); + } + + $xactions = array(); + + $type_name = PhabricatorRoleColumnNameTransaction::TRANSACTIONTYPE; + $type_limit = PhabricatorRoleColumnLimitTransaction::TRANSACTIONTYPE; + + if (!$column->getProxy()) { + $xactions[] = id(new PhabricatorRoleColumnTransaction()) + ->setTransactionType($type_name) + ->setNewValue($v_name); + } + + $xactions[] = id(new PhabricatorRoleColumnTransaction()) + ->setTransactionType($type_limit) + ->setNewValue($v_limit); + + try { + $editor = id(new PhabricatorRoleColumnTransactionEditor()) + ->setActor($viewer) + ->setContinueOnNoEffect(true) + ->setContentSourceFromRequest($request) + ->applyTransactions($column, $xactions); + return id(new AphrontRedirectResponse())->setURI($view_uri); + } catch (PhabricatorApplicationTransactionValidationException $ex) { + $e_name = $ex->getShortMessage($type_name); + $e_limit = $ex->getShortMessage($type_limit); + $validation_exception = $ex; + } + } + + $form = id(new AphrontFormView()) + ->setUser($request->getUser()); + + if (!$column->getProxy()) { + $form->appendChild( + id(new AphrontFormTextControl()) + ->setValue($v_name) + ->setLabel(pht('Name')) + ->setName('name') + ->setError($e_name)); + } + + $form->appendChild( + id(new AphrontFormTextControl()) + ->setValue($v_limit) + ->setLabel(pht('Point Limit')) + ->setName('limit') + ->setError($e_limit) + ->setCaption( + pht('Maximum number of points of tasks allowed in the column.'))); + + if ($is_new) { + $title = pht('Create Column'); + $submit = pht('Create Column'); + } else { + $title = pht('Edit %s', $column->getDisplayName()); + $submit = pht('Save Column'); + } + + return $this->newDialog() + ->setWidth(AphrontDialogView::WIDTH_FORM) + ->setTitle($title) + ->appendForm($form) + ->setValidationException($validation_exception) + ->addCancelButton($view_uri) + ->addSubmitButton($submit); + + } +} diff --git a/src/applications/roles/controller/PhabricatorRoleColumnHideController.php b/src/applications/roles/controller/PhabricatorRoleColumnHideController.php new file mode 100644 index 0000000000..1557de6c0a --- /dev/null +++ b/src/applications/roles/controller/PhabricatorRoleColumnHideController.php @@ -0,0 +1,149 @@ +getViewer(); + $id = $request->getURIData('id'); + $role_id = $request->getURIData('roleID'); + + $role = id(new PhabricatorRoleQuery()) + ->setViewer($viewer) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->withIDs(array($role_id)) + ->executeOne(); + + if (!$role) { + return new Aphront404Response(); + } + $this->setRole($role); + + $column = id(new PhabricatorRoleColumnQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$column) { + return new Aphront404Response(); + } + + $column_phid = $column->getPHID(); + + $view_uri = $role->getWorkboardURI(); + $view_uri = new PhutilURI($view_uri); + foreach ($request->getPassthroughRequestData() as $key => $value) { + $view_uri->replaceQueryParam($key, $value); + } + + if ($column->isDefaultColumn()) { + return $this->newDialog() + ->setTitle(pht('Can Not Hide Default Column')) + ->appendParagraph( + pht('You can not hide the default/backlog column on a board.')) + ->addCancelButton($view_uri, pht('Okay')); + } + + $proxy = $column->getProxy(); + + if ($request->isFormPost()) { + if ($proxy) { + if ($proxy->isArchived()) { + $new_status = PhabricatorRoleStatus::STATUS_ACTIVE; + } else { + $new_status = PhabricatorRoleStatus::STATUS_ARCHIVED; + } + + $xactions = array(); + + $xactions[] = id(new PhabricatorRoleTransaction()) + ->setTransactionType( + PhabricatorRoleStatusTransaction::TRANSACTIONTYPE) + ->setNewValue($new_status); + + id(new PhabricatorRoleTransactionEditor()) + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true) + ->applyTransactions($proxy, $xactions); + } else { + if ($column->isHidden()) { + $new_status = PhabricatorRoleColumn::STATUS_ACTIVE; + } else { + $new_status = PhabricatorRoleColumn::STATUS_HIDDEN; + } + + $type_status = + PhabricatorRoleColumnStatusTransaction::TRANSACTIONTYPE; + + $xactions = array( + id(new PhabricatorRoleColumnTransaction()) + ->setTransactionType($type_status) + ->setNewValue($new_status), + ); + + $editor = id(new PhabricatorRoleColumnTransactionEditor()) + ->setActor($viewer) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true) + ->setContentSourceFromRequest($request) + ->applyTransactions($column, $xactions); + } + + return id(new AphrontRedirectResponse())->setURI($view_uri); + } + + if ($proxy) { + if ($column->isHidden()) { + $title = pht('Activate and Show Column'); + $body = pht( + 'This column is hidden because it represents an archived '. + 'subrole. Do you want to activate the subrole so the '. + 'column is visible again?'); + $button = pht('Activate Subrole'); + } else { + $title = pht('Archive and Hide Column'); + $body = pht( + 'This column is visible because it represents an active '. + 'subrole. Do you want to hide the column by archiving the '. + 'subrole?'); + $button = pht('Archive Subrole'); + } + } else { + if ($column->isHidden()) { + $title = pht('Show Column'); + $body = pht('Are you sure you want to show this column?'); + $button = pht('Show Column'); + } else { + $title = pht('Hide Column'); + $body = pht( + 'Are you sure you want to hide this column? It will no longer '. + 'appear on the workboard.'); + $button = pht('Hide Column'); + } + } + + $dialog = $this->newDialog() + ->setWidth(AphrontDialogView::WIDTH_FORM) + ->setTitle($title) + ->appendChild($body) + ->setDisableWorkflowOnCancel(true) + ->addCancelButton($view_uri) + ->addSubmitButton($button); + + foreach ($request->getPassthroughRequestData() as $key => $value) { + $dialog->addHiddenInput($key, $value); + } + + return $dialog; + } +} diff --git a/src/applications/roles/controller/PhabricatorRoleColumnRemoveTriggerController.php b/src/applications/roles/controller/PhabricatorRoleColumnRemoveTriggerController.php new file mode 100644 index 0000000000..da0bcbf559 --- /dev/null +++ b/src/applications/roles/controller/PhabricatorRoleColumnRemoveTriggerController.php @@ -0,0 +1,60 @@ +getViewer(); + $id = $request->getURIData('id'); + + $column = id(new PhabricatorRoleColumnQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$column) { + return new Aphront404Response(); + } + + $done_uri = $column->getWorkboardURI(); + + if (!$column->getTriggerPHID()) { + return $this->newDialog() + ->setTitle(pht('No Trigger')) + ->appendParagraph( + pht('This column does not have a trigger.')) + ->addCancelButton($done_uri); + } + + if ($request->isFormPost()) { + $column_xactions = array(); + + $column_xactions[] = $column->getApplicationTransactionTemplate() + ->setTransactionType( + PhabricatorRoleColumnTriggerTransaction::TRANSACTIONTYPE) + ->setNewValue(null); + + $column_editor = $column->getApplicationTransactionEditor() + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true); + + $column_editor->applyTransactions($column, $column_xactions); + + return id(new AphrontRedirectResponse())->setURI($done_uri); + } + + $body = pht('Really remove the trigger from this column?'); + + return $this->newDialog() + ->setTitle(pht('Remove Trigger')) + ->appendParagraph($body) + ->addSubmitButton(pht('Remove Trigger')) + ->addCancelButton($done_uri); + } +} diff --git a/src/applications/roles/controller/PhabricatorRoleColumnViewQueryController.php b/src/applications/roles/controller/PhabricatorRoleColumnViewQueryController.php new file mode 100644 index 0000000000..a1b9d6f506 --- /dev/null +++ b/src/applications/roles/controller/PhabricatorRoleColumnViewQueryController.php @@ -0,0 +1,72 @@ +getViewer(); + + $response = $this->loadRole(); + if ($response) { + return $response; + } + + $role = $this->getRole(); + $state = $this->getViewState(); + $board_uri = $state->newWorkboardURI(); + + // NOTE: We're performing layout without handing the "LayoutEngine" any + // object PHIDs. We only want to get access to the column object the user + // is trying to query, so we do not need to actually position any cards on + // the board. + + $board_phid = $role->getPHID(); + + $layout_engine = id(new PhabricatorRoleBoardLayoutEngine()) + ->setViewer($viewer) + ->setBoardPHIDs(array($board_phid)) + ->setFetchAllBoards(true) + ->executeLayout(); + + $columns = $layout_engine->getColumns($board_phid); + $columns = mpull($columns, null, 'getID'); + + $column_id = $request->getURIData('columnID'); + $column = idx($columns, $column_id); + if (!$column) { + return new Aphront404Response(); + } + + // Create a saved query to combine the active filter on the workboard + // with the column filter. If the user currently has constraints on the + // board, we want to add a new column or role constraint, not + // completely replace the constraints. + $default_query = $state->getSavedQuery(); + $saved_query = $default_query->newCopy(); + + if ($column->getProxyPHID()) { + $role_phids = $saved_query->getParameter('rolePHIDs'); + if (!$role_phids) { + $role_phids = array(); + } + $role_phids[] = $column->getProxyPHID(); + $saved_query->setParameter('rolePHIDs', $role_phids); + } else { + $saved_query->setParameter( + 'columnPHIDs', + array($column->getPHID())); + } + + $search_engine = id(new ManiphestTaskSearchEngine()) + ->setViewer($viewer); + + $search_engine->saveQuery($saved_query); + + $query_key = $saved_query->getQueryKey(); + $query_uri = new PhutilURI("/maniphest/query/{$query_key}/#R"); + + return id(new AphrontRedirectResponse()) + ->setURI($query_uri); + } + +} diff --git a/src/applications/roles/controller/PhabricatorRoleController.php b/src/applications/roles/controller/PhabricatorRoleController.php new file mode 100644 index 0000000000..727958d16c --- /dev/null +++ b/src/applications/roles/controller/PhabricatorRoleController.php @@ -0,0 +1,231 @@ +role = $role; + return $this; + } + + protected function getRole() { + return $this->role; + } + + protected function loadRole() { + return $this->loadRoleWithCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + )); + } + + protected function loadRoleForEdit() { + return $this->loadRoleWithCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )); + } + + private function loadRoleWithCapabilities(array $capabilities) { + $viewer = $this->getViewer(); + $request = $this->getRequest(); + + $id = nonempty( + $request->getURIData('roleID'), + $request->getURIData('id')); + + $slug = $request->getURIData('slug'); + + if ($slug) { + $normal_slug = PhabricatorSlug::normalizeProjectSlug($slug); + $is_abnormal = ($slug !== $normal_slug); + $normal_uri = "/tag/{$normal_slug}/"; + } else { + $is_abnormal = false; + } + + $query = id(new PhabricatorRoleQuery()) + ->setViewer($viewer) + ->requireCapabilities($capabilities) + ->needMembers(true) + ->needWatchers(true) + ->needImages(true) + ->needSlugs(true); + + if ($slug) { + $query->withSlugs(array($slug)); + } else { + $query->withIDs(array($id)); + } + + $policy_exception = null; + try { + $role = $query->executeOne(); + } catch (PhabricatorPolicyException $ex) { + $policy_exception = $ex; + $role = null; + } + + if (!$role) { + // This role legitimately does not exist, so just 404 the user. + if (!$policy_exception) { + return new Aphront404Response(); + } + + // Here, the role exists but the user can't see it. If they are + // using a non-canonical slug to view the role, redirect to the + // canonical slug. If they're already using the canonical slug, rethrow + // the exception to give them the policy error. + if ($is_abnormal) { + return id(new AphrontRedirectResponse())->setURI($normal_uri); + } else { + throw $policy_exception; + } + } + + // The user can view the role, but is using a noncanonical slug. + // Redirect to the canonical slug. + $primary_slug = $role->getPrimarySlug(); + if ($slug && ($slug !== $primary_slug)) { + $primary_uri = "/tag/{$primary_slug}/"; + return id(new AphrontRedirectResponse())->setURI($primary_uri); + } + + $this->setRole($role); + + return null; + } + + protected function buildApplicationCrumbs() { + return $this->newApplicationCrumbs('profile'); + } + + protected function newWorkboardCrumbs() { + return $this->newApplicationCrumbs('workboard'); + } + + private function newApplicationCrumbs($mode) { + $crumbs = parent::buildApplicationCrumbs(); + + $role = $this->getRole(); + if ($role) { + $ancestors = $role->getAncestorRoles(); + $ancestors = array_reverse($ancestors); + $ancestors[] = $role; + foreach ($ancestors as $ancestor) { + if ($ancestor->getPHID() === $role->getPHID()) { + // Link the current role's crumb to its profile no matter what, + // since we're already on the right context page for it and linking + // to the current page isn't helpful. + $crumb_uri = $ancestor->getProfileURI(); + } else { + switch ($mode) { + case 'workboard': + if ($ancestor->getHasWorkboard()) { + $crumb_uri = $ancestor->getWorkboardURI(); + } else { + $crumb_uri = $ancestor->getProfileURI(); + } + break; + case 'profile': + default: + $crumb_uri = $ancestor->getProfileURI(); + break; + } + } + + $crumbs->addTextCrumb($ancestor->getName(), $crumb_uri); + } + } + + return $crumbs; + } + + protected function getProfileMenuEngine() { + if (!$this->profileMenuEngine) { + $viewer = $this->getViewer(); + $role = $this->getRole(); + if ($role) { + $engine = id(new PhabricatorRoleProfileMenuEngine()) + ->setViewer($viewer) + ->setController($this) + ->setProfileObject($role); + $this->profileMenuEngine = $engine; + } + } + + return $this->profileMenuEngine; + } + + protected function setProfileMenuEngine( + PhabricatorRoleProfileMenuEngine $engine) { + $this->profileMenuEngine = $engine; + return $this; + } + + protected function newCardResponse( + $board_phid, + $object_phid, + PhabricatorRoleColumnOrder $ordering = null, + $sounds = array()) { + + $viewer = $this->getViewer(); + + $request = $this->getRequest(); + $visible_phids = $request->getStrList('visiblePHIDs'); + if (!$visible_phids) { + $visible_phids = array(); + } + + $engine = id(new PhabricatorRoleBoardResponseEngine()) + ->setViewer($viewer) + ->setBoardPHID($board_phid) + ->setUpdatePHIDs(array($object_phid)) + ->setVisiblePHIDs($visible_phids) + ->setSounds($sounds); + + if ($ordering) { + $engine->setOrdering($ordering); + } + + return $engine->buildResponse(); + } + + public function renderHashtags(array $tags) { + $result = array(); + foreach ($tags as $key => $tag) { + $result[] = '#'.$tag; + } + return implode(', ', $result); + } + + final protected function newNavigation( + PhabricatorRole $role, + $item_identifier) { + + $engine = $this->getProfileMenuEngine(); + + $view_list = $engine->newProfileMenuItemViewList(); + + // See PHI1247. If the "Workboard" item is removed from the menu, we will + // not be able to select it. This can happen if a user removes the item, + // then manually navigate to the workboard URI (or follows an older link). + // In this case, just render the menu with no selected item. + if ($view_list->getViewsWithItemIdentifier($item_identifier)) { + $view_list->setSelectedViewWithItemIdentifier($item_identifier); + } + + $navigation = $view_list->newNavigationView(); + + if ($item_identifier === PhabricatorRole::ITEM_WORKBOARD) { + $navigation->addClass('role-board-nav'); + } + + return $navigation; + } + +} diff --git a/src/applications/roles/controller/PhabricatorRoleCoverController.php b/src/applications/roles/controller/PhabricatorRoleCoverController.php new file mode 100644 index 0000000000..9eb4ca9aa5 --- /dev/null +++ b/src/applications/roles/controller/PhabricatorRoleCoverController.php @@ -0,0 +1,53 @@ +getViewer(); + + $request->validateCSRF(); + + $board_phid = $request->getStr('boardPHID'); + $object_phid = $request->getStr('objectPHID'); + $file_phid = $request->getStr('filePHID'); + + $object = id(new ManiphestTaskQuery()) + ->setViewer($viewer) + ->withPHIDs(array($object_phid)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$object) { + return new Aphront404Response(); + } + + $file = id(new PhabricatorFileQuery()) + ->setViewer($viewer) + ->withPHIDs(array($file_phid)) + ->executeOne(); + if (!$file) { + return new Aphront404Response(); + } + + $xactions = array(); + + $xactions[] = id(new ManiphestTransaction()) + ->setTransactionType(ManiphestTaskCoverImageTransaction::TRANSACTIONTYPE) + ->setNewValue($file->getPHID()); + + $editor = id(new ManiphestTransactionEditor()) + ->setActor($viewer) + ->setContinueOnMissingFields(true) + ->setContinueOnNoEffect(true) + ->setContentSourceFromRequest($request); + + $editor->applyTransactions($object, $xactions); + + return $this->newCardResponse($board_phid, $object_phid); + } + +} diff --git a/src/applications/roles/controller/PhabricatorRoleEditController.php b/src/applications/roles/controller/PhabricatorRoleEditController.php new file mode 100644 index 0000000000..01fd652fcc --- /dev/null +++ b/src/applications/roles/controller/PhabricatorRoleEditController.php @@ -0,0 +1,112 @@ +engine = $engine; + return $this; + } + + public function getEngine() { + return $this->engine; + } + + public function handleRequest(AphrontRequest $request) { + $viewer = $this->getViewer(); + + $engine = id(new PhabricatorRoleEditEngine()) + ->setController($this); + + $this->setEngine($engine); + + $id = $request->getURIData('id'); + if (!$id) { + // This capability is checked again later, but checking it here + // explicitly gives us a better error message. + $this->requireApplicationCapability( + RoleCreateRolesCapability::CAPABILITY); + + $parent_id = head($request->getArr('parent')); + if (!$parent_id) { + $parent_id = $request->getStr('parent'); + } + + if ($parent_id) { + $is_milestone = false; + } else { + $parent_id = head($request->getArr('milestone')); + if (!$parent_id) { + $parent_id = $request->getStr('milestone'); + } + $is_milestone = true; + } + + if ($parent_id) { + $query = id(new PhabricatorRoleQuery()) + ->setViewer($viewer) + ->needImages(true) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )); + + if (ctype_digit($parent_id)) { + $query->withIDs(array($parent_id)); + } else { + $query->withPHIDs(array($parent_id)); + } + + $parent = $query->executeOne(); + + if ($is_milestone) { + if (!$parent->supportsMilestones()) { + $cancel_uri = "/role/subroles/{$parent_id}/"; + return $this->newDialog() + ->setTitle(pht('No Milestones')) + ->appendParagraph( + pht('You can not add milestones to this role.')) + ->addCancelButton($cancel_uri); + } + $engine->setMilestoneRole($parent); + } else { + if (!$parent->supportsSubroles()) { + $cancel_uri = "/role/subroles/{$parent_id}/"; + return $this->newDialog() + ->setTitle(pht('No Subroles')) + ->appendParagraph( + pht('You can not add subroles to this role.')) + ->addCancelButton($cancel_uri); + } + $engine->setParentRole($parent); + } + + $this->setRole($parent); + } + } + + return $engine->buildResponse(); + } + + protected function buildApplicationCrumbs() { + $crumbs = parent::buildApplicationCrumbs(); + + $engine = $this->getEngine(); + if ($engine) { + $parent = $engine->getParentRole(); + $milestone = $engine->getMilestoneRole(); + if ($parent || $milestone) { + $id = nonempty($parent, $milestone)->getID(); + $crumbs->addTextCrumb( + pht('Subroles'), + $this->getApplicationURI("subroles/{$id}/")); + } + } + + return $crumbs; + } + +} diff --git a/src/applications/roles/controller/PhabricatorRoleEditPictureController.php b/src/applications/roles/controller/PhabricatorRoleEditPictureController.php new file mode 100644 index 0000000000..f56ddbd945 --- /dev/null +++ b/src/applications/roles/controller/PhabricatorRoleEditPictureController.php @@ -0,0 +1,354 @@ +getViewer(); + $id = $request->getURIData('id'); + + $role = id(new PhabricatorRoleQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->needImages(true) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$role) { + return new Aphront404Response(); + } + + $this->setRole($role); + + $manage_uri = $this->getApplicationURI('manage/'.$role->getID().'/'); + + $supported_formats = PhabricatorFile::getTransformableImageFormats(); + $e_file = true; + $errors = array(); + + if ($request->isFormPost()) { + $phid = $request->getStr('phid'); + $is_default = false; + if ($phid == PhabricatorPHIDConstants::PHID_VOID) { + $phid = null; + $is_default = true; + } else if ($phid) { + $file = id(new PhabricatorFileQuery()) + ->setViewer($viewer) + ->withPHIDs(array($phid)) + ->executeOne(); + } else { + if ($request->getFileExists('picture')) { + $file = PhabricatorFile::newFromPHPUpload( + $_FILES['picture'], + array( + 'authorPHID' => $viewer->getPHID(), + 'canCDN' => true, + )); + } else { + $e_file = pht('Required'); + $errors[] = pht( + 'You must choose a file when uploading a new role picture.'); + } + } + + if (!$errors && !$is_default) { + if (!$file->isTransformableImage()) { + $e_file = pht('Not Supported'); + $errors[] = pht( + 'This server only supports these image formats: %s.', + implode(', ', $supported_formats)); + } else { + $xform = PhabricatorFileTransform::getTransformByKey( + PhabricatorFileThumbnailTransform::TRANSFORM_PROFILE); + $xformed = $xform->executeTransform($file); + } + } + + if (!$errors) { + if ($is_default) { + $new_value = null; + } else { + $new_value = $xformed->getPHID(); + } + + $xactions = array(); + $xactions[] = id(new PhabricatorRoleTransaction()) + ->setTransactionType( + PhabricatorRoleImageTransaction::TRANSACTIONTYPE) + ->setNewValue($new_value); + + $editor = id(new PhabricatorRoleTransactionEditor()) + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnMissingFields(true) + ->setContinueOnNoEffect(true); + + $editor->applyTransactions($role, $xactions); + + return id(new AphrontRedirectResponse())->setURI($manage_uri); + } + } + + $title = pht('Edit Role Picture'); + + $form = id(new PHUIFormLayoutView()) + ->setUser($viewer); + + $builtin = PhabricatorRoleIconSet::getIconImage( + $role->getIcon()); + $default_image = PhabricatorFile::loadBuiltin($this->getViewer(), + 'roles/'.$builtin); + + $images = array(); + + $current = $role->getProfileImagePHID(); + $has_current = false; + if ($current) { + $files = id(new PhabricatorFileQuery()) + ->setViewer($viewer) + ->withPHIDs(array($current)) + ->execute(); + if ($files) { + $file = head($files); + if ($file->isTransformableImage()) { + $has_current = true; + $images[$current] = array( + 'uri' => $file->getBestURI(), + 'tip' => pht('Current Picture'), + ); + } + } + } + + $root = dirname(phutil_get_library_root('phabricator')); + $root = $root.'/resources/builtin/roles/v3/'; + + $builtins = id(new FileFinder($root)) + ->withType('f') + ->withFollowSymlinks(true) + ->find(); + + foreach ($builtins as $builtin) { + $file = PhabricatorFile::loadBuiltin($viewer, 'roles/v3/'.$builtin); + $images[$file->getPHID()] = array( + 'uri' => $file->getBestURI(), + 'tip' => pht('Builtin Image'), + ); + } + + $images[PhabricatorPHIDConstants::PHID_VOID] = array( + 'uri' => $default_image->getBestURI(), + 'tip' => pht('Default Picture'), + ); + + require_celerity_resource('people-profile-css'); + Javelin::initBehavior('phabricator-tooltips', array()); + + $buttons = array(); + foreach ($images as $phid => $spec) { + $button = javelin_tag( + 'button', + array( + 'class' => 'button-grey profile-image-button', + 'sigil' => 'has-tooltip', + 'meta' => array( + 'tip' => $spec['tip'], + 'size' => 300, + ), + ), + phutil_tag( + 'img', + array( + 'height' => 50, + 'width' => 50, + 'src' => $spec['uri'], + ))); + + $button = array( + phutil_tag( + 'input', + array( + 'type' => 'hidden', + 'name' => 'phid', + 'value' => $phid, + )), + $button, + ); + + $button = phabricator_form( + $viewer, + array( + 'class' => 'profile-image-form', + 'method' => 'POST', + ), + $button); + + $buttons[] = $button; + } + + if ($has_current) { + $form->appendChild( + id(new AphrontFormMarkupControl()) + ->setLabel(pht('Current Picture')) + ->setValue(array_shift($buttons))); + } + + $form->appendChild( + id(new AphrontFormMarkupControl()) + ->setLabel(pht('Use Picture')) + ->setValue( + array( + $this->renderDefaultForm($role), + $buttons, + ))); + + $launch_id = celerity_generate_unique_node_id(); + $input_id = celerity_generate_unique_node_id(); + + Javelin::initBehavior( + 'launch-icon-composer', + array( + 'launchID' => $launch_id, + 'inputID' => $input_id, + )); + + $compose_button = javelin_tag( + 'button', + array( + 'class' => 'button-grey', + 'id' => $launch_id, + 'sigil' => 'icon-composer', + ), + pht('Choose Icon and Color...')); + + $compose_input = javelin_tag( + 'input', + array( + 'type' => 'hidden', + 'id' => $input_id, + 'name' => 'phid', + )); + + $compose_form = phabricator_form( + $viewer, + array( + 'class' => 'profile-image-form', + 'method' => 'POST', + ), + array( + $compose_input, + $compose_button, + )); + + $form->appendChild( + id(new AphrontFormMarkupControl()) + ->setLabel(pht('Custom')) + ->setValue($compose_form)); + + $upload_form = id(new AphrontFormView()) + ->setUser($viewer) + ->setEncType('multipart/form-data') + ->appendChild( + id(new AphrontFormFileControl()) + ->setName('picture') + ->setLabel(pht('Upload Picture')) + ->setError($e_file) + ->setCaption( + pht('Supported formats: %s', implode(', ', $supported_formats)))) + ->appendChild( + id(new AphrontFormSubmitControl()) + ->addCancelButton($manage_uri) + ->setValue(pht('Upload Picture'))); + + $form_box = id(new PHUIObjectBoxView()) + ->setHeaderText($title) + ->setFormErrors($errors) + ->setForm($form); + + $upload_box = id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Upload New Picture')) + ->setForm($upload_form); + + $nav = $this->newNavigation( + $role, + PhabricatorRole::ITEM_MANAGE); + + return $this->newPage() + ->setTitle($title) + ->setNavigation($nav) + ->appendChild( + array( + $form_box, + $upload_box, + )); + } + + private function renderDefaultForm(PhabricatorRole $role) { + $viewer = $this->getViewer(); + $compose_color = $role->getDisplayIconComposeColor(); + $compose_icon = $role->getDisplayIconComposeIcon(); + + $default_builtin = id(new PhabricatorFilesComposeIconBuiltinFile()) + ->setColor($compose_color) + ->setIcon($compose_icon); + + $file_builtins = PhabricatorFile::loadBuiltins( + $viewer, + array($default_builtin)); + + $file_builtin = head($file_builtins); + + $default_button = javelin_tag( + 'button', + array( + 'class' => 'button-grey profile-image-button', + 'sigil' => 'has-tooltip', + 'meta' => array( + 'tip' => pht('Use Icon and Color'), + 'size' => 300, + ), + ), + phutil_tag( + 'img', + array( + 'height' => 50, + 'width' => 50, + 'src' => $file_builtin->getBestURI(), + ))); + + $inputs = array( + 'rolePHID' => $role->getPHID(), + 'icon' => $compose_icon, + 'color' => $compose_color, + ); + + foreach ($inputs as $key => $value) { + $inputs[$key] = javelin_tag( + 'input', + array( + 'type' => 'hidden', + 'name' => $key, + 'value' => $value, + )); + } + + $default_form = phabricator_form( + $viewer, + array( + 'class' => 'profile-image-form', + 'method' => 'POST', + 'action' => '/file/compose/', + ), + array( + $inputs, + $default_button, + )); + + return $default_form; + } + +} diff --git a/src/applications/roles/controller/PhabricatorRoleListController.php b/src/applications/roles/controller/PhabricatorRoleListController.php new file mode 100644 index 0000000000..e4595c88b4 --- /dev/null +++ b/src/applications/roles/controller/PhabricatorRoleListController.php @@ -0,0 +1,26 @@ +setController($this) + ->buildResponse(); + } + + protected function buildApplicationCrumbs() { + $crumbs = parent::buildApplicationCrumbs(); + + id(new PhabricatorRoleEditEngine()) + ->setViewer($this->getViewer()) + ->addActionToCrumbs($crumbs); + + return $crumbs; + } + +} diff --git a/src/applications/roles/controller/PhabricatorRoleLockController.php b/src/applications/roles/controller/PhabricatorRoleLockController.php new file mode 100644 index 0000000000..4e62492d3b --- /dev/null +++ b/src/applications/roles/controller/PhabricatorRoleLockController.php @@ -0,0 +1,86 @@ +getViewer(); + + $this->requireApplicationCapability( + RoleCanLockRolesCapability::CAPABILITY); + + $id = $request->getURIData('id'); + $role = id(new PhabricatorRoleQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$role) { + return new Aphront404Response(); + } + + $done_uri = "/role/members/{$id}/"; + + if (!$role->supportsEditMembers()) { + return $this->newDialog() + ->setTitle(pht('Membership Immutable')) + ->appendChild( + pht('This role does not support editing membership.')) + ->addCancelButton($done_uri); + } + + $is_locked = $role->getIsMembershipLocked(); + + if ($request->isFormPost()) { + $xactions = array(); + + if ($is_locked) { + $new_value = 0; + } else { + $new_value = 1; + } + + $xactions[] = id(new PhabricatorRoleTransaction()) + ->setTransactionType( + PhabricatorRoleLockTransaction::TRANSACTIONTYPE) + ->setNewValue($new_value); + + $editor = id(new PhabricatorRoleTransactionEditor()) + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true) + ->applyTransactions($role, $xactions); + + return id(new AphrontRedirectResponse())->setURI($done_uri); + } + + if ($role->getIsMembershipLocked()) { + $title = pht('Unlock Role'); + $body = pht( + 'If you unlock this role, members will be free to leave.'); + $button = pht('Unlock Role'); + } else { + $title = pht('Lock Role'); + $body = pht( + 'If you lock this role, members will be prevented from '. + 'leaving it.'); + $button = pht('Lock Role'); + } + + return $this->newDialog() + ->setTitle($title) + ->appendParagraph($body) + ->addSubmitbutton($button) + ->addCancelButton($done_uri); + } + +} diff --git a/src/applications/roles/controller/PhabricatorRoleManageController.php b/src/applications/roles/controller/PhabricatorRoleManageController.php new file mode 100644 index 0000000000..efe5207300 --- /dev/null +++ b/src/applications/roles/controller/PhabricatorRoleManageController.php @@ -0,0 +1,157 @@ +loadRole(); + if ($response) { + return $response; + } + + $viewer = $request->getUser(); + $role = $this->getRole(); + $id = $role->getID(); + $picture = $role->getProfileImageURI(); + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Role History')) + ->setUser($viewer) + ->setPolicyObject($role); + + if ($role->getStatus() == PhabricatorRoleStatus::STATUS_ACTIVE) { + $header->setStatus('fa-check', 'bluegrey', pht('Active')); + } else { + $header->setStatus('fa-ban', 'red', pht('Archived')); + } + + $curtain = $this->buildCurtain($role); + $properties = $this->buildPropertyListView($role); + + $timeline = $this->buildTransactionTimeline( + $role, + new PhabricatorRoleTransactionQuery()); + $timeline->setShouldTerminate(true); + + $nav = $this->newNavigation( + $role, + PhabricatorRole::ITEM_MANAGE); + + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb(pht('Manage')); + $crumbs->setBorder(true); + + require_celerity_resource('project-view-css'); + + $manage = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setCurtain($curtain) + ->addPropertySection(pht('Details'), $properties) + ->addClass('project-view-home') + ->addClass('project-view-people-home') + ->setMainColumn( + array( + $timeline, + )); + + return $this->newPage() + ->setNavigation($nav) + ->setCrumbs($crumbs) + ->setTitle( + array( + $role->getDisplayName(), + pht('Manage'), + )) + ->appendChild( + array( + $manage, + )); + } + + private function buildCurtain(PhabricatorRole $role) { + $viewer = $this->getViewer(); + + $id = $role->getID(); + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $role, + PhabricatorPolicyCapability::CAN_EDIT); + + $curtain = $this->newCurtainView($role); + + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Edit Details')) + ->setIcon('fa-pencil') + ->setHref($this->getApplicationURI("edit/{$id}/")) + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit)); + + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Edit Menu')) + ->setIcon('fa-th-list') + ->setHref($this->getApplicationURI("{$id}/item/configure/")) + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit)); + + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Edit Picture')) + ->setIcon('fa-picture-o') + ->setHref($this->getApplicationURI("picture/{$id}/")) + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit)); + + if ($role->isArchived()) { + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Activate Role')) + ->setIcon('fa-check') + ->setHref($this->getApplicationURI("archive/{$id}/")) + ->setDisabled(!$can_edit) + ->setWorkflow(true)); + } else { + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Archive Role')) + ->setIcon('fa-ban') + ->setHref($this->getApplicationURI("archive/{$id}/")) + ->setDisabled(!$can_edit) + ->setWorkflow(true)); + } + + return $curtain; + } + + private function buildPropertyListView( + PhabricatorRole $role) { + $viewer = $this->getViewer(); + + $view = id(new PHUIPropertyListView()) + ->setUser($viewer); + + $view->addProperty( + pht('Looks Like'), + $viewer->renderHandle($role->getPHID())->setAsTag(true)); + + $slugs = $role->getSlugs(); + $tags = mpull($slugs, 'getSlug'); + + $view->addProperty( + pht('Hashtags'), + $this->renderHashtags($tags)); + + $field_list = PhabricatorCustomField::getObjectFields( + $role, + PhabricatorCustomField::ROLE_VIEW); + $field_list->appendFieldsToPropertyList($role, $viewer, $view); + + return $view; + } + +} diff --git a/src/applications/roles/controller/PhabricatorRoleMembersAddController.php b/src/applications/roles/controller/PhabricatorRoleMembersAddController.php new file mode 100644 index 0000000000..1940f6e2d3 --- /dev/null +++ b/src/applications/roles/controller/PhabricatorRoleMembersAddController.php @@ -0,0 +1,77 @@ +getViewer(); + $id = $request->getURIData('id'); + + $role = id(new PhabricatorRoleQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$role) { + return new Aphront404Response(); + } + + $this->setRole($role); + $done_uri = "/role/members/{$id}/"; + + if (!$role->supportsEditMembers()) { + $copy = pht('Parent roles and milestones do not support adding '. + 'members. You can add members directly to any non-parent subrole.'); + + return $this->newDialog() + ->setTitle(pht('Unsupported Role')) + ->appendParagraph($copy) + ->addCancelButton($done_uri); + } + + if ($request->isFormPost()) { + $member_phids = $request->getArr('memberPHIDs'); + + $type_member = PhabricatorRoleRoleHasMemberEdgeType::EDGECONST; + + $xactions = array(); + + $xactions[] = id(new PhabricatorRoleTransaction()) + ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) + ->setMetadataValue('edge:type', $type_member) + ->setNewValue( + array( + '+' => array_fuse($member_phids), + )); + + $editor = id(new PhabricatorRoleTransactionEditor()) + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true) + ->applyTransactions($role, $xactions); + + return id(new AphrontRedirectResponse()) + ->setURI($done_uri); + } + + $form = id(new AphrontFormView()) + ->setUser($viewer) + ->appendControl( + id(new AphrontFormTokenizerControl()) + ->setName('memberPHIDs') + ->setLabel(pht('Members')) + ->setDatasource(new PhabricatorPeopleDatasource())); + + return $this->newDialog() + ->setTitle(pht('Add Members')) + ->appendForm($form) + ->addCancelButton($done_uri) + ->addSubmitButton(pht('Add Members')); + } + +} diff --git a/src/applications/roles/controller/PhabricatorRoleMembersRemoveController.php b/src/applications/roles/controller/PhabricatorRoleMembersRemoveController.php new file mode 100644 index 0000000000..96fe1d94ff --- /dev/null +++ b/src/applications/roles/controller/PhabricatorRoleMembersRemoveController.php @@ -0,0 +1,95 @@ +getViewer(); + $id = $request->getURIData('id'); + $type = $request->getURIData('type'); + + $role = id(new PhabricatorRoleQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->needMembers(true) + ->needWatchers(true) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$role) { + return new Aphront404Response(); + } + + if ($type == 'watchers') { + $is_watcher = true; + $edge_type = PhabricatorObjectHasWatcherEdgeType::EDGECONST; + } else { + if (!$role->supportsEditMembers()) { + return new Aphront404Response(); + } + + $is_watcher = false; + $edge_type = PhabricatorRoleRoleHasMemberEdgeType::EDGECONST; + } + + $members_uri = $this->getApplicationURI('members/'.$role->getID().'/'); + $remove_phid = $request->getStr('phid'); + + if ($request->isFormPost()) { + $xactions = array(); + + $xactions[] = id(new PhabricatorRoleTransaction()) + ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) + ->setMetadataValue('edge:type', $edge_type) + ->setNewValue( + array( + '-' => array($remove_phid => $remove_phid), + )); + + $editor = id(new PhabricatorRoleTransactionEditor()) + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true) + ->applyTransactions($role, $xactions); + + return id(new AphrontRedirectResponse()) + ->setURI($members_uri); + } + + $handle = id(new PhabricatorHandleQuery()) + ->setViewer($viewer) + ->withPHIDs(array($remove_phid)) + ->executeOne(); + + $target_name = phutil_tag('strong', array(), $handle->getName()); + $role_name = phutil_tag('strong', array(), $role->getName()); + + if ($is_watcher) { + $title = pht('Remove Watcher'); + $body = pht( + 'Remove %s as a watcher of %s?', + $target_name, + $role_name); + $button = pht('Remove Watcher'); + } else { + $title = pht('Remove Member'); + $body = pht( + 'Remove %s as a role member of %s?', + $target_name, + $role_name); + $button = pht('Remove Member'); + } + + return $this->newDialog() + ->setTitle($title) + ->addHiddenInput('phid', $remove_phid) + ->appendParagraph($body) + ->addCancelButton($members_uri) + ->addSubmitButton($button); + } + +} diff --git a/src/applications/roles/controller/PhabricatorRoleMembersViewController.php b/src/applications/roles/controller/PhabricatorRoleMembersViewController.php new file mode 100644 index 0000000000..73b8332b7c --- /dev/null +++ b/src/applications/roles/controller/PhabricatorRoleMembersViewController.php @@ -0,0 +1,229 @@ +getViewer(); + $id = $request->getURIData('id'); + + $role = id(new PhabricatorRoleQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->needMembers(true) + ->needWatchers(true) + ->needImages(true) + ->executeOne(); + if (!$role) { + return new Aphront404Response(); + } + + $this->setRole($role); + $title = pht('Members and Watchers'); + $curtain = $this->buildCurtainView($role); + + $member_list = id(new PhabricatorRoleMemberListView()) + ->setUser($viewer) + ->setRole($role) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->setUserPHIDs($role->getMemberPHIDs()) + ->setShowNote(true); + + $watcher_list = id(new PhabricatorRoleWatcherListView()) + ->setUser($viewer) + ->setRole($role) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->setUserPHIDs($role->getWatcherPHIDs()) + ->setShowNote(true); + + $nav = $this->newNavigation( + $role, + PhabricatorRole::ITEM_MEMBERS); + + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb(pht('Members')); + $crumbs->setBorder(true); + + $header = id(new PHUIHeaderView()) + ->setHeader($title) + ->setHeaderIcon('fa-group'); + + require_celerity_resource('project-view-css'); + + $view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setCurtain($curtain) + ->addClass('project-view-home') + ->addClass('project-view-people-home') + ->setMainColumn(array( + $member_list, + $watcher_list, + )); + + return $this->newPage() + ->setNavigation($nav) + ->setCrumbs($crumbs) + ->setTitle(array($role->getName(), $title)) + ->appendChild($view); + } + + private function buildCurtainView(PhabricatorRole $role) { + $viewer = $this->getViewer(); + $id = $role->getID(); + + $curtain = $this->newCurtainView(); + + $is_locked = $role->getIsMembershipLocked(); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $role, + PhabricatorPolicyCapability::CAN_EDIT); + + $supports_edit = $role->supportsEditMembers(); + + $can_join = $supports_edit && PhabricatorPolicyFilter::hasCapability( + $viewer, + $role, + PhabricatorPolicyCapability::CAN_JOIN); + + $can_leave = $supports_edit && (!$is_locked || $can_edit); + + $viewer_phid = $viewer->getPHID(); + + if (!$role->isUserMember($viewer_phid)) { + $curtain->addAction( + id(new PhabricatorActionView()) + ->setHref('/role/update/'.$role->getID().'/join/') + ->setIcon('fa-plus') + ->setDisabled(!$can_join) + ->setWorkflow(true) + ->setName(pht('Join Role'))); + } else { + $curtain->addAction( + id(new PhabricatorActionView()) + ->setHref('/role/update/'.$role->getID().'/leave/') + ->setIcon('fa-times') + ->setDisabled(!$can_leave) + ->setWorkflow(true) + ->setName(pht('Leave Role'))); + } + + if (!$role->isUserWatcher($viewer->getPHID())) { + $curtain->addAction( + id(new PhabricatorActionView()) + ->setWorkflow(true) + ->setHref('/role/watch/'.$role->getID().'/') + ->setIcon('fa-eye') + ->setName(pht('Watch Role'))); + } else { + $curtain->addAction( + id(new PhabricatorActionView()) + ->setWorkflow(true) + ->setHref('/role/unwatch/'.$role->getID().'/') + ->setIcon('fa-eye-slash') + ->setName(pht('Unwatch Role'))); + } + + $can_silence = $role->isUserMember($viewer_phid); + $is_silenced = $this->isRoleSilenced($role); + + if ($is_silenced) { + $silence_text = pht('Enable Mail'); + } else { + $silence_text = pht('Disable Mail'); + } + + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName($silence_text) + ->setIcon('fa-envelope-o') + ->setHref("/role/silence/{$id}/") + ->setWorkflow(true) + ->setDisabled(!$can_silence)); + + $can_add = $can_edit && $supports_edit; + + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Add Members')) + ->setIcon('fa-user-plus') + ->setHref("/role/members/{$id}/add/") + ->setWorkflow(true) + ->setDisabled(!$can_add)); + + $can_lock = $can_edit && $supports_edit && $this->hasApplicationCapability( + RoleCanLockRolesCapability::CAPABILITY); + + if ($is_locked) { + $lock_name = pht('Unlock Role'); + $lock_icon = 'fa-unlock'; + } else { + $lock_name = pht('Lock Role'); + $lock_icon = 'fa-lock'; + } + + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName($lock_name) + ->setIcon($lock_icon) + ->setHref($this->getApplicationURI("lock/{$id}/")) + ->setDisabled(!$can_lock) + ->setWorkflow(true)); + + if ($role->isMilestone()) { + $icon_key = PhabricatorRoleIconSet::getMilestoneIconKey(); + $header = PhabricatorRoleIconSet::getIconName($icon_key); + $note = pht( + 'Members of the parent role are members of this role.'); + $show_join = false; + } else if ($role->getHasSubroles()) { + $header = pht('Parent Role'); + $note = pht( + 'Members of all subroles are members of this role.'); + $show_join = false; + } else if ($role->getIsMembershipLocked()) { + $header = pht('Locked Role'); + $note = pht( + 'Users with access may join this role, but may not leave.'); + $show_join = true; + } else { + $header = pht('Normal Role'); + $note = pht('Users with access may join and leave this role.'); + $show_join = true; + } + + $curtain->newPanel() + ->setHeaderText($header) + ->appendChild($note); + + if ($show_join) { + $descriptions = PhabricatorPolicyQuery::renderPolicyDescriptions( + $viewer, + $role); + + $curtain->newPanel() + ->setHeaderText(pht('Joinable By')) + ->appendChild($descriptions[PhabricatorPolicyCapability::CAN_JOIN]); + } + + return $curtain; + } + + private function isRoleSilenced(PhabricatorRole $role) { + $viewer = $this->getViewer(); + + $viewer_phid = $viewer->getPHID(); + if (!$viewer_phid) { + return false; + } + + $edge_type = PhabricatorRoleSilencedEdgeType::EDGECONST; + $silenced = PhabricatorEdgeQuery::loadDestinationPHIDs( + $role->getPHID(), + $edge_type); + $silenced = array_fuse($silenced); + return isset($silenced[$viewer_phid]); + } + +} diff --git a/src/applications/roles/controller/PhabricatorRoleMenuItemController.php b/src/applications/roles/controller/PhabricatorRoleMenuItemController.php new file mode 100644 index 0000000000..657f46ea68 --- /dev/null +++ b/src/applications/roles/controller/PhabricatorRoleMenuItemController.php @@ -0,0 +1,24 @@ +loadRole(); + if ($response) { + return $response; + } + + $viewer = $this->getViewer(); + $role = $this->getRole(); + + $engine = id(new PhabricatorRoleProfileMenuEngine()) + ->setProfileObject($role) + ->setController($this); + + $this->setProfileMenuEngine($engine); + + return $engine->buildResponse(); + } + +} diff --git a/src/applications/roles/controller/PhabricatorRoleMoveController.php b/src/applications/roles/controller/PhabricatorRoleMoveController.php new file mode 100644 index 0000000000..c57a0a252c --- /dev/null +++ b/src/applications/roles/controller/PhabricatorRoleMoveController.php @@ -0,0 +1,147 @@ +getViewer(); + $id = $request->getURIData('id'); + + $request->validateCSRF(); + + $column_phid = $request->getStr('columnPHID'); + $object_phid = $request->getStr('objectPHID'); + + $after_phids = $request->getStrList('afterPHIDs'); + $before_phids = $request->getStrList('beforePHIDs'); + + $order = $request->getStr('order'); + if ($order === null || $order === '') { + $order = PhabricatorRoleColumnNaturalOrder::ORDERKEY; + } + + $ordering = PhabricatorRoleColumnOrder::getOrderByKey($order); + $ordering = id(clone $ordering) + ->setViewer($viewer); + + $edit_header = null; + $raw_header = $request->getStr('header'); + if ($raw_header !== null && $raw_header !== '') { + $edit_header = phutil_json_decode($raw_header); + } else { + $edit_header = array(); + } + + $role = id(new PhabricatorRoleQuery()) + ->setViewer($viewer) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + )) + ->withIDs(array($id)) + ->executeOne(); + if (!$role) { + return new Aphront404Response(); + } + + $cancel_uri = $this->getApplicationURI( + new PhutilURI( + urisprintf('board/%d/', $role->getID()), + array( + 'order' => $order, + ))); + + $board_phid = $role->getPHID(); + + $object = id(new ManiphestTaskQuery()) + ->setViewer($viewer) + ->withPHIDs(array($object_phid)) + ->needRolePHIDs(true) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + + if (!$object) { + return new Aphront404Response(); + } + + $columns = id(new PhabricatorRoleColumnQuery()) + ->setViewer($viewer) + ->withRolePHIDs(array($role->getPHID())) + ->needTriggers(true) + ->execute(); + + $columns = mpull($columns, null, 'getPHID'); + $column = idx($columns, $column_phid); + if (!$column) { + // User is trying to drop this object into a nonexistent column, just kick + // them out. + return new Aphront404Response(); + } + + $engine = id(new PhabricatorRoleBoardLayoutEngine()) + ->setViewer($viewer) + ->setBoardPHIDs(array($board_phid)) + ->setObjectPHIDs(array($object_phid)) + ->executeLayout(); + + $order_params = array( + 'afterPHIDs' => $after_phids, + 'beforePHIDs' => $before_phids, + ); + + $xactions = array(); + $xactions[] = id(new ManiphestTransaction()) + ->setTransactionType(PhabricatorTransactions::TYPE_COLUMNS) + ->setNewValue( + array( + array( + 'columnPHID' => $column->getPHID(), + ) + $order_params, + )); + + $header_xactions = $ordering->getColumnTransactions( + $object, + $edit_header); + foreach ($header_xactions as $header_xaction) { + $xactions[] = $header_xaction; + } + + $sounds = array(); + if ($column->canHaveTrigger()) { + $trigger = $column->getTrigger(); + if ($trigger) { + $trigger_xactions = $trigger->newDropTransactions( + $viewer, + $column, + $object); + foreach ($trigger_xactions as $trigger_xaction) { + $xactions[] = $trigger_xaction; + } + + foreach ($trigger->getSoundEffects() as $effect) { + $sounds[] = $effect; + } + } + } + + $editor = id(new ManiphestTransactionEditor()) + ->setActor($viewer) + ->setContinueOnMissingFields(true) + ->setContinueOnNoEffect(true) + ->setContentSourceFromRequest($request) + ->setCancelURI($cancel_uri); + + $editor->applyTransactions($object, $xactions); + + return $this->newCardResponse( + $board_phid, + $object_phid, + $ordering, + $sounds); + } + +} diff --git a/src/applications/roles/controller/PhabricatorRoleProfileController.php b/src/applications/roles/controller/PhabricatorRoleProfileController.php new file mode 100644 index 0000000000..476abb77da --- /dev/null +++ b/src/applications/roles/controller/PhabricatorRoleProfileController.php @@ -0,0 +1,334 @@ +loadRole(); + if ($response) { + return $response; + } + + $viewer = $request->getUser(); + $role = $this->getRole(); + $id = $role->getID(); + $picture = $role->getProfileImageURI(); + $icon = $role->getDisplayIconIcon(); + $icon_name = $role->getDisplayIconName(); + $tag = id(new PHUITagView()) + ->setIcon($icon) + ->setName($icon_name) + ->addClass('project-view-header-tag') + ->setType(PHUITagView::TYPE_SHADE); + + $header = id(new PHUIHeaderView()) + ->setHeader(array($role->getDisplayName(), $tag)) + ->setUser($viewer) + ->setPolicyObject($role) + ->setProfileHeader(true); + + if ($role->getStatus() == PhabricatorRoleStatus::STATUS_ACTIVE) { + $header->setStatus('fa-check', 'bluegrey', pht('Active')); + } else { + $header->setStatus('fa-ban', 'red', pht('Archived')); + } + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $role, + PhabricatorPolicyCapability::CAN_EDIT); + + if ($can_edit) { + $header->setImageEditURL($this->getApplicationURI("picture/{$id}/")); + } + + $properties = $this->buildPropertyListView($role); + + $watch_action = $this->renderWatchAction($role); + $header->addActionLink($watch_action); + + $subtype = $role->newSubtypeObject(); + if ($subtype && $subtype->hasTagView()) { + $subtype_tag = $subtype->newTagView(); + $header->addTag($subtype_tag); + } + + $milestone_list = $this->buildMilestoneList($role); + $subrole_list = $this->buildSubroleList($role); + + $member_list = id(new PhabricatorRoleMemberListView()) + ->setUser($viewer) + ->setRole($role) + ->setLimit(10) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->setUserPHIDs($role->getMemberPHIDs()); + + $watcher_list = id(new PhabricatorRoleWatcherListView()) + ->setUser($viewer) + ->setRole($role) + ->setLimit(10) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->setUserPHIDs($role->getWatcherPHIDs()); + + $nav = $this->newNavigation( + $role, + PhabricatorRole::ITEM_PROFILE); + + $query = id(new PhabricatorFeedQuery()) + ->setViewer($viewer) + ->withFilterPHIDs(array($role->getPHID())) + ->setLimit(50) + ->setReturnPartialResultsOnOverheat(true); + + $stories = $query->execute(); + + $overheated_view = null; + $is_overheated = $query->getIsOverheated(); + if ($is_overheated) { + $overheated_message = + PhabricatorApplicationSearchController::newOverheatedError( + (bool)$stories); + + $overheated_view = id(new PHUIInfoView()) + ->setSeverity(PHUIInfoView::SEVERITY_WARNING) + ->setTitle(pht('Query Overheated')) + ->setErrors( + array( + $overheated_message, + )); + } + + $view_all = id(new PHUIButtonView()) + ->setTag('a') + ->setIcon( + id(new PHUIIconView()) + ->setIcon('fa-list-ul')) + ->setText(pht('View All')) + ->setHref('/feed/?rolePHIDs='.$role->getPHID()); + + $feed_header = id(new PHUIHeaderView()) + ->setHeader(pht('Recent Activity')) + ->addActionLink($view_all); + + $feed = $this->renderStories($stories); + $feed = id(new PHUIObjectBoxView()) + ->setHeader($feed_header) + ->addClass('project-view-feed') + ->appendChild( + array( + $overheated_view, + $feed, + )); + + require_celerity_resource('project-view-css'); + + $home = id(new PHUITwoColumnView()) + ->setHeader($header) + ->addClass('project-view-home') + ->addClass('project-view-people-home') + ->setMainColumn( + array( + $properties, + $feed, + )) + ->setSideColumn( + array( + $milestone_list, + $subrole_list, + $member_list, + $watcher_list, + )); + + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->setBorder(true); + + return $this->newPage() + ->setNavigation($nav) + ->setCrumbs($crumbs) + ->setTitle($role->getDisplayName()) + ->setPageObjectPHIDs(array($role->getPHID())) + ->appendChild($home); + } + + private function buildPropertyListView( + PhabricatorRole $role) { + $request = $this->getRequest(); + $viewer = $request->getUser(); + + $view = id(new PHUIPropertyListView()) + ->setUser($viewer) + ->setObject($role); + + $field_list = PhabricatorCustomField::getObjectFields( + $role, + PhabricatorCustomField::ROLE_VIEW); + $field_list->appendFieldsToPropertyList($role, $viewer, $view); + + if (!$view->hasAnyProperties()) { + return null; + } + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Details')); + + $view = id(new PHUIObjectBoxView()) + ->setHeader($header) + ->appendChild($view) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->addClass('project-view-properties'); + + return $view; + } + + private function renderStories(array $stories) { + assert_instances_of($stories, 'PhabricatorFeedStory'); + + $builder = new PhabricatorFeedBuilder($stories); + $builder->setUser($this->getRequest()->getUser()); + $builder->setShowHovercards(true); + $view = $builder->buildView(); + + return $view; + } + + private function renderWatchAction(PhabricatorRole $role) { + $viewer = $this->getViewer(); + $id = $role->getID(); + + if (!$viewer->isLoggedIn()) { + $is_watcher = false; + $is_ancestor = false; + } else { + $viewer_phid = $viewer->getPHID(); + $is_watcher = $role->isUserWatcher($viewer_phid); + $is_ancestor = $role->isUserAncestorWatcher($viewer_phid); + } + + if ($is_ancestor && !$is_watcher) { + $watch_icon = 'fa-eye'; + $watch_text = pht('Watching Ancestor'); + $watch_href = "/role/watch/{$id}/?via=profile"; + $watch_disabled = true; + } else if (!$is_watcher) { + $watch_icon = 'fa-eye'; + $watch_text = pht('Watch Role'); + $watch_href = "/role/watch/{$id}/?via=profile"; + $watch_disabled = false; + } else { + $watch_icon = 'fa-eye-slash'; + $watch_text = pht('Unwatch Role'); + $watch_href = "/role/unwatch/{$id}/?via=profile"; + $watch_disabled = false; + } + + $watch_icon = id(new PHUIIconView()) + ->setIcon($watch_icon); + + return id(new PHUIButtonView()) + ->setTag('a') + ->setWorkflow(true) + ->setIcon($watch_icon) + ->setText($watch_text) + ->setHref($watch_href) + ->setDisabled($watch_disabled); + } + + private function buildMilestoneList(PhabricatorRole $role) { + if (!$role->getHasMilestones()) { + return null; + } + + $viewer = $this->getViewer(); + $id = $role->getID(); + + $milestones = id(new PhabricatorRoleQuery()) + ->setViewer($viewer) + ->withParentRolePHIDs(array($role->getPHID())) + ->needImages(true) + ->withIsMilestone(true) + ->withStatuses( + array( + PhabricatorRoleStatus::STATUS_ACTIVE, + )) + ->setOrderVector(array('milestoneNumber', 'id')) + ->execute(); + if (!$milestones) { + return null; + } + + $milestone_list = id(new PhabricatorRoleListView()) + ->setUser($viewer) + ->setRoles($milestones) + ->renderList(); + + $view_all = id(new PHUIButtonView()) + ->setTag('a') + ->setIcon( + id(new PHUIIconView()) + ->setIcon('fa-list-ul')) + ->setText(pht('View All')) + ->setHref("/role/subroles/{$id}/"); + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Milestones')) + ->addActionLink($view_all); + + return id(new PHUIObjectBoxView()) + ->setHeader($header) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->setObjectList($milestone_list); + } + + private function buildSubroleList(PhabricatorRole $role) { + if (!$role->getHasSubroles()) { + return null; + } + + $viewer = $this->getViewer(); + $id = $role->getID(); + + $limit = 25; + + $subroles = id(new PhabricatorRoleQuery()) + ->setViewer($viewer) + ->withParentRolePHIDs(array($role->getPHID())) + ->needImages(true) + ->withStatuses( + array( + PhabricatorRoleStatus::STATUS_ACTIVE, + )) + ->withIsMilestone(false) + ->setLimit($limit) + ->execute(); + if (!$subroles) { + return null; + } + + $subrole_list = id(new PhabricatorRoleListView()) + ->setUser($viewer) + ->setRoles($subroles) + ->renderList(); + + $view_all = id(new PHUIButtonView()) + ->setTag('a') + ->setIcon( + id(new PHUIIconView()) + ->setIcon('fa-list-ul')) + ->setText(pht('View All')) + ->setHref("/role/subroles/{$id}/"); + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Subroles')) + ->addActionLink($view_all); + + return id(new PHUIObjectBoxView()) + ->setHeader($header) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->setObjectList($subrole_list); + } + +} diff --git a/src/applications/roles/controller/PhabricatorRoleReportsController.php b/src/applications/roles/controller/PhabricatorRoleReportsController.php new file mode 100644 index 0000000000..92601cb1d5 --- /dev/null +++ b/src/applications/roles/controller/PhabricatorRoleReportsController.php @@ -0,0 +1,74 @@ +getViewer(); + + $response = $this->loadRole(); + if ($response) { + return $response; + } + + $role = $this->getRole(); + $id = $role->getID(); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $role, + PhabricatorPolicyCapability::CAN_EDIT); + + $nav = $this->newNavigation( + $role, + PhabricatorRole::ITEM_REPORTS); + + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb(pht('Reports')); + $crumbs->setBorder(true); + + $chart_panel = id(new PhabricatorRoleBurndownChartEngine()) + ->setViewer($viewer) + ->setRoles(array($role)) + ->buildChartPanel(); + + $chart_panel->setName(pht('%s: Burndown', $role->getName())); + + $chart_view = id(new PhabricatorDashboardPanelRenderingEngine()) + ->setViewer($viewer) + ->setPanel($chart_panel) + ->setParentPanelPHIDs(array()) + ->renderPanel(); + + $activity_panel = id(new PhabricatorRoleActivityChartEngine()) + ->setViewer($viewer) + ->setRoles(array($role)) + ->buildChartPanel(); + + $activity_panel->setName(pht('%s: Activity', $role->getName())); + + $activity_view = id(new PhabricatorDashboardPanelRenderingEngine()) + ->setViewer($viewer) + ->setPanel($activity_panel) + ->setParentPanelPHIDs(array()) + ->renderPanel(); + + $view = id(new PHUITwoColumnView()) + ->setFooter( + array( + $chart_view, + $activity_view, + )); + + return $this->newPage() + ->setNavigation($nav) + ->setCrumbs($crumbs) + ->setTitle(array($role->getName(), pht('Reports'))) + ->appendChild($view); + } + +} diff --git a/src/applications/roles/controller/PhabricatorRoleSilenceController.php b/src/applications/roles/controller/PhabricatorRoleSilenceController.php new file mode 100644 index 0000000000..9259319efe --- /dev/null +++ b/src/applications/roles/controller/PhabricatorRoleSilenceController.php @@ -0,0 +1,87 @@ +getViewer(); + $id = $request->getURIData('id'); + $action = $request->getURIData('action'); + + $role = id(new PhabricatorRoleQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->needMembers(true) + ->executeOne(); + if (!$role) { + return new Aphront404Response(); + } + + $edge_type = PhabricatorRoleSilencedEdgeType::EDGECONST; + $done_uri = "/role/members/{$id}/"; + $viewer_phid = $viewer->getPHID(); + + if (!$role->isUserMember($viewer_phid)) { + return $this->newDialog() + ->setTitle(pht('Not a Member')) + ->appendParagraph( + pht( + 'You are not a role member, so you do not receive mail sent '. + 'to members of this role.')) + ->addCancelButton($done_uri); + } + + $silenced = PhabricatorEdgeQuery::loadDestinationPHIDs( + $role->getPHID(), + $edge_type); + $silenced = array_fuse($silenced); + $is_silenced = isset($silenced[$viewer_phid]); + + if ($request->isDialogFormPost()) { + if ($is_silenced) { + $edge_action = '-'; + } else { + $edge_action = '+'; + } + + $xactions = array(); + $xactions[] = id(new PhabricatorRoleTransaction()) + ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) + ->setMetadataValue('edge:type', $edge_type) + ->setNewValue( + array( + $edge_action => array($viewer_phid => $viewer_phid), + )); + + $editor = id(new PhabricatorRoleTransactionEditor()) + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true) + ->applyTransactions($role, $xactions); + + return id(new AphrontRedirectResponse())->setURI($done_uri); + } + + if ($is_silenced) { + $title = pht('Enable Mail'); + $body = pht( + 'When mail is sent to members of this role, you will receive a '. + 'copy.'); + $button = pht('Enable Role Mail'); + } else { + $title = pht('Disable Mail'); + $body = pht( + 'When mail is sent to members of this role, you will no longer '. + 'receive a copy.'); + $button = pht('Disable Role Mail'); + } + + return $this->newDialog() + ->setTitle($title) + ->appendParagraph($body) + ->addCancelButton($done_uri) + ->addSubmitButton($button); + } + +} diff --git a/src/applications/roles/controller/PhabricatorRoleSubroleWarningController.php b/src/applications/roles/controller/PhabricatorRoleSubroleWarningController.php new file mode 100644 index 0000000000..341c49d89a --- /dev/null +++ b/src/applications/roles/controller/PhabricatorRoleSubroleWarningController.php @@ -0,0 +1,51 @@ +getViewer(); + + $response = $this->loadRole(); + if ($response) { + return $response; + } + + $role = $this->getRole(); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $role, + PhabricatorPolicyCapability::CAN_EDIT); + + if (!$can_edit) { + return new Aphront404Response(); + } + + $id = $role->getID(); + $cancel_uri = "/role/subroles/{$id}/"; + $done_uri = "/role/edit/?parent={$id}"; + + if ($request->isFormPost()) { + return id(new AphrontRedirectResponse()) + ->setURI($done_uri); + } + + $doc_href = PhabricatorEnv::getDoclink('Roles User Guide'); + + $conversion_help = pht( + "Creating a role's first subrole **moves all ". + "members** to become members of the subrole instead.". + "\n\n". + "See [[ %s | Roles User Guide ]] in the documentation for details. ". + "This process can not be undone.", + $doc_href); + + return $this->newDialog() + ->setTitle(pht('Convert to Parent Role')) + ->appendChild(new PHUIRemarkupView($viewer, $conversion_help)) + ->addCancelButton($cancel_uri) + ->addSubmitButton(pht('Convert Role')); + } + +} diff --git a/src/applications/roles/controller/PhabricatorRoleSubrolesController.php b/src/applications/roles/controller/PhabricatorRoleSubrolesController.php new file mode 100644 index 0000000000..4e0c25794f --- /dev/null +++ b/src/applications/roles/controller/PhabricatorRoleSubrolesController.php @@ -0,0 +1,227 @@ +getViewer(); + + $response = $this->loadRole(); + if ($response) { + return $response; + } + + $role = $this->getRole(); + $id = $role->getID(); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $role, + PhabricatorPolicyCapability::CAN_EDIT); + + $allows_subroles = $role->supportsSubroles(); + $allows_milestones = $role->supportsMilestones(); + + $subrole_list = null; + $milestone_list = null; + + if ($allows_subroles) { + $subroles = id(new PhabricatorRoleQuery()) + ->setViewer($viewer) + ->withParentRolePHIDs(array($role->getPHID())) + ->needImages(true) + ->withIsMilestone(false) + ->execute(); + + $subrole_list = id(new PHUIObjectBoxView()) + ->setHeaderText(pht('%s Subroles', $role->getName())) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->setObjectList( + id(new PhabricatorRoleListView()) + ->setUser($viewer) + ->setRoles($subroles) + ->setNoDataString(pht('This role has no subroles.')) + ->renderList()); + } else { + $subroles = array(); + } + + if ($allows_milestones) { + $milestones = id(new PhabricatorRoleQuery()) + ->setViewer($viewer) + ->withParentRolePHIDs(array($role->getPHID())) + ->needImages(true) + ->withIsMilestone(true) + ->setOrderVector(array('milestoneNumber', 'id')) + ->execute(); + + $milestone_list = id(new PHUIObjectBoxView()) + ->setHeaderText(pht('%s Milestones', $role->getName())) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->setObjectList( + id(new PhabricatorRoleListView()) + ->setUser($viewer) + ->setRoles($milestones) + ->setNoDataString(pht('This role has no milestones.')) + ->renderList()); + } else { + $milestones = array(); + } + + $curtain = $this->buildCurtainView( + $role, + $milestones, + $subroles); + + $nav = $this->newNavigation( + $role, + PhabricatorRole::ITEM_SUBROLES); + + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb(pht('Subroles')); + $crumbs->setBorder(true); + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Subroles and Milestones')) + ->setHeaderIcon('fa-sitemap'); + + require_celerity_resource('project-view-css'); + + // This page isn't reachable via UI, but make it pretty anyways. + $info_view = null; + if (!$milestone_list && !$subrole_list) { + $info_view = id(new PHUIInfoView()) + ->setSeverity(PHUIInfoView::SEVERITY_NOTICE) + ->appendChild(pht('Milestone roles do not support subroles '. + 'or milestones.')); + } + + $view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setCurtain($curtain) + ->addClass('project-view-home') + ->addClass('project-view-people-home') + ->setMainColumn(array( + $info_view, + $subrole_list, + $milestone_list, + )); + + return $this->newPage() + ->setNavigation($nav) + ->setCrumbs($crumbs) + ->setTitle(array($role->getName(), pht('Subroles'))) + ->appendChild($view); + } + + private function buildCurtainView( + PhabricatorRole $role, + array $milestones, + array $subroles) { + $viewer = $this->getViewer(); + $id = $role->getID(); + + $can_create = $this->hasApplicationCapability( + RoleCreateRolesCapability::CAPABILITY); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $role, + PhabricatorPolicyCapability::CAN_EDIT); + + $allows_subroles = $role->supportsSubroles(); + $allows_milestones = $role->supportsMilestones(); + + $curtain = $this->newCurtainView(); + + $can_subrole = ($can_create && $can_edit && $allows_subroles); + + // If we're offering to create the first subrole, we're going to warn + // the user about the effects before moving forward. + if ($can_subrole && !$subroles) { + $subrole_href = "/role/warning/{$id}/"; + $subrole_disabled = false; + $subrole_workflow = true; + } else { + $subrole_href = "/role/edit/?parent={$id}"; + $subrole_disabled = !$can_subrole; + $subrole_workflow = !$can_subrole; + } + + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Create Subrole')) + ->setIcon('fa-plus') + ->setHref($subrole_href) + ->setDisabled($subrole_disabled) + ->setWorkflow($subrole_workflow)); + + if ($allows_milestones && $milestones) { + $milestone_text = pht('Create Next Milestone'); + } else { + $milestone_text = pht('Create Milestone'); + } + + $can_milestone = ($can_create && $can_edit && $allows_milestones); + $milestone_href = "/role/edit/?milestone={$id}"; + + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName($milestone_text) + ->setIcon('fa-plus') + ->setHref($milestone_href) + ->setDisabled(!$can_milestone) + ->setWorkflow(!$can_milestone)); + + if (!$role->supportsSubroles()) { + $note = pht( + 'This role is a milestone, and milestones may not have '. + 'subroles.'); + } else { + if (!$subroles) { + $note = pht('Subroles can be created for this role.'); + } else { + $note = pht('This role has subroles.'); + } + } + + $curtain->newPanel() + ->setHeaderText(pht('Subroles')) + ->appendChild($note); + + if (!$role->supportsSubroles()) { + $note = pht( + 'This role is already a milestone, and milestones may not '. + 'have their own milestones.'); + } else { + if (!$milestones) { + $note = pht('Milestones can be created for this role.'); + } else { + $note = pht('This role has milestones.'); + } + } + + $curtain->newPanel() + ->setHeaderText(pht('Milestones')) + ->appendChild($note); + + return $curtain; + } + + private function renderStatus($icon, $target, $note) { + $item = id(new PHUIStatusItemView()) + ->setIcon($icon) + ->setTarget(phutil_tag('strong', array(), $target)) + ->setNote($note); + + return id(new PHUIStatusListView()) + ->addItem($item); + } + + + +} diff --git a/src/applications/roles/controller/PhabricatorRoleUpdateController.php b/src/applications/roles/controller/PhabricatorRoleUpdateController.php new file mode 100644 index 0000000000..ce08e17eb5 --- /dev/null +++ b/src/applications/roles/controller/PhabricatorRoleUpdateController.php @@ -0,0 +1,120 @@ +getViewer(); + $id = $request->getURIData('id'); + $action = $request->getURIData('action'); + + $capabilities = array( + PhabricatorPolicyCapability::CAN_VIEW, + ); + + switch ($action) { + case 'join': + $capabilities[] = PhabricatorPolicyCapability::CAN_JOIN; + break; + case 'leave': + break; + default: + return new Aphront404Response(); + } + + $role = id(new PhabricatorRoleQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->needMembers(true) + ->requireCapabilities($capabilities) + ->executeOne(); + if (!$role) { + return new Aphront404Response(); + } + + $done_uri = "/role/members/{$id}/"; + + if (!$role->supportsEditMembers()) { + $copy = pht('Parent roles and milestones do not support adding '. + 'members. You can add members directly to any non-parent subrole.'); + + return $this->newDialog() + ->setTitle(pht('Unsupported Role')) + ->appendParagraph($copy) + ->addCancelButton($done_uri); + } + + if ($request->isFormPost()) { + $edge_action = null; + switch ($action) { + case 'join': + $edge_action = '+'; + break; + case 'leave': + $edge_action = '-'; + break; + } + + $type_member = PhabricatorRoleRoleHasMemberEdgeType::EDGECONST; + + $member_spec = array( + $edge_action => array($viewer->getPHID() => $viewer->getPHID()), + ); + + $xactions = array(); + $xactions[] = id(new PhabricatorRoleTransaction()) + ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) + ->setMetadataValue('edge:type', $type_member) + ->setNewValue($member_spec); + + $editor = id(new PhabricatorRoleTransactionEditor()) + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true) + ->applyTransactions($role, $xactions); + + return id(new AphrontRedirectResponse())->setURI($done_uri); + } + + $is_locked = $role->getIsMembershipLocked(); + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $role, + PhabricatorPolicyCapability::CAN_EDIT); + $can_leave = ($can_edit || !$is_locked); + + $button = null; + if ($action == 'leave') { + if ($can_leave) { + $title = pht('Leave Role'); + $body = pht( + 'Your tremendous contributions to this role will be sorely '. + 'missed. Are you sure you want to leave?'); + $button = pht('Leave Role'); + } else { + $title = pht('Membership Locked'); + $body = pht( + 'Membership for this role is locked. You can not leave.'); + } + } else { + $title = pht('Join Role'); + $body = pht( + 'Join this role? You will become a member and enjoy whatever '. + 'benefits membership may confer.'); + $button = pht('Join Role'); + } + + $dialog = $this->newDialog() + ->setTitle($title) + ->appendParagraph($body) + ->addCancelButton($done_uri); + + if ($button) { + $dialog->addSubmitButton($button); + } + + return $dialog; + } + +} diff --git a/src/applications/roles/controller/PhabricatorRoleViewController.php b/src/applications/roles/controller/PhabricatorRoleViewController.php new file mode 100644 index 0000000000..e550e625fe --- /dev/null +++ b/src/applications/roles/controller/PhabricatorRoleViewController.php @@ -0,0 +1,48 @@ +getRequest(); + $viewer = $request->getViewer(); + + $response = $this->loadRole(); + if ($response) { + return $response; + } + $role = $this->getRole(); + + $engine = $this->getProfileMenuEngine(); + $default = $engine->getDefaultMenuItemConfiguration(); + + // If defaults are broken somehow, serve the manage page. See T13033 for + // discussion. + if ($default) { + $default_key = $default->getBuiltinKey(); + } else { + $default_key = PhabricatorRole::ITEM_MANAGE; + } + + switch ($default_key) { + case PhabricatorRole::ITEM_WORKBOARD: + $controller_object = new PhabricatorRoleBoardViewController(); + break; + case PhabricatorRole::ITEM_PROFILE: + $controller_object = new PhabricatorRoleProfileController(); + break; + case PhabricatorRole::ITEM_MANAGE: + $controller_object = new PhabricatorRoleManageController(); + break; + default: + return $engine->buildResponse(); + } + + return $this->delegateToController($controller_object); + } + +} diff --git a/src/applications/roles/controller/PhabricatorRoleWatchController.php b/src/applications/roles/controller/PhabricatorRoleWatchController.php new file mode 100644 index 0000000000..e11b8c18a1 --- /dev/null +++ b/src/applications/roles/controller/PhabricatorRoleWatchController.php @@ -0,0 +1,115 @@ +getViewer(); + $id = $request->getURIData('id'); + $action = $request->getURIData('action'); + + $role = id(new PhabricatorRoleQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->needMembers(true) + ->needWatchers(true) + ->executeOne(); + if (!$role) { + return new Aphront404Response(); + } + + $via = $request->getStr('via'); + if ($via == 'profile') { + $done_uri = "/role/profile/{$id}/"; + } else { + $done_uri = "/role/members/{$id}/"; + } + + $is_watcher = $role->isUserWatcher($viewer->getPHID()); + $is_ancestor = $role->isUserAncestorWatcher($viewer->getPHID()); + if ($is_ancestor && !$is_watcher) { + $ancestor_phid = $role->getWatchedAncestorPHID($viewer->getPHID()); + $handles = $viewer->loadHandles(array($ancestor_phid)); + $ancestor_handle = $handles[$ancestor_phid]; + + return $this->newDialog() + ->setTitle(pht('Watching Ancestor')) + ->appendParagraph( + pht( + 'You are already watching %s, an ancestor of this role, and '. + 'are thus watching all of its subroles.', + $ancestor_handle->renderTag()->render())) + ->addCancelbutton($done_uri); + } + + if ($request->isDialogFormPost()) { + $edge_action = null; + switch ($action) { + case 'watch': + $edge_action = '+'; + break; + case 'unwatch': + $edge_action = '-'; + break; + } + + $type_watcher = PhabricatorObjectHasWatcherEdgeType::EDGECONST; + $member_spec = array( + $edge_action => array($viewer->getPHID() => $viewer->getPHID()), + ); + + $xactions = array(); + $xactions[] = id(new PhabricatorRoleTransaction()) + ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) + ->setMetadataValue('edge:type', $type_watcher) + ->setNewValue($member_spec); + + $editor = id(new PhabricatorRoleTransactionEditor()) + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true) + ->applyTransactions($role, $xactions); + + return id(new AphrontRedirectResponse())->setURI($done_uri); + } + + $dialog = null; + switch ($action) { + case 'watch': + $title = pht('Watch Role?'); + $body = array(); + $body[] = pht( + 'Watching a role will let you monitor it closely. You will '. + 'receive email and notifications about changes to every object '. + 'tagged with roles you watch.'); + $body[] = pht( + 'Watching a role also watches all subroles and milestones of '. + 'that role.'); + $submit = pht('Watch Role'); + break; + case 'unwatch': + $title = pht('Unwatch Role?'); + $body = pht( + 'You will no longer receive email or notifications about every '. + 'object associated with this role.'); + $submit = pht('Unwatch Role'); + break; + default: + return new Aphront404Response(); + } + + $dialog = $this->newDialog() + ->setTitle($title) + ->addHiddenInput('via', $via) + ->addCancelButton($done_uri) + ->addSubmitButton($submit); + + foreach ((array)$body as $paragraph) { + $dialog->appendParagraph($paragraph); + } + + return $dialog; + } + +} diff --git a/src/applications/roles/controller/trigger/PhabricatorRoleTriggerController.php b/src/applications/roles/controller/trigger/PhabricatorRoleTriggerController.php new file mode 100644 index 0000000000..638f8de8de --- /dev/null +++ b/src/applications/roles/controller/trigger/PhabricatorRoleTriggerController.php @@ -0,0 +1,16 @@ +addTextCrumb( + pht('Triggers'), + $this->getApplicationURI('trigger/')); + + return $crumbs; + } + +} diff --git a/src/applications/roles/controller/trigger/PhabricatorRoleTriggerEditController.php b/src/applications/roles/controller/trigger/PhabricatorRoleTriggerEditController.php new file mode 100644 index 0000000000..84b76c8242 --- /dev/null +++ b/src/applications/roles/controller/trigger/PhabricatorRoleTriggerEditController.php @@ -0,0 +1,299 @@ +getRequest(); + $viewer = $request->getViewer(); + + $id = $request->getURIData('id'); + if ($id) { + $trigger = id(new PhabricatorRoleTriggerQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$trigger) { + return new Aphront404Response(); + } + } else { + $trigger = PhabricatorRoleTrigger::initializeNewTrigger(); + } + + $trigger->setViewer($viewer); + + $column_phid = $request->getStr('columnPHID'); + if ($column_phid) { + $column = id(new PhabricatorRoleColumnQuery()) + ->setViewer($viewer) + ->withPHIDs(array($column_phid)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$column) { + return new Aphront404Response(); + } + $board_uri = $column->getWorkboardURI(); + } else { + $column = null; + $board_uri = null; + } + + if ($board_uri) { + $cancel_uri = $board_uri; + } else if ($trigger->getID()) { + $cancel_uri = $trigger->getURI(); + } else { + $cancel_uri = $this->getApplicationURI('trigger/'); + } + + $v_name = $trigger->getName(); + $v_edit = $trigger->getEditPolicy(); + $v_rules = $trigger->getTriggerRules(); + + $e_name = null; + $e_edit = null; + + $validation_exception = null; + if ($request->isFormPost()) { + try { + $v_name = $request->getStr('name'); + $v_edit = $request->getStr('editPolicy'); + + // Read the JSON rules from the request and convert them back into + // "TriggerRule" objects so we can render the correct form state + // if the user is modifying the rules + $raw_rules = $request->getStr('rules'); + $raw_rules = phutil_json_decode($raw_rules); + + $copy = clone $trigger; + $copy->setRuleset($raw_rules); + $v_rules = $copy->getTriggerRules(); + + $xactions = array(); + if (!$trigger->getID()) { + $xactions[] = $trigger->getApplicationTransactionTemplate() + ->setTransactionType(PhabricatorTransactions::TYPE_CREATE) + ->setNewValue(true); + } + + $xactions[] = $trigger->getApplicationTransactionTemplate() + ->setTransactionType( + PhabricatorRoleTriggerNameTransaction::TRANSACTIONTYPE) + ->setNewValue($v_name); + + $xactions[] = $trigger->getApplicationTransactionTemplate() + ->setTransactionType(PhabricatorTransactions::TYPE_EDIT_POLICY) + ->setNewValue($v_edit); + + $xactions[] = $trigger->getApplicationTransactionTemplate() + ->setTransactionType( + PhabricatorRoleTriggerRulesetTransaction::TRANSACTIONTYPE) + ->setNewValue($raw_rules); + + $editor = $trigger->getApplicationTransactionEditor() + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true); + + $editor->applyTransactions($trigger, $xactions); + + $next_uri = $trigger->getURI(); + + if ($column) { + $column_xactions = array(); + + $column_xactions[] = $column->getApplicationTransactionTemplate() + ->setTransactionType( + PhabricatorRoleColumnTriggerTransaction::TRANSACTIONTYPE) + ->setNewValue($trigger->getPHID()); + + $column_editor = $column->getApplicationTransactionEditor() + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true); + + $column_editor->applyTransactions($column, $column_xactions); + + $next_uri = $column->getWorkboardURI(); + } + + return id(new AphrontRedirectResponse())->setURI($next_uri); + } catch (PhabricatorApplicationTransactionValidationException $ex) { + $validation_exception = $ex; + + $e_name = $ex->getShortMessage( + PhabricatorRoleTriggerNameTransaction::TRANSACTIONTYPE); + + $e_edit = $ex->getShortMessage( + PhabricatorTransactions::TYPE_EDIT_POLICY); + + $trigger->setEditPolicy($v_edit); + } + } + + if ($trigger->getID()) { + $title = $trigger->getObjectName(); + $submit = pht('Save Trigger'); + $header = pht('Edit Trigger: %s', $trigger->getObjectName()); + } else { + $title = pht('New Trigger'); + $submit = pht('Create Trigger'); + $header = pht('New Trigger'); + } + + $form_id = celerity_generate_unique_node_id(); + $table_id = celerity_generate_unique_node_id(); + $create_id = celerity_generate_unique_node_id(); + $input_id = celerity_generate_unique_node_id(); + + $form = id(new AphrontFormView()) + ->setViewer($viewer) + ->setID($form_id); + + if ($column) { + $form->addHiddenInput('columnPHID', $column->getPHID()); + } + + $form->appendControl( + id(new AphrontFormTextControl()) + ->setLabel(pht('Name')) + ->setName('name') + ->setValue($v_name) + ->setError($e_name) + ->setPlaceholder($trigger->getDefaultName())); + + $policies = id(new PhabricatorPolicyQuery()) + ->setViewer($viewer) + ->setObject($trigger) + ->execute(); + + $form->appendControl( + id(new AphrontFormPolicyControl()) + ->setName('editPolicy') + ->setPolicyObject($trigger) + ->setCapability(PhabricatorPolicyCapability::CAN_EDIT) + ->setPolicies($policies) + ->setError($e_edit)); + + $form->appendChild( + phutil_tag( + 'input', + array( + 'type' => 'hidden', + 'name' => 'rules', + 'id' => $input_id, + ))); + + $form->appendChild( + id(new PHUIFormInsetView()) + ->setTitle(pht('Rules')) + ->setDescription( + pht( + 'When a card is dropped into a column which uses this trigger:')) + ->setRightButton( + javelin_tag( + 'a', + array( + 'href' => '#', + 'class' => 'button button-green', + 'id' => $create_id, + 'mustcapture' => true, + ), + pht('New Rule'))) + ->setContent( + javelin_tag( + 'table', + array( + 'id' => $table_id, + 'class' => 'trigger-rules-table', + )))); + + $this->setupEditorBehavior( + $trigger, + $v_rules, + $form_id, + $table_id, + $create_id, + $input_id); + + $form->appendControl( + id(new AphrontFormSubmitControl()) + ->setValue($submit) + ->addCancelButton($cancel_uri)); + + $header = id(new PHUIHeaderView()) + ->setHeader($header); + + $box_view = id(new PHUIObjectBoxView()) + ->setHeader($header) + ->setValidationException($validation_exception) + ->appendChild($form); + + $column_view = id(new PHUITwoColumnView()) + ->setFooter($box_view); + + $crumbs = $this->buildApplicationCrumbs() + ->setBorder(true); + + if ($column) { + $crumbs->addTextCrumb( + pht( + '%s: %s', + $column->getRole()->getDisplayName(), + $column->getName()), + $board_uri); + } + + $crumbs->addTextCrumb($title); + + return $this->newPage() + ->setTitle($title) + ->setCrumbs($crumbs) + ->appendChild($column_view); + } + + private function setupEditorBehavior( + PhabricatorRoleTrigger $trigger, + array $rule_list, + $form_id, + $table_id, + $create_id, + $input_id) { + + $rule_list = mpull($rule_list, 'toDictionary'); + $rule_list = array_values($rule_list); + + $type_list = PhabricatorRoleTriggerRule::getAllTriggerRules(); + + foreach ($type_list as $rule) { + $rule->setViewer($this->getViewer()); + } + $type_list = mpull($type_list, 'newTemplate'); + $type_list = array_values($type_list); + + require_celerity_resource('project-triggers-css'); + + Javelin::initBehavior( + 'trigger-rule-editor', + array( + 'formNodeID' => $form_id, + 'tableNodeID' => $table_id, + 'createNodeID' => $create_id, + 'inputNodeID' => $input_id, + + 'rules' => $rule_list, + 'types' => $type_list, + )); + } + +} diff --git a/src/applications/roles/controller/trigger/PhabricatorRoleTriggerListController.php b/src/applications/roles/controller/trigger/PhabricatorRoleTriggerListController.php new file mode 100644 index 0000000000..2d320bf5ce --- /dev/null +++ b/src/applications/roles/controller/trigger/PhabricatorRoleTriggerListController.php @@ -0,0 +1,16 @@ +setController($this) + ->buildResponse(); + } + +} diff --git a/src/applications/roles/controller/trigger/PhabricatorRoleTriggerViewController.php b/src/applications/roles/controller/trigger/PhabricatorRoleTriggerViewController.php new file mode 100644 index 0000000000..39b2e736d4 --- /dev/null +++ b/src/applications/roles/controller/trigger/PhabricatorRoleTriggerViewController.php @@ -0,0 +1,232 @@ +getRequest(); + $viewer = $request->getViewer(); + + $id = $request->getURIData('id'); + + $trigger = id(new PhabricatorRoleTriggerQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->executeOne(); + if (!$trigger) { + return new Aphront404Response(); + } + $trigger->setViewer($viewer); + + $rules_view = $this->newRulesView($trigger); + $columns_view = $this->newColumnsView($trigger); + + $title = $trigger->getObjectName(); + + $header = id(new PHUIHeaderView()) + ->setHeader($trigger->getDisplayName()); + + $timeline = $this->buildTransactionTimeline( + $trigger, + new PhabricatorRoleTriggerTransactionQuery()); + $timeline->setShouldTerminate(true); + + $curtain = $this->newCurtain($trigger); + + $column_view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setCurtain($curtain) + ->setMainColumn( + array( + $rules_view, + $columns_view, + $timeline, + )); + + $crumbs = $this->buildApplicationCrumbs() + ->addTextCrumb($trigger->getObjectName()) + ->setBorder(true); + + return $this->newPage() + ->setTitle($title) + ->setCrumbs($crumbs) + ->appendChild($column_view); + } + + private function newColumnsView(PhabricatorRoleTrigger $trigger) { + $viewer = $this->getViewer(); + + // NOTE: When showing columns which use this trigger, we want to represent + // all columns the trigger is used by: even columns the user can't see. + + // If we hide columns the viewer can't see, they might think that the + // trigger isn't widely used and is safe to edit, when it may actually + // be in use on workboards they don't have access to. + + // Query the columns with the omnipotent viewer first, then pull out their + // PHIDs and throw the actual objects away. Re-query with the real viewer + // so we load only the columns they can actually see, but have a list of + // all the impacted column PHIDs. + + // (We're also exposing the status of columns the user might not be able + // to see. This technically violates policy, but the trigger usage table + // hints at it anyway and it seems unlikely to ever have any security + // impact, but is useful in assessing whether a trigger is really in use + // or not.) + + $omnipotent_viewer = PhabricatorUser::getOmnipotentUser(); + $all_columns = id(new PhabricatorRoleColumnQuery()) + ->setViewer($omnipotent_viewer) + ->withTriggerPHIDs(array($trigger->getPHID())) + ->execute(); + $column_map = mpull($all_columns, 'getStatus', 'getPHID'); + + if ($column_map) { + $visible_columns = id(new PhabricatorRoleColumnQuery()) + ->setViewer($viewer) + ->withPHIDs(array_keys($column_map)) + ->execute(); + $visible_columns = mpull($visible_columns, null, 'getPHID'); + } else { + $visible_columns = array(); + } + + $rows = array(); + foreach ($column_map as $column_phid => $column_status) { + $column = idx($visible_columns, $column_phid); + + if ($column) { + $role = $column->getRole(); + + $role_name = phutil_tag( + 'a', + array( + 'href' => $role->getURI(), + ), + $role->getDisplayName()); + + $column_name = phutil_tag( + 'a', + array( + 'href' => $column->getWorkboardURI(), + ), + $column->getDisplayName()); + } else { + $role_name = null; + $column_name = phutil_tag('em', array(), pht('Restricted Column')); + } + + if ($column_status == PhabricatorRoleColumn::STATUS_ACTIVE) { + $status_icon = id(new PHUIIconView()) + ->setIcon('fa-columns', 'blue') + ->setTooltip(pht('Active Column')); + } else { + $status_icon = id(new PHUIIconView()) + ->setIcon('fa-eye-slash', 'grey') + ->setTooltip(pht('Hidden Column')); + } + + $rows[] = array( + $status_icon, + $role_name, + $column_name, + ); + } + + $table_view = id(new AphrontTableView($rows)) + ->setNoDataString(pht('This trigger is not used by any columns.')) + ->setHeaders( + array( + null, + pht('Role'), + pht('Column'), + )) + ->setColumnClasses( + array( + null, + null, + 'wide pri', + )); + + $header_view = id(new PHUIHeaderView()) + ->setHeader(pht('Used by Columns')); + + return id(new PHUIObjectBoxView()) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->setHeader($header_view) + ->setTable($table_view); + } + + private function newRulesView(PhabricatorRoleTrigger $trigger) { + $viewer = $this->getViewer(); + $rules = $trigger->getTriggerRules(); + + $rows = array(); + foreach ($rules as $rule) { + $value = $rule->getRecord()->getValue(); + + $rows[] = array( + $rule->getRuleViewIcon($value), + $rule->getRuleViewLabel(), + $rule->getRuleViewDescription($value), + ); + } + + $table_view = id(new AphrontTableView($rows)) + ->setNoDataString(pht('This trigger has no rules.')) + ->setHeaders( + array( + null, + pht('Rule'), + pht('Action'), + )) + ->setColumnClasses( + array( + null, + 'pri', + 'wide', + )); + + $header_view = id(new PHUIHeaderView()) + ->setHeader(pht('Trigger Rules')) + ->setSubheader( + pht( + 'When a card is dropped into a column that uses this trigger, '. + 'these actions will be taken.')); + + return id(new PHUIObjectBoxView()) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->setHeader($header_view) + ->setTable($table_view); + } + private function newCurtain(PhabricatorRoleTrigger $trigger) { + $viewer = $this->getViewer(); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $trigger, + PhabricatorPolicyCapability::CAN_EDIT); + + $curtain = $this->newCurtainView($trigger); + + $edit_uri = $this->getApplicationURI( + urisprintf( + 'trigger/edit/%d/', + $trigger->getID())); + + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Edit Trigger')) + ->setIcon('fa-pencil') + ->setHref($edit_uri) + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit)); + + return $curtain; + } + +} diff --git a/src/applications/roles/customfield/PhabricatorRoleConfiguredCustomField.php b/src/applications/roles/customfield/PhabricatorRoleConfiguredCustomField.php new file mode 100644 index 0000000000..0bf214b440 --- /dev/null +++ b/src/applications/roles/customfield/PhabricatorRoleConfiguredCustomField.php @@ -0,0 +1,17 @@ + array( + 'name' => pht('Description'), + 'type' => 'remarkup', + 'description' => pht('Short role description.'), + 'fulltext' => PhabricatorSearchDocumentFieldType::FIELD_BODY, + ), + ), + $internal = true); + } + +} diff --git a/src/applications/roles/customfield/PhabricatorRoleStandardCustomField.php b/src/applications/roles/customfield/PhabricatorRoleStandardCustomField.php new file mode 100644 index 0000000000..c113c8a7fa --- /dev/null +++ b/src/applications/roles/customfield/PhabricatorRoleStandardCustomField.php @@ -0,0 +1,23 @@ +isMilestone = $is_milestone; + return $this; + } + + public function getIsMilestone() { + return $this->isMilestone; + } + + public function getEditorApplicationClass() { + return 'PhabricatorRoleApplication'; + } + + public function getEditorObjectsDescription() { + return pht('Roles'); + } + + public function getCreateObjectTitle($author, $object) { + return pht('%s created this role.', $author); + } + + public function getCreateObjectTitleForFeed($author, $object) { + return pht('%s created %s.', $author, $object); + } + + public function getTransactionTypes() { + $types = parent::getTransactionTypes(); + + $types[] = PhabricatorTransactions::TYPE_EDGE; + $types[] = PhabricatorTransactions::TYPE_VIEW_POLICY; + $types[] = PhabricatorTransactions::TYPE_EDIT_POLICY; + $types[] = PhabricatorTransactions::TYPE_JOIN_POLICY; + + return $types; + } + + protected function validateAllTransactions( + PhabricatorLiskDAO $object, + array $xactions) { + + $errors = array(); + + // Prevent creating roles which are both subroles and milestones, + // since this does not make sense, won't work, and will break everything. + $parent_xaction = null; + foreach ($xactions as $xaction) { + switch ($xaction->getTransactionType()) { + case PhabricatorRoleParentTransaction::TRANSACTIONTYPE: + case PhabricatorRoleMilestoneTransaction::TRANSACTIONTYPE: + if ($xaction->getNewValue() === null) { + continue 2; + } + + if (!$parent_xaction) { + $parent_xaction = $xaction; + continue 2; + } + + $errors[] = new PhabricatorApplicationTransactionValidationError( + $xaction->getTransactionType(), + pht('Invalid'), + pht( + 'When creating a role, specify a maximum of one parent '. + 'role or milestone role. A role can not be both a '. + 'subrole and a milestone.'), + $xaction); + break 2; + } + } + + $is_milestone = $this->getIsMilestone(); + + $is_parent = $object->getHasSubroles(); + + foreach ($xactions as $xaction) { + switch ($xaction->getTransactionType()) { + case PhabricatorTransactions::TYPE_EDGE: + $type = $xaction->getMetadataValue('edge:type'); + if ($type != PhabricatorRoleRoleHasMemberEdgeType::EDGECONST) { + break; + } + + if ($is_parent) { + $errors[] = new PhabricatorApplicationTransactionValidationError( + $xaction->getTransactionType(), + pht('Invalid'), + pht( + 'You can not change members of a role with subroles '. + 'directly. Members of any subrole are automatically '. + 'members of the parent role.'), + $xaction); + } + + if ($is_milestone) { + $errors[] = new PhabricatorApplicationTransactionValidationError( + $xaction->getTransactionType(), + pht('Invalid'), + pht( + 'You can not change members of a milestone. Members of the '. + 'parent role are automatically members of the milestone.'), + $xaction); + } + break; + } + } + + return $errors; + } + + protected function willPublish(PhabricatorLiskDAO $object, array $xactions) { + // NOTE: We're using the omnipotent user here because the original actor + // may no longer have permission to view the object. + return id(new PhabricatorRoleQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withPHIDs(array($object->getPHID())) + ->needAncestorMembers(true) + ->executeOne(); + } + + protected function shouldSendMail( + PhabricatorLiskDAO $object, + array $xactions) { + return true; + } + + protected function getMailSubjectPrefix() { + return pht('[Role]'); + } + + protected function getMailTo(PhabricatorLiskDAO $object) { + return array( + $this->getActingAsPHID(), + ); + } + + protected function getMailCc(PhabricatorLiskDAO $object) { + return array(); + } + + public function getMailTagsMap() { + return array( + PhabricatorRoleTransaction::MAILTAG_METADATA => + pht('Role name, hashtags, icon, image, or color changes.'), + PhabricatorRoleTransaction::MAILTAG_MEMBERS => + pht('Role membership changes.'), + PhabricatorRoleTransaction::MAILTAG_WATCHERS => + pht('Role watcher list changes.'), + PhabricatorRoleTransaction::MAILTAG_OTHER => + pht('Other role activity not listed above occurs.'), + ); + } + + protected function buildReplyHandler(PhabricatorLiskDAO $object) { + return id(new RoleReplyHandler()) + ->setMailReceiver($object); + } + + protected function buildMailTemplate(PhabricatorLiskDAO $object) { + $name = $object->getName(); + + return id(new PhabricatorMetaMTAMail()) + ->setSubject("{$name}"); + } + + protected function buildMailBody( + PhabricatorLiskDAO $object, + array $xactions) { + + $body = parent::buildMailBody($object, $xactions); + + $uri = '/role/profile/'.$object->getID().'/'; + $body->addLinkSection( + pht('ROLE DETAIL'), + PhabricatorEnv::getProductionURI($uri)); + + return $body; + } + + protected function shouldPublishFeedStory( + PhabricatorLiskDAO $object, + array $xactions) { + return true; + } + + protected function supportsSearch() { + return true; + } + + protected function applyFinalEffects( + PhabricatorLiskDAO $object, + array $xactions) { + + $materialize = false; + $new_parent = null; + foreach ($xactions as $xaction) { + switch ($xaction->getTransactionType()) { + case PhabricatorTransactions::TYPE_EDGE: + switch ($xaction->getMetadataValue('edge:type')) { + case PhabricatorRoleRoleHasMemberEdgeType::EDGECONST: + $materialize = true; + break; + } + break; + case PhabricatorRoleParentTransaction::TRANSACTIONTYPE: + case PhabricatorRoleMilestoneTransaction::TRANSACTIONTYPE: + $materialize = true; + $new_parent = $object->getParentRole(); + break; + } + } + + if ($new_parent) { + // If we just created the first subrole of this parent, we want to + // copy all of the real members to the subrole. + if (!$new_parent->getHasSubroles()) { + $member_type = PhabricatorRoleRoleHasMemberEdgeType::EDGECONST; + + $role_members = PhabricatorEdgeQuery::loadDestinationPHIDs( + $new_parent->getPHID(), + $member_type); + + if ($role_members) { + $editor = id(new PhabricatorEdgeEditor()); + foreach ($role_members as $phid) { + $editor->addEdge($object->getPHID(), $member_type, $phid); + } + $editor->save(); + } + } + } + + // TODO: We should dump an informational transaction onto the parent + // role to show that we created the sub-thing. + + if ($materialize) { + id(new PhabricatorRolesMembershipIndexEngineExtension()) + ->rematerialize($object); + } + + if ($new_parent) { + id(new PhabricatorRolesMembershipIndexEngineExtension()) + ->rematerialize($new_parent); + } + + // See PHI1046. Milestones are always in the Space of their parent role. + // Synchronize the database values to match the application values. + $conn = $object->establishConnection('w'); + queryfx( + $conn, + 'UPDATE %R SET spacePHID = %ns + WHERE parentRolePHID = %s AND milestoneNumber IS NOT NULL', + $object, + $object->getSpacePHID(), + $object->getPHID()); + + return parent::applyFinalEffects($object, $xactions); + } + + public function addSlug(PhabricatorRole $role, $slug, $force) { + $slug = PhabricatorSlug::normalizeProjectSlug($slug); + $table = new PhabricatorRoleSlug(); + $role_phid = $role->getPHID(); + + if ($force) { + // If we have the `$force` flag set, we only want to ignore an existing + // slug if it's for the same role. We'll error on collisions with + // other roles. + $current = $table->loadOneWhere( + 'slug = %s AND rolePHID = %s', + $slug, + $role_phid); + } else { + // Without the `$force` flag, we'll just return without doing anything + // if any other role already has the slug. + $current = $table->loadOneWhere( + 'slug = %s', + $slug); + } + + if ($current) { + return; + } + + return id(new PhabricatorRoleSlug()) + ->setSlug($slug) + ->setRolePHID($role_phid) + ->save(); + } + + public function removeSlugs(PhabricatorRole $role, array $slugs) { + if (!$slugs) { + return; + } + + // We're going to try to delete both the literal and normalized versions + // of all slugs. This allows us to destroy old slugs that are no longer + // valid. + foreach ($this->normalizeSlugs($slugs) as $slug) { + $slugs[] = $slug; + } + + $objects = id(new PhabricatorRoleSlug())->loadAllWhere( + 'rolePHID = %s AND slug IN (%Ls)', + $role->getPHID(), + $slugs); + + foreach ($objects as $object) { + $object->delete(); + } + } + + public function normalizeSlugs(array $slugs) { + foreach ($slugs as $key => $slug) { + $slugs[$key] = PhabricatorSlug::normalizeProjectSlug($slug); + } + + $slugs = array_unique($slugs); + $slugs = array_values($slugs); + + return $slugs; + } + + protected function adjustObjectForPolicyChecks( + PhabricatorLiskDAO $object, + array $xactions) { + + $copy = parent::adjustObjectForPolicyChecks($object, $xactions); + + $type_edge = PhabricatorTransactions::TYPE_EDGE; + $edgetype_member = PhabricatorRoleRoleHasMemberEdgeType::EDGECONST; + + // See T13462. If we're creating a milestone, set a dummy milestone + // number so the role behaves like a milestone and uses milestone + // policy rules. Otherwise, we'll end up checking the default policies + // (which are not relevant to milestones) instead of the parent role + // policies (which are the correct policies). + if ($this->getIsMilestone() && !$copy->isMilestone()) { + $copy->setMilestoneNumber(1); + } + + $hint = null; + if ($this->getIsMilestone()) { + // See T13462. If we're creating a milestone, predict that the members + // of the newly created milestone will be the same as the members of the + // parent role, since this is the governing rule. + + $parent = $copy->getParentRole(); + + $parent = id(new PhabricatorRoleQuery()) + ->setViewer($this->getActor()) + ->withPHIDs(array($parent->getPHID())) + ->needMembers(true) + ->executeOne(); + $members = $parent->getMemberPHIDs(); + + $hint = array_fuse($members); + } else { + $member_xaction = null; + foreach ($xactions as $xaction) { + if ($xaction->getTransactionType() !== $type_edge) { + continue; + } + + $edgetype = $xaction->getMetadataValue('edge:type'); + if ($edgetype !== $edgetype_member) { + continue; + } + + $member_xaction = $xaction; + } + + if ($member_xaction) { + $object_phid = $object->getPHID(); + + if ($object_phid) { + $role = id(new PhabricatorRoleQuery()) + ->setViewer($this->getActor()) + ->withPHIDs(array($object_phid)) + ->needMembers(true) + ->executeOne(); + $members = $role->getMemberPHIDs(); + } else { + $members = array(); + } + + $clone_xaction = clone $member_xaction; + $hint = $this->getPHIDTransactionNewValue($clone_xaction, $members); + $hint = array_fuse($hint); + } + } + + if ($hint !== null) { + $rule = new PhabricatorRoleMembersPolicyRule(); + PhabricatorPolicyRule::passTransactionHintToRule( + $copy, + $rule, + $hint); + } + + return $copy; + } + + protected function expandTransactions( + PhabricatorLiskDAO $object, + array $xactions) { + + $actor = $this->getActor(); + $actor_phid = $actor->getPHID(); + + $results = parent::expandTransactions($object, $xactions); + + $is_milestone = $object->isMilestone(); + foreach ($xactions as $xaction) { + switch ($xaction->getTransactionType()) { + case PhabricatorRoleMilestoneTransaction::TRANSACTIONTYPE: + if ($xaction->getNewValue() !== null) { + $is_milestone = true; + } + break; + } + } + + $this->setIsMilestone($is_milestone); + + return $results; + } + + protected function shouldApplyHeraldRules( + PhabricatorLiskDAO $object, + array $xactions) { + return true; + } + + protected function buildHeraldAdapter( + PhabricatorLiskDAO $object, + array $xactions) { + + // Herald rules may run on behalf of other users and need to execute + // membership checks against ancestors. + $role = id(new PhabricatorRoleQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withPHIDs(array($object->getPHID())) + ->needAncestorMembers(true) + ->executeOne(); + + return id(new PhabricatorRoleHeraldAdapter()) + ->setRole($role); + } + +} diff --git a/src/applications/roles/editor/PhabricatorRoleTriggerEditor.php b/src/applications/roles/editor/PhabricatorRoleTriggerEditor.php new file mode 100644 index 0000000000..9f273a73e2 --- /dev/null +++ b/src/applications/roles/editor/PhabricatorRoleTriggerEditor.php @@ -0,0 +1,34 @@ +viewer = $viewer; + return $this; + } + + public function getViewer() { + return $this->viewer; + } + + public function setBoardPHIDs(array $board_phids) { + $this->boardPHIDs = array_fuse($board_phids); + return $this; + } + + public function getBoardPHIDs() { + return $this->boardPHIDs; + } + + public function setObjectPHIDs(array $object_phids) { + $this->objectPHIDs = array_fuse($object_phids); + return $this; + } + + public function getObjectPHIDs() { + return $this->objectPHIDs; + } + + /** + * Fetch all boards, even if the board is disabled. + */ + public function setFetchAllBoards($fetch_all) { + $this->fetchAllBoards = $fetch_all; + return $this; + } + + public function getFetchAllBoards() { + return $this->fetchAllBoards; + } + + public function executeLayout() { + $viewer = $this->getViewer(); + + $boards = $this->loadBoards(); + if (!$boards) { + return $this; + } + + $columns = $this->loadColumns($boards); + $positions = $this->loadPositions($boards); + + foreach ($boards as $board_phid => $board) { + $board_columns = idx($columns, $board_phid); + + // Don't layout boards with no columns. These boards need to be formally + // created first. + if (!$columns) { + continue; + } + + $board_positions = idx($positions, $board_phid, array()); + + $this->layoutBoard($board, $board_columns, $board_positions); + } + + return $this; + } + + public function getColumns($board_phid) { + $columns = idx($this->boardLayout, $board_phid, array()); + return array_select_keys($this->columnMap, array_keys($columns)); + } + + public function getColumnObjectPositions($board_phid, $column_phid) { + $columns = idx($this->boardLayout, $board_phid, array()); + return idx($columns, $column_phid, array()); + } + + + public function getColumnObjectPHIDs($board_phid, $column_phid) { + $positions = $this->getColumnObjectPositions($board_phid, $column_phid); + return mpull($positions, 'getObjectPHID'); + } + + public function getObjectColumns($board_phid, $object_phid) { + $board_map = idx($this->objectColumnMap, $board_phid, array()); + + $column_phids = idx($board_map, $object_phid); + if (!$column_phids) { + return array(); + } + + return array_select_keys($this->columnMap, $column_phids); + } + + public function queueRemovePosition( + $board_phid, + $column_phid, + $object_phid) { + + $board_layout = idx($this->boardLayout, $board_phid, array()); + $positions = idx($board_layout, $column_phid, array()); + $position = idx($positions, $object_phid); + + if ($position) { + $this->remQueue[] = $position; + + // If this position hasn't been saved yet, get it out of the add queue. + if (!$position->getID()) { + foreach ($this->addQueue as $key => $add_position) { + if ($add_position === $position) { + unset($this->addQueue[$key]); + } + } + } + } + + unset($this->boardLayout[$board_phid][$column_phid][$object_phid]); + + return $this; + } + + public function queueAddPosition( + $board_phid, + $column_phid, + $object_phid, + array $after_phids, + array $before_phids) { + + $board_layout = idx($this->boardLayout, $board_phid, array()); + $positions = idx($board_layout, $column_phid, array()); + + // Check if the object is already in the column, and remove it if it is. + $object_position = idx($positions, $object_phid); + unset($positions[$object_phid]); + + if (!$object_position) { + $object_position = id(new PhabricatorRoleColumnPosition()) + ->setBoardPHID($board_phid) + ->setColumnPHID($column_phid) + ->setObjectPHID($object_phid); + } + + if (!$positions) { + $object_position->setSequence(0); + } else { + // The user's view of the board may fall out of date, so they might + // try to drop a card under a different card which is no longer where + // they thought it was. + + // When this happens, we perform the move anyway, since this is almost + // certainly what users want when interacting with the UI. We'l try to + // fall back to another nearby card if the client provided us one. If + // we don't find any of the cards the client specified in the column, + // we'll just move the card to the default position. + + $search_phids = array(); + foreach ($after_phids as $after_phid) { + $search_phids[] = array($after_phid, false); + } + + foreach ($before_phids as $before_phid) { + $search_phids[] = array($before_phid, true); + } + + // This makes us fall back to the default position if we fail every + // candidate position. The default position counts as a "before" position + // because we want to put the new card at the top of the column. + $search_phids[] = array(null, true); + + $found = false; + foreach ($search_phids as $search_position) { + list($relative_phid, $is_before) = $search_position; + foreach ($positions as $position) { + if (!$found) { + if ($relative_phid === null) { + $is_match = true; + } else { + $position_phid = $position->getObjectPHID(); + $is_match = ($relative_phid === $position_phid); + } + + if ($is_match) { + $found = true; + + $sequence = $position->getSequence(); + + if (!$is_before) { + $sequence++; + } + + $object_position->setSequence($sequence++); + + if (!$is_before) { + // If we're inserting after this position, continue the loop so + // we don't update it. + continue; + } + } + } + + if ($found) { + $position->setSequence($sequence++); + $this->addQueue[] = $position; + } + } + + if ($found) { + break; + } + } + } + + $this->addQueue[] = $object_position; + + $positions[$object_phid] = $object_position; + $positions = msortv($positions, 'newColumnPositionOrderVector'); + + $this->boardLayout[$board_phid][$column_phid] = $positions; + + return $this; + } + + public function applyPositionUpdates() { + foreach ($this->remQueue as $position) { + if ($position->getID()) { + $position->delete(); + } + } + $this->remQueue = array(); + + $adds = array(); + $updates = array(); + + foreach ($this->addQueue as $position) { + $id = $position->getID(); + if ($id) { + $updates[$id] = $position; + } else { + $adds[] = $position; + } + } + $this->addQueue = array(); + + $table = new PhabricatorRoleColumnPosition(); + $conn_w = $table->establishConnection('w'); + + $pairs = array(); + foreach ($updates as $id => $position) { + // This is ugly because MySQL gets upset with us if it is configured + // strictly and we attempt inserts which can't work. We'll never actually + // do these inserts since they'll always collide (triggering the ON + // DUPLICATE KEY logic), so we just provide dummy values in order to get + // there. + + $pairs[] = qsprintf( + $conn_w, + '(%d, %d, "", "", "")', + $id, + $position->getSequence()); + } + + if ($pairs) { + queryfx( + $conn_w, + 'INSERT INTO %T (id, sequence, boardPHID, columnPHID, objectPHID) + VALUES %LQ ON DUPLICATE KEY UPDATE sequence = VALUES(sequence)', + $table->getTableName(), + $pairs); + } + + foreach ($adds as $position) { + $position->save(); + } + + return $this; + } + + private function loadBoards() { + $viewer = $this->getViewer(); + $board_phids = $this->getBoardPHIDs(); + + $boards = id(new PhabricatorObjectQuery()) + ->setViewer($viewer) + ->withPHIDs($board_phids) + ->execute(); + $boards = mpull($boards, null, 'getPHID'); + + foreach ($boards as $key => $board) { + if (!($board instanceof PhabricatorRoleWorkboardInterface)) { + unset($boards[$key]); + } + } + + if (!$this->fetchAllBoards) { + foreach ($boards as $key => $board) { + if (!$board->getHasWorkboard()) { + unset($boards[$key]); + } + } + } + + return $boards; + } + + private function loadColumns(array $boards) { + $viewer = $this->getViewer(); + + $columns = id(new PhabricatorRoleColumnQuery()) + ->setViewer($viewer) + ->withRolePHIDs(array_keys($boards)) + ->needTriggers(true) + ->execute(); + $columns = msort($columns, 'getOrderingKey'); + $columns = mpull($columns, null, 'getPHID'); + + $need_children = array(); + foreach ($boards as $phid => $board) { + if ($board->getHasMilestones()) { + $need_children[] = $phid; + } + } + + if ($need_children) { + $children = id(new PhabricatorRoleQuery()) + ->setViewer($viewer) + ->withParentRolePHIDs($need_children) + ->execute(); + $children = mpull($children, null, 'getPHID'); + $children = mgroup($children, 'getParentRolePHID'); + } else { + $children = array(); + } + + $columns = mgroup($columns, 'getRolePHID'); + foreach ($boards as $board_phid => $board) { + $board_columns = idx($columns, $board_phid, array()); + + // If the role has milestones, create any missing columns. + if ($board->getHasMilestones()) { + $child_roles = idx($children, $board_phid, array()); + + if ($board_columns) { + $next_sequence = last($board_columns)->getSequence() + 1; + } else { + $next_sequence = 1; + } + + $proxy_columns = mpull($board_columns, null, 'getProxyPHID'); + foreach ($child_roles as $child_phid => $child) { + if (isset($proxy_columns[$child_phid])) { + continue; + } + + $new_column = PhabricatorRoleColumn::initializeNewColumn($viewer) + ->attachRole($board) + ->attachProxy($child) + ->setSequence($next_sequence++) + ->setRolePHID($board_phid) + ->setProxyPHID($child_phid); + + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); + $new_column->save(); + unset($unguarded); + + $board_columns[$new_column->getPHID()] = $new_column; + } + } + + $board_columns = msort($board_columns, 'getOrderingKey'); + + $columns[$board_phid] = $board_columns; + } + + foreach ($columns as $board_phid => $board_columns) { + foreach ($board_columns as $board_column) { + $column_phid = $board_column->getPHID(); + $this->columnMap[$column_phid] = $board_column; + } + } + + return $columns; + } + + private function loadPositions(array $boards) { + $viewer = $this->getViewer(); + + $object_phids = $this->getObjectPHIDs(); + if (!$object_phids) { + return array(); + } + + $positions = id(new PhabricatorRoleColumnPositionQuery()) + ->setViewer($viewer) + ->withBoardPHIDs(array_keys($boards)) + ->withObjectPHIDs($object_phids) + ->execute(); + $positions = msortv($positions, 'newColumnPositionOrderVector'); + $positions = mgroup($positions, 'getBoardPHID'); + + return $positions; + } + + private function layoutBoard( + $board, + array $columns, + array $positions) { + + $viewer = $this->getViewer(); + + $board_phid = $board->getPHID(); + $position_groups = mgroup($positions, 'getObjectPHID'); + + $layout = array(); + $default_phid = null; + foreach ($columns as $column) { + $column_phid = $column->getPHID(); + $layout[$column_phid] = array(); + + if ($column->isDefaultColumn()) { + $default_phid = $column_phid; + } + } + + // Find all the columns which are proxies for other objects. + $proxy_map = array(); + foreach ($columns as $column) { + $proxy_phid = $column->getProxyPHID(); + if ($proxy_phid) { + $proxy_map[$proxy_phid] = $column->getPHID(); + } + } + + $object_phids = $this->getObjectPHIDs(); + + // If we have proxies, we need to force cards into the correct proxy + // columns. + if ($proxy_map && $object_phids) { + $edge_query = id(new PhabricatorEdgeQuery()) + ->withSourcePHIDs($object_phids) + ->withEdgeTypes( + array( + PhabricatorRoleObjectHasRoleEdgeType::EDGECONST, + )); + $edge_query->execute(); + + $role_phids = $edge_query->getDestinationPHIDs(); + $role_phids = array_fuse($role_phids); + } else { + $role_phids = array(); + } + + if ($role_phids) { + $roles = id(new PhabricatorRoleQuery()) + ->setViewer($viewer) + ->withPHIDs($role_phids) + ->execute(); + $roles = mpull($roles, null, 'getPHID'); + } else { + $roles = array(); + } + + // Build a map from every role that any task is tagged with to the + // ancestor role which has a column on this board, if one exists. + $ancestor_map = array(); + foreach ($roles as $phid => $role) { + if (isset($proxy_map[$phid])) { + $ancestor_map[$phid] = $proxy_map[$phid]; + } else { + $seen = array($phid); + foreach ($role->getAncestorRoles() as $ancestor) { + $ancestor_phid = $ancestor->getPHID(); + $seen[] = $ancestor_phid; + if (isset($proxy_map[$ancestor_phid])) { + foreach ($seen as $role_phid) { + $ancestor_map[$role_phid] = $proxy_map[$ancestor_phid]; + } + } + } + } + } + + $view_sequence = 1; + foreach ($object_phids as $object_phid) { + $positions = idx($position_groups, $object_phid, array()); + + // First, check for objects that have corresponding proxy columns. We're + // going to overwrite normal column positions if a tag belongs to a proxy + // column, since you can't be in normal columns if you're in proxy + // columns. + $proxy_hits = array(); + if ($proxy_map) { + $object_role_phids = $edge_query->getDestinationPHIDs( + array( + $object_phid, + )); + + foreach ($object_role_phids as $role_phid) { + if (isset($ancestor_map[$role_phid])) { + $proxy_hits[] = $ancestor_map[$role_phid]; + } + } + } + + if ($proxy_hits) { + // TODO: For now, only one column hit is permissible. + $proxy_hits = array_slice($proxy_hits, 0, 1); + + $proxy_hits = array_fuse($proxy_hits); + + // Check the object positions: we hope to find a position in each + // column the object should be part of. We're going to drop any + // invalid positions and create new positions where positions are + // missing. + foreach ($positions as $key => $position) { + $column_phid = $position->getColumnPHID(); + if (isset($proxy_hits[$column_phid])) { + // Valid column, mark the position as found. + unset($proxy_hits[$column_phid]); + } else { + // Invalid column, ignore the position. + unset($positions[$key]); + } + } + + // Create new positions for anything we haven't found. + foreach ($proxy_hits as $proxy_hit) { + $new_position = id(new PhabricatorRoleColumnPosition()) + ->setBoardPHID($board_phid) + ->setColumnPHID($proxy_hit) + ->setObjectPHID($object_phid) + ->setSequence(0) + ->setViewSequence($view_sequence++); + + $this->addQueue[] = $new_position; + + $positions[] = $new_position; + } + } else { + // Ignore any positions in columns which no longer exist. We don't + // actively destory them because the rest of the code ignores them and + // there's no real need to destroy the data. + foreach ($positions as $key => $position) { + $column_phid = $position->getColumnPHID(); + if (empty($columns[$column_phid])) { + unset($positions[$key]); + } + } + + // If the object has no position, put it on the default column if + // one exists. + if (!$positions && $default_phid) { + $new_position = id(new PhabricatorRoleColumnPosition()) + ->setBoardPHID($board_phid) + ->setColumnPHID($default_phid) + ->setObjectPHID($object_phid) + ->setSequence(0) + ->setViewSequence($view_sequence++); + + $this->addQueue[] = $new_position; + + $positions = array( + $new_position, + ); + } + } + + foreach ($positions as $position) { + $column_phid = $position->getColumnPHID(); + $layout[$column_phid][$object_phid] = $position; + } + } + + foreach ($layout as $column_phid => $map) { + $map = msortv($map, 'newColumnPositionOrderVector'); + $layout[$column_phid] = $map; + + foreach ($map as $object_phid => $position) { + $this->objectColumnMap[$board_phid][$object_phid][] = $column_phid; + } + } + + $this->boardLayout[$board_phid] = $layout; + } + +} diff --git a/src/applications/roles/engine/PhabricatorRoleBoardRenderingEngine.php b/src/applications/roles/engine/PhabricatorRoleBoardRenderingEngine.php new file mode 100644 index 0000000000..b9b7ee4ffa --- /dev/null +++ b/src/applications/roles/engine/PhabricatorRoleBoardRenderingEngine.php @@ -0,0 +1,147 @@ +viewer = $viewer; + return $this; + } + + public function getViewer() { + return $this->viewer; + } + + public function setObjects(array $objects) { + $this->objects = mpull($objects, null, 'getPHID'); + return $this; + } + + public function getObjects() { + return $this->objects; + } + + public function setExcludedRolePHIDs(array $phids) { + $this->excludedRolePHIDs = $phids; + return $this; + } + + public function getExcludedRolePHIDs() { + return $this->excludedRolePHIDs; + } + + public function setEditMap(array $edit_map) { + $this->editMap = $edit_map; + return $this; + } + + public function getEditMap() { + return $this->editMap; + } + + public function renderCard($phid) { + $this->willRender(); + + $viewer = $this->getViewer(); + $object = idx($this->getObjects(), $phid); + + $card = id(new RoleBoardTaskCard()) + ->setViewer($viewer) + ->setTask($object) + ->setShowEditControls(true) + ->setCanEdit($this->getCanEdit($phid)); + + $owner_phid = $object->getOwnerPHID(); + if ($owner_phid) { + $owner_handle = $this->handles[$owner_phid]; + $card->setOwner($owner_handle); + } + + $role_phids = $object->getRolePHIDs(); + $role_handles = array_select_keys($this->handles, $role_phids); + if ($role_handles) { + $card + ->setHideArchivedRoles(true) + ->setRoleHandles($role_handles); + } + + $cover_phid = $object->getCoverImageThumbnailPHID(); + if ($cover_phid) { + $cover_file = idx($this->coverFiles, $cover_phid); + if ($cover_file) { + $card->setCoverImageFile($cover_file); + } + } + + return $card; + } + + private function willRender() { + if ($this->loaded) { + return; + } + + $phids = array(); + foreach ($this->objects as $object) { + $owner_phid = $object->getOwnerPHID(); + if ($owner_phid) { + $phids[$owner_phid] = $owner_phid; + } + + foreach ($object->getRolePHIDs() as $phid) { + $phids[$phid] = $phid; + } + } + + if ($this->excludedRolePHIDs) { + foreach ($this->excludedRolePHIDs as $excluded_phid) { + unset($phids[$excluded_phid]); + } + } + + $viewer = $this->getViewer(); + + $handles = $viewer->loadHandles($phids); + $handles = iterator_to_array($handles); + $this->handles = $handles; + + $cover_phids = array(); + foreach ($this->objects as $object) { + $cover_phid = $object->getCoverImageThumbnailPHID(); + if ($cover_phid) { + $cover_phids[$cover_phid] = $cover_phid; + } + } + + if ($cover_phids) { + $cover_files = id(new PhabricatorFileQuery()) + ->setViewer($viewer) + ->withPHIDs($cover_phids) + ->execute(); + $cover_files = mpull($cover_files, null, 'getPHID'); + } else { + $cover_files = array(); + } + + $this->coverFiles = $cover_files; + + $this->loaded = true; + } + + private function getCanEdit($phid) { + if ($this->editMap === null) { + return true; + } + + return idx($this->editMap, $phid); + } + +} diff --git a/src/applications/roles/engine/PhabricatorRoleBoardResponseEngine.php b/src/applications/roles/engine/PhabricatorRoleBoardResponseEngine.php new file mode 100644 index 0000000000..5c7c6eb070 --- /dev/null +++ b/src/applications/roles/engine/PhabricatorRoleBoardResponseEngine.php @@ -0,0 +1,279 @@ +viewer = $viewer; + return $this; + } + + public function getViewer() { + return $this->viewer; + } + + public function setBoardPHID($board_phid) { + $this->boardPHID = $board_phid; + return $this; + } + + public function getBoardPHID() { + return $this->boardPHID; + } + + public function setObjects(array $objects) { + $this->objects = $objects; + return $this; + } + + public function getObjects() { + return $this->objects; + } + + public function setVisiblePHIDs(array $visible_phids) { + $this->visiblePHIDs = $visible_phids; + return $this; + } + + public function getVisiblePHIDs() { + return $this->visiblePHIDs; + } + + public function setUpdatePHIDs(array $update_phids) { + $this->updatePHIDs = $update_phids; + return $this; + } + + public function getUpdatePHIDs() { + return $this->updatePHIDs; + } + + public function setOrdering(PhabricatorRoleColumnOrder $ordering) { + $this->ordering = $ordering; + return $this; + } + + public function getOrdering() { + return $this->ordering; + } + + public function setSounds(array $sounds) { + $this->sounds = $sounds; + return $this; + } + + public function getSounds() { + return $this->sounds; + } + + public function buildResponse() { + $viewer = $this->getViewer(); + $board_phid = $this->getBoardPHID(); + $ordering = $this->getOrdering(); + + $update_phids = $this->getUpdatePHIDs(); + $update_phids = array_fuse($update_phids); + + $visible_phids = $this->getVisiblePHIDs(); + $visible_phids = array_fuse($visible_phids); + + $all_phids = $update_phids + $visible_phids; + + // Load all the other tasks that are visible in the affected columns and + // perform layout for them. + + if ($this->objects !== null) { + $all_objects = $this->getObjects(); + $all_objects = mpull($all_objects, null, 'getPHID'); + } else { + $all_objects = id(new ManiphestTaskQuery()) + ->setViewer($viewer) + ->withPHIDs($all_phids) + ->execute(); + $all_objects = mpull($all_objects, null, 'getPHID'); + } + + // NOTE: The board layout engine is sensitive to PHID input order, and uses + // the input order as a component of the "natural" column ordering if no + // explicit ordering is specified. Rearrange the PHIDs in ID order. + + $all_objects = msort($all_objects, 'getID'); + $ordered_phids = mpull($all_objects, 'getPHID'); + + $layout_engine = id(new PhabricatorRoleBoardLayoutEngine()) + ->setViewer($viewer) + ->setBoardPHIDs(array($board_phid)) + ->setObjectPHIDs($ordered_phids) + ->executeLayout(); + + $natural = array(); + + $update_columns = array(); + foreach ($update_phids as $update_phid) { + $update_columns += $layout_engine->getObjectColumns( + $board_phid, + $update_phid); + } + + foreach ($update_columns as $column_phid => $column) { + $column_object_phids = $layout_engine->getColumnObjectPHIDs( + $board_phid, + $column_phid); + $natural[$column_phid] = array_values($column_object_phids); + } + + if ($ordering) { + $vectors = $ordering->getSortVectorsForObjects($all_objects); + $header_keys = $ordering->getHeaderKeysForObjects($all_objects); + $headers = $ordering->getHeadersForObjects($all_objects); + $headers = mpull($headers, 'toDictionary'); + } else { + $vectors = array(); + $header_keys = array(); + $headers = array(); + } + + $templates = $this->newCardTemplates(); + + $cards = array(); + foreach ($all_objects as $card_phid => $object) { + $card = array( + 'vectors' => array(), + 'headers' => array(), + 'properties' => array(), + 'nodeHTMLTemplate' => null, + ); + + if ($ordering) { + $order_key = $ordering->getColumnOrderKey(); + + $vector = idx($vectors, $card_phid); + if ($vector !== null) { + $card['vectors'][$order_key] = $vector; + } + + $header = idx($header_keys, $card_phid); + if ($header !== null) { + $card['headers'][$order_key] = $header; + } + + $card['properties'] = self::newTaskProperties($object); + } + + if (isset($templates[$card_phid])) { + $card['nodeHTMLTemplate'] = hsprintf('%s', $templates[$card_phid]); + $card['update'] = true; + } else { + $card['update'] = false; + } + + $card['vectors'] = (object)$card['vectors']; + $card['headers'] = (object)$card['headers']; + $card['properties'] = (object)$card['properties']; + + $cards[$card_phid] = $card; + } + + // Mark cards which are currently visible on the client but not visible + // on the board on the server for removal from the client view of the + // board state. + foreach ($visible_phids as $card_phid) { + if (!isset($cards[$card_phid])) { + $cards[$card_phid] = array( + 'remove' => true, + ); + } + } + + $payload = array( + 'columnMaps' => $natural, + 'cards' => $cards, + 'headers' => $headers, + 'sounds' => $this->getSounds(), + ); + + return id(new AphrontAjaxResponse()) + ->setContent($payload); + } + + public static function newTaskProperties($task) { + return array( + 'points' => (double)$task->getPoints(), + 'status' => $task->getStatus(), + 'priority' => (int)$task->getPriority(), + 'owner' => $task->getOwnerPHID(), + ); + } + + private function loadExcludedRolePHIDs() { + $viewer = $this->getViewer(); + $board_phid = $this->getBoardPHID(); + + $exclude_phids = array($board_phid); + + $descendants = id(new PhabricatorRoleQuery()) + ->setViewer($viewer) + ->withAncestorRolePHIDs($exclude_phids) + ->execute(); + + foreach ($descendants as $descendant) { + $exclude_phids[] = $descendant->getPHID(); + } + + return array_fuse($exclude_phids); + } + + private function newCardTemplates() { + $viewer = $this->getViewer(); + + $update_phids = $this->getUpdatePHIDs(); + if (!$update_phids) { + return array(); + } + $update_phids = array_fuse($update_phids); + + if ($this->objects === null) { + $objects = id(new ManiphestTaskQuery()) + ->setViewer($viewer) + ->withPHIDs($update_phids) + ->needRolePHIDs(true) + ->execute(); + } else { + $objects = $this->getObjects(); + $objects = mpull($objects, null, 'getPHID'); + $objects = array_select_keys($objects, $update_phids); + } + + if (!$objects) { + return array(); + } + + $excluded_phids = $this->loadExcludedRolePHIDs(); + + $rendering_engine = id(new PhabricatorRoleBoardRenderingEngine()) + ->setViewer($viewer) + ->setObjects($objects) + ->setExcludedRolePHIDs($excluded_phids); + + $templates = array(); + foreach ($objects as $object) { + $object_phid = $object->getPHID(); + + $card = $rendering_engine->renderCard($object_phid); + $item = $card->getItem(); + $template = hsprintf('%s', $item); + + $templates[$object_phid] = $template; + } + + return $templates; + } + +} diff --git a/src/applications/roles/engine/PhabricatorRoleEditEngine.php b/src/applications/roles/engine/PhabricatorRoleEditEngine.php new file mode 100644 index 0000000000..aef1f7e802 --- /dev/null +++ b/src/applications/roles/engine/PhabricatorRoleEditEngine.php @@ -0,0 +1,326 @@ +parentRole = $parent_role; + return $this; + } + + public function getParentRole() { + return $this->parentRole; + } + + public function setMilestoneRole(PhabricatorRole $milestone_role) { + $this->milestoneRole = $milestone_role; + return $this; + } + + public function getMilestoneRole() { + return $this->milestoneRole; + } + + public function isDefaultQuickCreateEngine() { + return true; + } + + public function getQuickCreateOrderVector() { + return id(new PhutilSortVector())->addInt(200); + } + + public function getEngineName() { + return pht('Roles'); + } + + public function getSummaryHeader() { + return pht('Configure Role Forms'); + } + + public function getSummaryText() { + return pht('Configure forms for creating roles.'); + } + + public function getEngineApplicationClass() { + return 'PhabricatorRoleApplication'; + } + + protected function newEditableObject() { + $parent = nonempty($this->parentRole, $this->milestoneRole); + + return PhabricatorRole::initializeNewRole( + $this->getViewer(), + $parent); + } + + protected function newObjectQuery() { + return id(new PhabricatorRoleQuery()) + ->needSlugs(true); + } + + protected function getObjectCreateTitleText($object) { + return pht('Create New Role'); + } + + protected function getObjectEditTitleText($object) { + return pht('Edit Role: %s', $object->getName()); + } + + protected function getObjectEditShortText($object) { + return $object->getName(); + } + + protected function getObjectCreateShortText() { + return pht('Create Role'); + } + + protected function getObjectName() { + return pht('Role'); + } + + protected function getObjectViewURI($object) { + if ($this->getIsCreate()) { + return $object->getURI(); + } else { + $id = $object->getID(); + return "/role/manage/{$id}/"; + } + } + + protected function getObjectCreateCancelURI($object) { + $parent = $this->getParentRole(); + $milestone = $this->getMilestoneRole(); + + if ($parent || $milestone) { + $id = nonempty($parent, $milestone)->getID(); + return "/role/subroles/{$id}/"; + } + + return parent::getObjectCreateCancelURI($object); + } + + protected function getCreateNewObjectPolicy() { + return $this->getApplication()->getPolicy( + RoleCreateRolesCapability::CAPABILITY); + } + + protected function willConfigureFields($object, array $fields) { + $is_milestone = ($this->getMilestoneRole() || $object->isMilestone()); + + $unavailable = array( + PhabricatorTransactions::TYPE_VIEW_POLICY, + PhabricatorTransactions::TYPE_EDIT_POLICY, + PhabricatorTransactions::TYPE_JOIN_POLICY, + PhabricatorTransactions::TYPE_SPACE, + PhabricatorRoleIconTransaction::TRANSACTIONTYPE, + PhabricatorRoleColorTransaction::TRANSACTIONTYPE, + ); + $unavailable = array_fuse($unavailable); + + if ($is_milestone) { + foreach ($fields as $key => $field) { + $xaction_type = $field->getTransactionType(); + if (isset($unavailable[$xaction_type])) { + unset($fields[$key]); + } + } + } + + return $fields; + } + + protected function newBuiltinEngineConfigurations() { + $configuration = head(parent::newBuiltinEngineConfigurations()); + + // TODO: This whole method is clumsy, and the ordering for the custom + // field is especially clumsy. Maybe try to make this more natural to + // express. + + $configuration + ->setFieldOrder( + array( + 'parent', + 'milestone', + 'milestone.previous', + 'name', + 'std:role:internal:description', + 'icon', + 'color', + 'slugs', + )); + + return array( + $configuration, + ); + } + + protected function buildCustomEditFields($object) { + $slugs = mpull($object->getSlugs(), 'getSlug'); + $slugs = array_fuse($slugs); + unset($slugs[$object->getPrimarySlug()]); + $slugs = array_values($slugs); + + $milestone = $this->getMilestoneRole(); + $parent = $this->getParentRole(); + + if ($parent) { + $parent_phid = $parent->getPHID(); + } else { + $parent_phid = null; + } + + $previous_milestone_phid = null; + if ($milestone) { + $milestone_phid = $milestone->getPHID(); + + // Load the current milestone so we can show the user a hint about what + // it was called, so they don't have to remember if the next one should + // be "Sprint 287" or "Sprint 278". + + $number = ($milestone->loadNextMilestoneNumber() - 1); + if ($number > 0) { + $previous_milestone = id(new PhabricatorRoleQuery()) + ->setViewer($this->getViewer()) + ->withParentRolePHIDs(array($milestone->getPHID())) + ->withIsMilestone(true) + ->withMilestoneNumberBetween($number, $number) + ->executeOne(); + if ($previous_milestone) { + $previous_milestone_phid = $previous_milestone->getPHID(); + } + } + } else { + $milestone_phid = null; + } + + $fields = array( + id(new PhabricatorHandlesEditField()) + ->setKey('parent') + ->setLabel(pht('Parent')) + ->setDescription(pht('Create a subrole of an existing role.')) + ->setConduitDescription( + pht('Choose a parent role to create a subrole beneath.')) + ->setConduitTypeDescription(pht('PHID of the parent role.')) + ->setAliases(array('parentPHID')) + ->setTransactionType( + PhabricatorRoleParentTransaction::TRANSACTIONTYPE) + ->setHandleParameterType(new AphrontPHIDHTTPParameterType()) + ->setSingleValue($parent_phid) + ->setIsReorderable(false) + ->setIsDefaultable(false) + ->setIsLockable(false) + ->setIsLocked(true), + id(new PhabricatorHandlesEditField()) + ->setKey('milestone') + ->setLabel(pht('Milestone Of')) + ->setDescription(pht('Parent role to create a milestone for.')) + ->setConduitDescription( + pht('Choose a parent role to create a new milestone for.')) + ->setConduitTypeDescription(pht('PHID of the parent role.')) + ->setAliases(array('milestonePHID')) + ->setTransactionType( + PhabricatorRoleMilestoneTransaction::TRANSACTIONTYPE) + ->setHandleParameterType(new AphrontPHIDHTTPParameterType()) + ->setSingleValue($milestone_phid) + ->setIsReorderable(false) + ->setIsDefaultable(false) + ->setIsLockable(false) + ->setIsLocked(true), + id(new PhabricatorHandlesEditField()) + ->setKey('milestone.previous') + ->setLabel(pht('Previous Milestone')) + ->setSingleValue($previous_milestone_phid) + ->setIsReorderable(false) + ->setIsDefaultable(false) + ->setIsLockable(false) + ->setIsLocked(true), + id(new PhabricatorTextEditField()) + ->setKey('name') + ->setLabel(pht('Name')) + ->setTransactionType(PhabricatorRoleNameTransaction::TRANSACTIONTYPE) + ->setIsRequired(true) + ->setDescription(pht('Role name.')) + ->setConduitDescription(pht('Rename the role')) + ->setConduitTypeDescription(pht('New role name.')) + ->setValue($object->getName()), + id(new PhabricatorIconSetEditField()) + ->setKey('icon') + ->setLabel(pht('Icon')) + ->setTransactionType( + PhabricatorRoleIconTransaction::TRANSACTIONTYPE) + ->setIconSet(new PhabricatorRoleIconSet()) + ->setDescription(pht('Role icon.')) + ->setConduitDescription(pht('Change the role icon.')) + ->setConduitTypeDescription(pht('New role icon.')) + ->setValue($object->getIcon()), + id(new PhabricatorSelectEditField()) + ->setKey('color') + ->setLabel(pht('Color')) + ->setTransactionType( + PhabricatorRoleColorTransaction::TRANSACTIONTYPE) + ->setOptions(PhabricatorRoleIconSet::getColorMap()) + ->setDescription(pht('Role tag color.')) + ->setConduitDescription(pht('Change the role tag color.')) + ->setConduitTypeDescription(pht('New role tag color.')) + ->setValue($object->getColor()), + id(new PhabricatorStringListEditField()) + ->setKey('slugs') + ->setLabel(pht('Additional Hashtags')) + ->setTransactionType( + PhabricatorRoleSlugsTransaction::TRANSACTIONTYPE) + ->setDescription(pht('Additional role slugs.')) + ->setConduitDescription(pht('Change role slugs.')) + ->setConduitTypeDescription(pht('New list of slugs.')) + ->setValue($slugs), + ); + + $can_edit_members = (!$milestone) && + (!$object->isMilestone()) && + (!$object->getHasSubroles()); + + if ($can_edit_members) { + + // Show this on the web UI when creating a role, but not when editing + // one. It is always available via Conduit. + $show_field = (bool)$this->getIsCreate(); + + $members_field = id(new PhabricatorUsersEditField()) + ->setKey('members') + ->setAliases(array('memberPHIDs')) + ->setLabel(pht('Initial Members')) + ->setIsFormField($show_field) + ->setUseEdgeTransactions(true) + ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) + ->setMetadataValue( + 'edge:type', + PhabricatorRoleRoleHasMemberEdgeType::EDGECONST) + ->setDescription(pht('Initial role members.')) + ->setConduitDescription(pht('Set role members.')) + ->setConduitTypeDescription(pht('New list of members.')) + ->setValue(array()); + + $members_field->setViewer($this->getViewer()); + + $edit_add = $members_field->getConduitEditType('members.add') + ->setConduitDescription(pht('Add members.')); + + $edit_set = $members_field->getConduitEditType('members.set') + ->setConduitDescription( + pht('Set members, overwriting the current value.')); + + $edit_rem = $members_field->getConduitEditType('members.remove') + ->setConduitDescription(pht('Remove members.')); + + $fields[] = $members_field; + } + + return $fields; + + } + +} diff --git a/src/applications/roles/engine/PhabricatorRoleProfileMenuEngine.php b/src/applications/roles/engine/PhabricatorRoleProfileMenuEngine.php new file mode 100644 index 0000000000..c4f2ace998 --- /dev/null +++ b/src/applications/roles/engine/PhabricatorRoleProfileMenuEngine.php @@ -0,0 +1,61 @@ +getProfileObject(); + $id = $role->getID(); + return "/role/{$id}/item/{$path}"; + } + + protected function getBuiltinProfileItems($object) { + $items = array(); + + $items[] = $this->newItem() + ->setBuiltinKey(PhabricatorRole::ITEM_PICTURE) + ->setMenuItemKey(PhabricatorRolePictureProfileMenuItem::MENUITEMKEY) + ->setIsHeadItem(true); + + $items[] = $this->newItem() + ->setBuiltinKey(PhabricatorRole::ITEM_PROFILE) + ->setMenuItemKey(PhabricatorRoleDetailsProfileMenuItem::MENUITEMKEY); + + $items[] = $this->newItem() + ->setBuiltinKey(PhabricatorRole::ITEM_POINTS) + ->setMenuItemKey(PhabricatorRolePointsProfileMenuItem::MENUITEMKEY); + + $items[] = $this->newItem() + ->setBuiltinKey(PhabricatorRole::ITEM_WORKBOARD) + ->setMenuItemKey(PhabricatorRoleWorkboardProfileMenuItem::MENUITEMKEY); + + $items[] = $this->newItem() + ->setBuiltinKey(PhabricatorRole::ITEM_REPORTS) + ->setMenuItemKey(PhabricatorRoleReportsProfileMenuItem::MENUITEMKEY); + + $items[] = $this->newItem() + ->setBuiltinKey(PhabricatorRole::ITEM_MEMBERS) + ->setMenuItemKey(PhabricatorRoleMembersProfileMenuItem::MENUITEMKEY); + + $items[] = $this->newItem() + ->setBuiltinKey(PhabricatorRole::ITEM_SUBROLES) + ->setMenuItemKey( + PhabricatorRoleSubrolesProfileMenuItem::MENUITEMKEY); + + $items[] = $this->newItem() + ->setBuiltinKey(PhabricatorRole::ITEM_MANAGE) + ->setMenuItemKey(PhabricatorRoleManageProfileMenuItem::MENUITEMKEY) + ->setIsTailItem(true); + + return $items; + } + +} diff --git a/src/applications/roles/engineextension/PhabricatorRoleBoardColumnsSearchEngineAttachment.php b/src/applications/roles/engineextension/PhabricatorRoleBoardColumnsSearchEngineAttachment.php new file mode 100644 index 0000000000..6dc3fa05af --- /dev/null +++ b/src/applications/roles/engineextension/PhabricatorRoleBoardColumnsSearchEngineAttachment.php @@ -0,0 +1,80 @@ +getViewer(); + + $objects = mpull($objects, null, 'getPHID'); + $object_phids = array_keys($objects); + + $edge_query = id(new PhabricatorEdgeQuery()) + ->withSourcePHIDs($object_phids) + ->withEdgeTypes( + array( + PhabricatorRoleObjectHasRoleEdgeType::EDGECONST, + )); + $edge_query->execute(); + + $role_phids = $edge_query->getDestinationPHIDs(); + + $engine = id(new PhabricatorRoleBoardLayoutEngine()) + ->setViewer($viewer) + ->setBoardPHIDs($role_phids) + ->setObjectPHIDs($object_phids) + ->executeLayout(); + + $results = array(); + foreach ($objects as $phid => $object) { + $board_phids = $edge_query->getDestinationPHIDs(array($phid)); + + $boards = array(); + foreach ($board_phids as $board_phid) { + $columns = array(); + foreach ($engine->getObjectColumns($board_phid, $phid) as $column) { + if ($column->getProxyPHID()) { + // When an object is in a proxy column, don't return it on this + // attachment. This information can be reconstructed from other + // queries, is something of an implementation detail, and seems + // unlikely to be interesting to API consumers. + continue 2; + } + + $columns[] = $column->getRefForConduit(); + } + + // If a role has no workboard, the object won't appear on any + // columns. Just omit it from the result set. + if (!$columns) { + continue; + } + + $boards[$board_phid] = array( + 'columns' => $columns, + ); + } + + $results[$phid] = $boards; + } + + return $results; + } + + public function getAttachmentForObject($object, $data, $spec) { + $boards = idx($data, $object->getPHID(), array()); + + return array( + 'boards' => $boards, + ); + } + +} diff --git a/src/applications/roles/engineextension/PhabricatorRoleHovercardEngineExtension.php b/src/applications/roles/engineextension/PhabricatorRoleHovercardEngineExtension.php new file mode 100644 index 0000000000..516624605c --- /dev/null +++ b/src/applications/roles/engineextension/PhabricatorRoleHovercardEngineExtension.php @@ -0,0 +1,55 @@ +getViewer(); + $phids = mpull($objects, 'getPHID'); + + $roles = id(new PhabricatorRoleQuery()) + ->setViewer($viewer) + ->withPHIDs($phids) + ->needImages(true) + ->execute(); + $roles = mpull($roles, null, 'getPHID'); + + return array( + 'roles' => $roles, + ); + } + + public function renderHovercard( + PHUIHovercardView $hovercard, + PhabricatorObjectHandle $handle, + $object, + $data) { + $viewer = $this->getViewer(); + + $role = idx($data['roles'], $object->getPHID()); + if (!$role) { + return; + } + + $role_card = id(new PhabricatorRoleCardView()) + ->setRole($role) + ->setViewer($viewer); + + $hovercard->appendChild($role_card); + } + +} diff --git a/src/applications/roles/engineextension/PhabricatorRoleTriggerUsageIndexEngineExtension.php b/src/applications/roles/engineextension/PhabricatorRoleTriggerUsageIndexEngineExtension.php new file mode 100644 index 0000000000..89eba9eea4 --- /dev/null +++ b/src/applications/roles/engineextension/PhabricatorRoleTriggerUsageIndexEngineExtension.php @@ -0,0 +1,69 @@ +establishConnection('w'); + + $active_statuses = array( + PhabricatorRoleColumn::STATUS_ACTIVE, + ); + + // Select summary information to populate the usage index. When picking + // an "examplePHID", we try to pick an active column. + $row = queryfx_one( + $conn_w, + 'SELECT phid, COUNT(*) N, SUM(IF(status IN (%Ls), 1, 0)) M FROM %R + WHERE triggerPHID = %s + ORDER BY IF(status IN (%Ls), 1, 0) DESC, id ASC', + $active_statuses, + $column_table, + $object->getPHID(), + $active_statuses); + if ($row) { + $example_phid = $row['phid']; + $column_count = $row['N']; + $active_count = $row['M']; + } else { + $example_phid = null; + $column_count = 0; + $active_count = 0; + } + + queryfx( + $conn_w, + 'INSERT INTO %R (triggerPHID, examplePHID, columnCount, activeColumnCount) + VALUES (%s, %ns, %d, %d) + ON DUPLICATE KEY UPDATE + examplePHID = VALUES(examplePHID), + columnCount = VALUES(columnCount), + activeColumnCount = VALUES(activeColumnCount)', + $usage_table, + $object->getPHID(), + $example_phid, + $column_count, + $active_count); + } + +} diff --git a/src/applications/roles/engineextension/PhabricatorRolesAncestorsSearchEngineAttachment.php b/src/applications/roles/engineextension/PhabricatorRolesAncestorsSearchEngineAttachment.php new file mode 100644 index 0000000000..d43643e7f9 --- /dev/null +++ b/src/applications/roles/engineextension/PhabricatorRolesAncestorsSearchEngineAttachment.php @@ -0,0 +1,30 @@ +getAncestorRoles(); + + // Order ancestors by depth, ascending. + $ancestors = array_reverse($ancestors); + + $results = array(); + foreach ($ancestors as $ancestor) { + $results[] = $ancestor->getRefForConduit(); + } + + return array( + 'ancestors' => $results, + ); + } + +} diff --git a/src/applications/roles/engineextension/PhabricatorRolesCurtainExtension.php b/src/applications/roles/engineextension/PhabricatorRolesCurtainExtension.php new file mode 100644 index 0000000000..7e17a3ae8d --- /dev/null +++ b/src/applications/roles/engineextension/PhabricatorRolesCurtainExtension.php @@ -0,0 +1,91 @@ +getViewer(); + + $role_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( + $object->getPHID(), + PhabricatorRoleObjectHasRoleEdgeType::EDGECONST); + + $has_roles = (bool)$role_phids; + $role_phids = array_reverse($role_phids); + $handles = $viewer->loadHandles($role_phids); + + // If this object can appear on boards, build the workboard annotations. + // Some day, this might be a generic interface. For now, only tasks can + // appear on boards. + $can_appear_on_boards = ($object instanceof ManiphestTask); + + $annotations = array(); + if ($has_roles && $can_appear_on_boards) { + $engine = id(new PhabricatorRoleBoardLayoutEngine()) + ->setViewer($viewer) + ->setBoardPHIDs($role_phids) + ->setObjectPHIDs(array($object->getPHID())) + ->executeLayout(); + + // TDOO: Generalize this UI and move it out of Maniphest. + require_celerity_resource('maniphest-task-summary-css'); + + foreach ($role_phids as $role_phid) { + $handle = $handles[$role_phid]; + + $columns = $engine->getObjectColumns( + $role_phid, + $object->getPHID()); + + $annotation = array(); + foreach ($columns as $column) { + $role_id = $column->getRole()->getID(); + + $column_name = pht('(%s)', $column->getDisplayName()); + $column_link = phutil_tag( + 'a', + array( + 'href' => $column->getWorkboardURI(), + 'class' => 'maniphest-board-link', + ), + $column_name); + + $annotation[] = $column_link; + } + + if ($annotation) { + $annotations[$role_phid] = array( + ' ', + phutil_implode_html(', ', $annotation), + ); + } + } + + } + + if ($has_roles) { + $list = id(new PHUIHandleTagListView()) + ->setHandles($handles) + ->setAnnotations($annotations) + ->setShowHovercards(true); + } else { + $list = phutil_tag('em', array(), pht('None')); + } + + return $this->newPanel() + ->setHeaderText(pht('Tags')) + ->setOrder(10000) + ->appendChild($list); + } + +} diff --git a/src/applications/roles/engineextension/PhabricatorRolesEditEngineExtension.php b/src/applications/roles/engineextension/PhabricatorRolesEditEngineExtension.php new file mode 100644 index 0000000000..b76dc06190 --- /dev/null +++ b/src/applications/roles/engineextension/PhabricatorRolesEditEngineExtension.php @@ -0,0 +1,96 @@ +getPHID(); + if ($object_phid) { + $role_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( + $object_phid, + $role_edge_type); + $role_phids = array_reverse($role_phids); + } else { + $role_phids = array(); + } + + $viewer = $engine->getViewer(); + + $roles_field = id(new PhabricatorRolesEditField()) + ->setKey('rolePHIDs') + ->setLabel(pht('Tags')) + ->setEditTypeKey('roles') + ->setAliases(array('role', 'roles', 'tag', 'tags')) + ->setIsCopyable(true) + ->setUseEdgeTransactions(true) + ->setCommentActionLabel(pht('Change Role Tags')) + ->setCommentActionOrder(8000) + ->setDescription(pht('Select role tags for the object.')) + ->setTransactionType($edge_type) + ->setMetadataValue('edge:type', $role_edge_type) + ->setValue($role_phids) + ->setViewer($viewer); + + $roles_datasource = id(new PhabricatorRoleDatasource()) + ->setViewer($viewer); + + $edit_add = $roles_field->getConduitEditType(self::EDITKEY_ADD) + ->setConduitDescription(pht('Add role tags.')); + + $edit_set = $roles_field->getConduitEditType(self::EDITKEY_SET) + ->setConduitDescription( + pht('Set role tags, overwriting current value.')); + + $edit_rem = $roles_field->getConduitEditType(self::EDITKEY_REMOVE) + ->setConduitDescription(pht('Remove role tags.')); + + $roles_field->getBulkEditType(self::EDITKEY_ADD) + ->setBulkEditLabel(pht('Add role tags')) + ->setDatasource($roles_datasource); + + $roles_field->getBulkEditType(self::EDITKEY_SET) + ->setBulkEditLabel(pht('Set role tags to')) + ->setDatasource($roles_datasource); + + $roles_field->getBulkEditType(self::EDITKEY_REMOVE) + ->setBulkEditLabel(pht('Remove role tags')) + ->setDatasource($roles_datasource); + + return array( + $roles_field, + ); + } + +} diff --git a/src/applications/roles/engineextension/PhabricatorRolesFulltextEngineExtension.php b/src/applications/roles/engineextension/PhabricatorRolesFulltextEngineExtension.php new file mode 100644 index 0000000000..c7a46c76f0 --- /dev/null +++ b/src/applications/roles/engineextension/PhabricatorRolesFulltextEngineExtension.php @@ -0,0 +1,37 @@ +getPHID(), + PhabricatorRoleObjectHasRoleEdgeType::EDGECONST); + + if (!$role_phids) { + return; + } + + foreach ($role_phids as $role_phid) { + $document->addRelationship( + PhabricatorSearchRelationship::RELATIONSHIP_ROLE, + $role_phid, + PhabricatorRoleRolePHIDType::TYPECONST, + $document->getDocumentModified()); // Bogus timestamp. + } + } + +} diff --git a/src/applications/roles/engineextension/PhabricatorRolesMailEngineExtension.php b/src/applications/roles/engineextension/PhabricatorRolesMailEngineExtension.php new file mode 100644 index 0000000000..0f15f27009 --- /dev/null +++ b/src/applications/roles/engineextension/PhabricatorRolesMailEngineExtension.php @@ -0,0 +1,32 @@ +setKey('tag') + ->setLabel(pht('Tagged with Role')), + ); + } + + public function newMailStamps($object, array $xactions) { + $editor = $this->getEditor(); + $viewer = $this->getViewer(); + + $role_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( + $object->getPHID(), + PhabricatorRoleObjectHasRoleEdgeType::EDGECONST); + + $this->getMailStamp('tag') + ->setValue($role_phids); + } + +} diff --git a/src/applications/roles/engineextension/PhabricatorRolesMembersSearchEngineAttachment.php b/src/applications/roles/engineextension/PhabricatorRolesMembersSearchEngineAttachment.php new file mode 100644 index 0000000000..d509c98b92 --- /dev/null +++ b/src/applications/roles/engineextension/PhabricatorRolesMembersSearchEngineAttachment.php @@ -0,0 +1,31 @@ +needMembers(true); + } + + public function getAttachmentForObject($object, $data, $spec) { + $members = array(); + foreach ($object->getMemberPHIDs() as $member_phid) { + $members[] = array( + 'phid' => $member_phid, + ); + } + + return array( + 'members' => $members, + ); + } + +} diff --git a/src/applications/roles/engineextension/PhabricatorRolesMembershipIndexEngineExtension.php b/src/applications/roles/engineextension/PhabricatorRolesMembershipIndexEngineExtension.php new file mode 100644 index 0000000000..141d73001c --- /dev/null +++ b/src/applications/roles/engineextension/PhabricatorRolesMembershipIndexEngineExtension.php @@ -0,0 +1,176 @@ +rematerialize($object); + } + + public function rematerialize(PhabricatorRole $role) { + $materialize = $role->getAncestorRoles(); + array_unshift($materialize, $role); + + foreach ($materialize as $role) { + $this->materializeRole($role); + } + } + + private function materializeRole(PhabricatorRole $role) { + $material_type = PhabricatorRoleMaterializedMemberEdgeType::EDGECONST; + $member_type = PhabricatorRoleRoleHasMemberEdgeType::EDGECONST; + + $role_phid = $role->getPHID(); + + if ($role->isMilestone()) { + $source_phids = array($role->getParentRolePHID()); + $has_subroles = false; + } else { + $descendants = id(new PhabricatorRoleQuery()) + ->setViewer($this->getViewer()) + ->withAncestorRolePHIDs(array($role->getPHID())) + ->withIsMilestone(false) + ->withHasSubroles(false) + ->execute(); + $descendant_phids = mpull($descendants, 'getPHID'); + + if ($descendant_phids) { + $source_phids = $descendant_phids; + $has_subroles = true; + } else { + $source_phids = array($role->getPHID()); + $has_subroles = false; + } + } + + $conn_w = $role->establishConnection('w'); + + $any_milestone = queryfx_one( + $conn_w, + 'SELECT id FROM %T + WHERE parentRolePHID = %s AND milestoneNumber IS NOT NULL + LIMIT 1', + $role->getTableName(), + $role_phid); + $has_milestones = (bool)$any_milestone; + + $role->openTransaction(); + + // Copy current member edges to create new materialized edges. + + // See T13596. Avoid executing this as an "INSERT ... SELECT" to reduce + // the required level of table locking. Since we're decomposing it into + // "SELECT" + "INSERT" anyway, we can also compute exactly which rows + // need to be modified. + + $have_rows = queryfx_all( + $conn_w, + 'SELECT dst FROM %T + WHERE src = %s AND type = %d', + PhabricatorEdgeConfig::TABLE_NAME_EDGE, + $role_phid, + $material_type); + + $want_rows = queryfx_all( + $conn_w, + 'SELECT dst, dateCreated, seq FROM %T + WHERE src IN (%Ls) AND type = %d', + PhabricatorEdgeConfig::TABLE_NAME_EDGE, + $source_phids, + $member_type); + + $have_phids = ipull($have_rows, 'dst', 'dst'); + $want_phids = ipull($want_rows, null, 'dst'); + + $rem_phids = array_diff_key($have_phids, $want_phids); + $rem_phids = array_keys($rem_phids); + + $add_phids = array_diff_key($want_phids, $have_phids); + $add_phids = array_keys($add_phids); + + $rem_sql = array(); + foreach ($rem_phids as $rem_phid) { + $rem_sql[] = qsprintf( + $conn_w, + '%s', + $rem_phid); + } + + $add_sql = array(); + foreach ($add_phids as $add_phid) { + $add_row = $want_phids[$add_phid]; + $add_sql[] = qsprintf( + $conn_w, + '(%s, %d, %s, %d, %d)', + $role_phid, + $material_type, + $add_row['dst'], + $add_row['dateCreated'], + $add_row['seq']); + } + + // Remove materialized members who are no longer role members. + + if ($rem_sql) { + foreach (PhabricatorLiskDAO::chunkSQL($rem_sql) as $sql_chunk) { + queryfx( + $conn_w, + 'DELETE FROM %T + WHERE src = %s AND type = %s AND dst IN (%LQ)', + PhabricatorEdgeConfig::TABLE_NAME_EDGE, + $role_phid, + $material_type, + $sql_chunk); + } + } + + // Add role members who are not yet materialized members. + + if ($add_sql) { + foreach (PhabricatorLiskDAO::chunkSQL($add_sql) as $sql_chunk) { + queryfx( + $conn_w, + 'INSERT IGNORE INTO %T (src, type, dst, dateCreated, seq) + VALUES %LQ', + PhabricatorEdgeConfig::TABLE_NAME_EDGE, + $sql_chunk); + } + } + + // Update the hasSubroles flag. + queryfx( + $conn_w, + 'UPDATE %T SET hasSubroles = %d WHERE id = %d', + $role->getTableName(), + (int)$has_subroles, + $role->getID()); + + // Update the hasMilestones flag. + queryfx( + $conn_w, + 'UPDATE %T SET hasMilestones = %d WHERE id = %d', + $role->getTableName(), + (int)$has_milestones, + $role->getID()); + + $role->saveTransaction(); + } + +} diff --git a/src/applications/roles/engineextension/PhabricatorRolesSearchEngineAttachment.php b/src/applications/roles/engineextension/PhabricatorRolesSearchEngineAttachment.php new file mode 100644 index 0000000000..663f1f4bfd --- /dev/null +++ b/src/applications/roles/engineextension/PhabricatorRolesSearchEngineAttachment.php @@ -0,0 +1,43 @@ +withSourcePHIDs($object_phids) + ->withEdgeTypes( + array( + PhabricatorRoleObjectHasRoleEdgeType::EDGECONST, + )); + $roles_query->execute(); + + return array( + 'roles.query' => $roles_query, + ); + } + + public function getAttachmentForObject($object, $data, $spec) { + $roles_query = $data['roles.query']; + $object_phid = $object->getPHID(); + + $role_phids = $roles_query->getDestinationPHIDs( + array($object_phid), + array(PhabricatorRoleObjectHasRoleEdgeType::EDGECONST)); + + return array( + 'rolePHIDs' => array_values($role_phids), + ); + } + +} diff --git a/src/applications/roles/engineextension/PhabricatorRolesSearchEngineExtension.php b/src/applications/roles/engineextension/PhabricatorRolesSearchEngineExtension.php new file mode 100644 index 0000000000..96943f330a --- /dev/null +++ b/src/applications/roles/engineextension/PhabricatorRolesSearchEngineExtension.php @@ -0,0 +1,60 @@ +withEdgeLogicConstraints( + PhabricatorRoleObjectHasRoleEdgeType::EDGECONST, + $map['rolePHIDs']); + } + } + + public function getSearchFields($object) { + $fields = array(); + + $fields[] = id(new PhabricatorRoleSearchField()) + ->setKey('rolePHIDs') + ->setConduitKey('roles') + ->setAliases(array('role', 'roles', 'tag', 'tags')) + ->setLabel(pht('Tags')) + ->setDescription( + pht('Search for objects tagged with given roles.')); + + return $fields; + } + + public function getSearchAttachments($object) { + return array( + id(new PhabricatorRolesSearchEngineAttachment()) + ->setAttachmentKey('roles'), + ); + } + + +} diff --git a/src/applications/roles/engineextension/PhabricatorRolesWatchersSearchEngineAttachment.php b/src/applications/roles/engineextension/PhabricatorRolesWatchersSearchEngineAttachment.php new file mode 100644 index 0000000000..d224523e48 --- /dev/null +++ b/src/applications/roles/engineextension/PhabricatorRolesWatchersSearchEngineAttachment.php @@ -0,0 +1,31 @@ +needWatchers(true); + } + + public function getAttachmentForObject($object, $data, $spec) { + $watchers = array(); + foreach ($object->getWatcherPHIDs() as $watcher_phid) { + $watchers[] = array( + 'phid' => $watcher_phid, + ); + } + + return array( + 'watchers' => $watchers, + ); + } + +} diff --git a/src/applications/roles/engineextension/RoleDatasourceEngineExtension.php b/src/applications/roles/engineextension/RoleDatasourceEngineExtension.php new file mode 100644 index 0000000000..a4caa42fb7 --- /dev/null +++ b/src/applications/roles/engineextension/RoleDatasourceEngineExtension.php @@ -0,0 +1,57 @@ +getViewer(); + + // Send "p" to Roles. + if (preg_match('/^p\z/i', $query)) { + return '/diffusion/'; + } + + // Send "p " to a search for similar roles. + $matches = null; + if (preg_match('/^p\s+(.+)\z/i', $query, $matches)) { + $raw_query = $matches[1]; + + $engine = id(new PhabricatorRole()) + ->newFerretEngine(); + + $compiler = id(new PhutilSearchQueryCompiler()) + ->setEnableFunctions(true); + + $raw_tokens = $compiler->newTokens($raw_query); + + $fulltext_tokens = array(); + foreach ($raw_tokens as $raw_token) { + $fulltext_token = id(new PhabricatorFulltextToken()) + ->setToken($raw_token); + $fulltext_tokens[] = $fulltext_token; + } + + $roles = id(new PhabricatorRoleQuery()) + ->setViewer($viewer) + ->withFerretConstraint($engine, $fulltext_tokens) + ->execute(); + if (count($roles) == 1) { + // Just one match, jump to role. + return head($roles)->getURI(); + } else { + // More than one match, jump to search. + return urisprintf( + '/role/?order=relevance&query=%s#R', + $raw_query); + } + } + + return null; + } +} diff --git a/src/applications/roles/events/PhabricatorRoleUIEventListener.php b/src/applications/roles/events/PhabricatorRoleUIEventListener.php new file mode 100644 index 0000000000..2a3c3cb7bc --- /dev/null +++ b/src/applications/roles/events/PhabricatorRoleUIEventListener.php @@ -0,0 +1,115 @@ +listen(PhabricatorEventType::TYPE_UI_WILLRENDERPROPERTIES); + } + + public function handleEvent(PhutilEvent $event) { + $object = $event->getValue('object'); + + switch ($event->getType()) { + case PhabricatorEventType::TYPE_UI_WILLRENDERPROPERTIES: + // Hacky solution so that property list view on Diffusion + // commits shows build status, but not Roles, Subscriptions, + // or Tokens. + if ($object instanceof PhabricatorRepositoryCommit) { + return; + } + $this->handlePropertyEvent($event); + break; + } + } + + private function handlePropertyEvent($event) { + $user = $event->getUser(); + $object = $event->getValue('object'); + + if (!$object || !$object->getPHID()) { + // No object, or the object has no PHID yet.. + return; + } + + if (!($object instanceof PhabricatorRoleInterface)) { + // This object doesn't have roles. + return; + } + + $role_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( + $object->getPHID(), + PhabricatorRoleObjectHasRoleEdgeType::EDGECONST); + if ($role_phids) { + $role_phids = array_reverse($role_phids); + $handles = id(new PhabricatorHandleQuery()) + ->setViewer($user) + ->withPHIDs($role_phids) + ->execute(); + } else { + $handles = array(); + } + + // If this object can appear on boards, build the workboard annotations. + // Some day, this might be a generic interface. For now, only tasks can + // appear on boards. + $can_appear_on_boards = ($object instanceof ManiphestTask); + + $annotations = array(); + if ($handles && $can_appear_on_boards) { + $engine = id(new PhabricatorRoleBoardLayoutEngine()) + ->setViewer($user) + ->setBoardPHIDs($role_phids) + ->setObjectPHIDs(array($object->getPHID())) + ->executeLayout(); + + // TDOO: Generalize this UI and move it out of Maniphest. + require_celerity_resource('maniphest-task-summary-css'); + + foreach ($role_phids as $role_phid) { + $handle = $handles[$role_phid]; + + $columns = $engine->getObjectColumns( + $role_phid, + $object->getPHID()); + + $annotation = array(); + foreach ($columns as $column) { + $role_id = $column->getRole()->getID(); + + $column_name = pht('(%s)', $column->getDisplayName()); + $column_link = phutil_tag( + 'a', + array( + 'href' => $column->getWorkboardURI(), + 'class' => 'maniphest-board-link', + ), + $column_name); + + $annotation[] = $column_link; + } + + if ($annotation) { + $annotations[$role_phid] = array( + ' ', + phutil_implode_html(', ', $annotation), + ); + } + } + + } + + if ($handles) { + $list = id(new PHUIHandleTagListView()) + ->setHandles($handles) + ->setAnnotations($annotations) + ->setShowHovercards(true); + } else { + $list = phutil_tag('em', array(), pht('None')); + } + + $view = $event->getValue('view'); + $view->addProperty(pht('Roles'), $list); + } + +} diff --git a/src/applications/roles/exception/PhabricatorRoleTriggerCorruptionException.php b/src/applications/roles/exception/PhabricatorRoleTriggerCorruptionException.php new file mode 100644 index 0000000000..77911d81d5 --- /dev/null +++ b/src/applications/roles/exception/PhabricatorRoleTriggerCorruptionException.php @@ -0,0 +1,4 @@ +getPHID()); + } + + protected function getHeraldFieldStandardType() { + return self::STANDARD_PHID_LIST; + } + + protected function getDatasource() { + return new PhabricatorRoleDatasource(); + } + +} diff --git a/src/applications/roles/herald/HeraldRolesField.php b/src/applications/roles/herald/HeraldRolesField.php new file mode 100644 index 0000000000..52243f879b --- /dev/null +++ b/src/applications/roles/herald/HeraldRolesField.php @@ -0,0 +1,18 @@ +getPHID(), + PhabricatorRoleObjectHasRoleEdgeType::EDGECONST); + } + +} diff --git a/src/applications/roles/herald/PhabricatorRoleAddHeraldAction.php b/src/applications/roles/herald/PhabricatorRoleAddHeraldAction.php new file mode 100644 index 0000000000..8db6e179fe --- /dev/null +++ b/src/applications/roles/herald/PhabricatorRoleAddHeraldAction.php @@ -0,0 +1,28 @@ +applyRoles($effect->getTarget(), $is_add = true); + } + + public function getHeraldActionStandardType() { + return self::STANDARD_PHID_LIST; + } + + protected function getDatasource() { + return new PhabricatorRoleDatasource(); + } + + public function renderActionDescription($value) { + return pht('Add roles: %s.', $this->renderHandleList($value)); + } + +} diff --git a/src/applications/roles/herald/PhabricatorRoleHeraldAction.php b/src/applications/roles/herald/PhabricatorRoleHeraldAction.php new file mode 100644 index 0000000000..cdb0ffee0b --- /dev/null +++ b/src/applications/roles/herald/PhabricatorRoleHeraldAction.php @@ -0,0 +1,125 @@ +getAdapter(); + + $allowed_types = array( + PhabricatorRoleRolePHIDType::TYPECONST, + ); + + // Detection of "No Effect" is a bit tricky for this action, so just do it + // manually a little later on. + $current = array(); + + $targets = $this->loadStandardTargets($phids, $allowed_types, $current); + if (!$targets) { + return; + } + + $phids = array_fuse(array_keys($targets)); + + $role_type = PhabricatorRoleObjectHasRoleEdgeType::EDGECONST; + $current = $adapter->loadEdgePHIDs($role_type); + + if ($is_add) { + $already = array(); + foreach ($phids as $phid) { + if (isset($current[$phid])) { + $already[$phid] = $phid; + unset($phids[$phid]); + } + } + + if ($already) { + $this->logEffect(self::DO_STANDARD_NO_EFFECT, $already); + } + } else { + $already = array(); + foreach ($phids as $phid) { + if (empty($current[$phid])) { + $already[$phid] = $phid; + unset($phids[$phid]); + } + } + + if ($already) { + $this->logEffect(self::DO_STANDARD_NO_EFFECT, $already); + } + } + + if (!$phids) { + return; + } + + if ($is_add) { + $kind = '+'; + } else { + $kind = '-'; + } + + $xaction = $adapter->newTransaction() + ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) + ->setMetadataValue('edge:type', $role_type) + ->setNewValue( + array( + $kind => $phids, + )); + + $adapter->queueTransaction($xaction); + + if ($is_add) { + $this->logEffect(self::DO_ADD_ROLES, $phids); + } else { + $this->logEffect(self::DO_REMOVE_ROLES, $phids); + } + } + + protected function getActionEffectMap() { + return array( + self::DO_ADD_ROLES => array( + 'icon' => 'fa-briefcase', + 'color' => 'green', + 'name' => pht('Added Roles'), + ), + self::DO_REMOVE_ROLES => array( + 'icon' => 'fa-minus-circle', + 'color' => 'green', + 'name' => pht('Removed Roles'), + ), + ); + } + + protected function renderActionEffectDescription($type, $data) { + switch ($type) { + case self::DO_ADD_ROLES: + return pht( + 'Added %s role(s): %s.', + phutil_count($data), + $this->renderHandleList($data)); + case self::DO_REMOVE_ROLES: + return pht( + 'Removed %s role(s): %s.', + phutil_count($data), + $this->renderHandleList($data)); + } + } + +} diff --git a/src/applications/roles/herald/PhabricatorRoleHeraldAdapter.php b/src/applications/roles/herald/PhabricatorRoleHeraldAdapter.php new file mode 100644 index 0000000000..aa38d0d00f --- /dev/null +++ b/src/applications/roles/herald/PhabricatorRoleHeraldAdapter.php @@ -0,0 +1,59 @@ +role = $this->newObject(); + } + + public function supportsApplicationEmail() { + return true; + } + + public function supportsRuleType($rule_type) { + switch ($rule_type) { + case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL: + case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL: + return true; + case HeraldRuleTypeConfig::RULE_TYPE_OBJECT: + default: + return false; + } + } + + public function setRole(PhabricatorRole $role) { + $this->role = $role; + return $this; + } + + public function getRole() { + return $this->role; + } + + public function getObject() { + return $this->role; + } + + public function getAdapterContentName() { + return pht('Roles'); + } + + public function getHeraldName() { + return pht('Role %s', $this->getRole()->getName()); + } + +} diff --git a/src/applications/roles/herald/PhabricatorRoleHeraldFieldGroup.php b/src/applications/roles/herald/PhabricatorRoleHeraldFieldGroup.php new file mode 100644 index 0000000000..10c1754247 --- /dev/null +++ b/src/applications/roles/herald/PhabricatorRoleHeraldFieldGroup.php @@ -0,0 +1,15 @@ +applyRoles($effect->getTarget(), $is_add = false); + } + + public function getHeraldActionStandardType() { + return self::STANDARD_PHID_LIST; + } + + protected function getDatasource() { + return new PhabricatorRoleDatasource(); + } + + public function renderActionDescription($value) { + return pht('Remove roles: %s.', $this->renderHandleList($value)); + } + +} diff --git a/src/applications/roles/herald/PhabricatorRoleTagsAddedField.php b/src/applications/roles/herald/PhabricatorRoleTagsAddedField.php new file mode 100644 index 0000000000..ae9489a58a --- /dev/null +++ b/src/applications/roles/herald/PhabricatorRoleTagsAddedField.php @@ -0,0 +1,23 @@ +getRoleTagsTransaction(); + if (!$xaction) { + return array(); + } + + $record = PhabricatorEdgeChangeRecord::newFromTransaction($xaction); + + return $record->getAddedPHIDs(); + } + +} diff --git a/src/applications/roles/herald/PhabricatorRoleTagsField.php b/src/applications/roles/herald/PhabricatorRoleTagsField.php new file mode 100644 index 0000000000..ffc14df4ab --- /dev/null +++ b/src/applications/roles/herald/PhabricatorRoleTagsField.php @@ -0,0 +1,27 @@ +getAppliedEdgeTransactionOfType( + PhabricatorRoleObjectHasRoleEdgeType::EDGECONST); + } + +} diff --git a/src/applications/roles/herald/PhabricatorRoleTagsRemovedField.php b/src/applications/roles/herald/PhabricatorRoleTagsRemovedField.php new file mode 100644 index 0000000000..c16b5f4f96 --- /dev/null +++ b/src/applications/roles/herald/PhabricatorRoleTagsRemovedField.php @@ -0,0 +1,23 @@ +getRoleTagsTransaction(); + if (!$xaction) { + return array(); + } + + $record = PhabricatorEdgeChangeRecord::newFromTransaction($xaction); + + return $record->getRemovedPHIDs(); + } + +} diff --git a/src/applications/roles/icon/PhabricatorRoleDropEffect.php b/src/applications/roles/icon/PhabricatorRoleDropEffect.php new file mode 100644 index 0000000000..d291cd8590 --- /dev/null +++ b/src/applications/roles/icon/PhabricatorRoleDropEffect.php @@ -0,0 +1,83 @@ +icon = $icon; + return $this; + } + + public function getIcon() { + return $this->icon; + } + + public function setColor($color) { + $this->color = $color; + return $this; + } + + public function getColor() { + return $this->color; + } + + public function setContent($content) { + $this->content = $content; + return $this; + } + + public function getContent() { + return $this->content; + } + + public function toDictionary() { + return array( + 'icon' => $this->getIcon(), + 'color' => $this->getColor(), + 'content' => hsprintf('%s', $this->getContent()), + 'isTriggerEffect' => $this->getIsTriggerEffect(), + 'isHeader' => $this->getIsHeader(), + 'conditions' => $this->getConditions(), + ); + } + + public function addCondition($field, $operator, $value) { + $this->conditions[] = array( + 'field' => $field, + 'operator' => $operator, + 'value' => $value, + ); + + return $this; + } + + public function getConditions() { + return $this->conditions; + } + + public function setIsTriggerEffect($is_trigger_effect) { + $this->isTriggerEffect = $is_trigger_effect; + return $this; + } + + public function getIsTriggerEffect() { + return $this->isTriggerEffect; + } + + public function setIsHeader($is_header) { + $this->isHeader = $is_header; + return $this; + } + + public function getIsHeader() { + return $this->isHeader; + } + +} diff --git a/src/applications/roles/icon/PhabricatorRoleIconSet.php b/src/applications/roles/icon/PhabricatorRoleIconSet.php new file mode 100644 index 0000000000..1b4aab7f91 --- /dev/null +++ b/src/applications/roles/icon/PhabricatorRoleIconSet.php @@ -0,0 +1,509 @@ + 'role', + 'icon' => 'fa-briefcase', + 'name' => pht('Role'), + 'default' => true, + 'image' => 'v3/briefcase.png', + ), + array( + 'key' => 'tag', + 'icon' => 'fa-tags', + 'name' => pht('Tag'), + 'image' => 'v3/tag.png', + ), + array( + 'key' => 'policy', + 'icon' => 'fa-lock', + 'name' => pht('Policy'), + 'image' => 'v3/lock.png', + ), + array( + 'key' => 'management', + 'icon' => 'fa-users', + 'name' => pht('Management'), + 'image' => 'v3/people.png', + ), + array( + 'key' => 'folder', + 'icon' => 'fa-folder', + 'name' => pht('Folder'), + 'image' => 'v3/folder.png', + ), + array( + 'key' => 'timeline', + 'icon' => 'fa-calendar', + 'name' => pht('Timeline'), + 'image' => 'v3/calendar.png', + ), + array( + 'key' => 'developers', + 'icon' => 'fa-code', + 'name' => pht('Developers'), + 'image' => 'v3/code.png', + ), + array( + 'key' => 'release', + 'icon' => 'fa-truck', + 'name' => pht('Release'), + 'image' => 'v3/truck.png', + ), + array( + 'key' => 'qa', + 'icon' => 'fa-bug', + 'name' => pht('QA'), + 'image' => 'v3/bug.png', + ), + array( + 'key' => 'cleanup', + 'icon' => 'fa-trash-o', + 'name' => pht('Cleanup'), + 'image' => 'v3/trash.png', + ), + array( + 'key' => 'art', + 'icon' => 'fa-paint-brush', + 'name' => pht('Art'), + 'image' => 'v3/art.png', + ), + array( + 'key' => 'communication', + 'icon' => 'fa-envelope', + 'name' => pht('Communication'), + 'image' => 'v3/mail.png', + ), + array( + 'key' => 'organization', + 'icon' => 'fa-building', + 'name' => pht('Organization'), + 'image' => 'v3/organization.png', + ), + array( + 'key' => 'infrastructure', + 'icon' => 'fa-cloud', + 'name' => pht('Infrastructure'), + 'image' => 'v3/cloud.png', + ), + array( + 'key' => 'account', + 'icon' => 'fa-credit-card', + 'name' => pht('Account'), + 'image' => 'v3/creditcard.png', + ), + array( + 'key' => 'experimental', + 'icon' => 'fa-flask', + 'name' => pht('Experimental'), + 'image' => 'v3/experimental.png', + ), + array( + 'key' => 'milestone', + 'icon' => 'fa-map-marker', + 'name' => pht('Milestone'), + 'special' => self::SPECIAL_MILESTONE, + 'image' => 'v3/marker.png', + ), + ); + } + + + protected function newIcons() { + $map = self::getIconSpecifications(); + + $icons = array(); + foreach ($map as $spec) { + $special = idx($spec, 'special'); + + if ($special === self::SPECIAL_MILESTONE) { + continue; + } + + $icons[] = id(new PhabricatorIconSetIcon()) + ->setKey($spec['key']) + ->setIsDisabled(idx($spec, 'disabled')) + ->setIcon($spec['icon']) + ->setLabel($spec['name']); + } + + return $icons; + } + + private static function getIconSpecifications() { + return PhabricatorEnv::getEnvConfig('roles.icons'); + } + + public static function getDefaultIconKey() { + $icons = self::getIconSpecifications(); + foreach ($icons as $icon) { + if (idx($icon, 'default')) { + return $icon['key']; + } + } + return null; + } + + public static function getIconIcon($key) { + $spec = self::getIconSpec($key); + return idx($spec, 'icon', null); + } + + public static function getIconName($key) { + $spec = self::getIconSpec($key); + return idx($spec, 'name', null); + } + + public static function getIconImage($key) { + $spec = self::getIconSpec($key); + return idx($spec, 'image', 'v3/briefcase.png'); + } + + private static function getIconSpec($key) { + $icons = self::getIconSpecifications(); + foreach ($icons as $icon) { + if (idx($icon, 'key') === $key) { + return $icon; + } + } + + return array(); + } + + public static function getMilestoneIconKey() { + $icons = self::getIconSpecifications(); + foreach ($icons as $icon) { + if (idx($icon, 'special') === self::SPECIAL_MILESTONE) { + return idx($icon, 'key'); + } + } + return null; + } + + public static function validateConfiguration($config) { + if (!is_array($config)) { + throw new Exception( + pht('Configuration must be a list of role icon specifications.')); + } + + foreach ($config as $idx => $value) { + if (!is_array($value)) { + throw new Exception( + pht( + 'Value for index "%s" should be a dictionary.', + $idx)); + } + + PhutilTypeSpec::checkMap( + $value, + array( + 'key' => 'string', + 'name' => 'string', + 'icon' => 'string', + 'image' => 'optional string', + 'special' => 'optional string', + 'disabled' => 'optional bool', + 'default' => 'optional bool', + )); + + if (!preg_match('/^[a-z]{1,32}\z/', $value['key'])) { + throw new Exception( + pht( + 'Icon key "%s" is not a valid icon key. Icon keys must be 1-32 '. + 'characters long and contain only lowercase letters. For example, '. + '"%s" and "%s" are reasonable keys.', + $value['key'], + 'tag', + 'group')); + } + + $special = idx($value, 'special'); + $valid = array( + self::SPECIAL_MILESTONE => true, + ); + + if ($special !== null) { + if (empty($valid[$special])) { + throw new Exception( + pht( + 'Icon special attribute "%s" is not valid. Recognized special '. + 'attributes are: %s.', + $special, + implode(', ', array_keys($valid)))); + } + } + } + + $default = null; + $milestone = null; + $keys = array(); + foreach ($config as $idx => $value) { + $key = $value['key']; + if (isset($keys[$key])) { + throw new Exception( + pht( + 'Role icons must have unique keys, but two icons share the '. + 'same key ("%s").', + $key)); + } else { + $keys[$key] = true; + } + + $is_disabled = idx($value, 'disabled'); + + $image = idx($value, 'image'); + if ($image !== null) { + $builtin = idx($value, 'image'); + $builtin_map = id(new PhabricatorFilesOnDiskBuiltinFile()) + ->getRoleBuiltinFiles(); + $builtin_map = array_flip($builtin_map); + + $root = dirname(phutil_get_library_root('phabricator')); + $image = $root.'/resources/builtin/roles/'.$builtin; + + if (!array_key_exists($image, $builtin_map)) { + throw new Exception( + pht( + 'The role image ("%s") specified for ("%s") '. + 'was not found in the folder "resources/builtin/roles/".', + $builtin, + $key)); + } + } + + if (idx($value, 'default')) { + if ($default === null) { + if ($is_disabled) { + throw new Exception( + pht( + 'The role icon marked as the default icon ("%s") must not '. + 'be disabled.', + $key)); + } + $default = $value; + } else { + $original_key = $default['key']; + throw new Exception( + pht( + 'Two different icons ("%s", "%s") are marked as the default '. + 'icon. Only one icon may be marked as the default.', + $key, + $original_key)); + } + } + + $special = idx($value, 'special'); + if ($special === self::SPECIAL_MILESTONE) { + if ($milestone === null) { + if ($is_disabled) { + throw new Exception( + pht( + 'The role icon ("%s") with special attribute "%s" must '. + 'not be disabled', + $key, + self::SPECIAL_MILESTONE)); + } + $milestone = $value; + } else { + $original_key = $milestone['key']; + throw new Exception( + pht( + 'Two different icons ("%s", "%s") are marked with special '. + 'attribute "%s". Only one icon may be marked with this '. + 'attribute.', + $key, + $original_key, + self::SPECIAL_MILESTONE)); + } + } + } + + if ($default === null) { + throw new Exception( + pht( + 'Role icons must include one icon marked as the "%s" icon, '. + 'but no such icon exists.', + 'default')); + } + + if ($milestone === null) { + throw new Exception( + pht( + 'Role icons must include one icon marked with special attribute '. + '"%s", but no such icon exists.', + self::SPECIAL_MILESTONE)); + } + + } + + private static function getColorSpecifications() { + return PhabricatorEnv::getEnvConfig('roles.colors'); + } + + public static function getColorMap() { + $specifications = self::getColorSpecifications(); + return ipull($specifications, 'name', 'key'); + } + + public static function getDefaultColorKey() { + $specifications = self::getColorSpecifications(); + + foreach ($specifications as $specification) { + if (idx($specification, 'default')) { + return $specification['key']; + } + } + + return null; + } + + private static function getAvailableColorKeys() { + $list = array(); + + $specifications = self::getDefaultColorMap(); + foreach ($specifications as $specification) { + $list[] = $specification['key']; + } + + return $list; + } + + public static function getColorName($color_key) { + $map = self::getColorMap(); + return idx($map, $color_key); + } + + public static function getDefaultColorMap() { + return array( + array( + 'key' => PHUITagView::COLOR_RED, + 'name' => pht('Red'), + ), + array( + 'key' => PHUITagView::COLOR_ORANGE, + 'name' => pht('Orange'), + ), + array( + 'key' => PHUITagView::COLOR_YELLOW, + 'name' => pht('Yellow'), + ), + array( + 'key' => PHUITagView::COLOR_GREEN, + 'name' => pht('Green'), + ), + array( + 'key' => PHUITagView::COLOR_BLUE, + 'name' => pht('Blue'), + 'default' => true, + ), + array( + 'key' => PHUITagView::COLOR_INDIGO, + 'name' => pht('Indigo'), + ), + array( + 'key' => PHUITagView::COLOR_VIOLET, + 'name' => pht('Violet'), + ), + array( + 'key' => PHUITagView::COLOR_PINK, + 'name' => pht('Pink'), + ), + array( + 'key' => PHUITagView::COLOR_GREY, + 'name' => pht('Grey'), + ), + array( + 'key' => PHUITagView::COLOR_CHECKERED, + 'name' => pht('Checkered'), + ), + ); + } + + public static function validateColorConfiguration($config) { + if (!is_array($config)) { + throw new Exception( + pht('Configuration must be a list of role color specifications.')); + } + + $available_keys = self::getAvailableColorKeys(); + $available_keys = array_fuse($available_keys); + + foreach ($config as $idx => $value) { + if (!is_array($value)) { + throw new Exception( + pht( + 'Value for index "%s" should be a dictionary.', + $idx)); + } + + PhutilTypeSpec::checkMap( + $value, + array( + 'key' => 'string', + 'name' => 'string', + 'default' => 'optional bool', + )); + + $key = $value['key']; + if (!isset($available_keys[$key])) { + throw new Exception( + pht( + 'Color key "%s" is not a valid color key. The supported color '. + 'keys are: %s.', + $key, + implode(', ', $available_keys))); + } + } + + $default = null; + $keys = array(); + foreach ($config as $idx => $value) { + $key = $value['key']; + if (isset($keys[$key])) { + throw new Exception( + pht( + 'Role colors must have unique keys, but two icons share the '. + 'same key ("%s").', + $key)); + } else { + $keys[$key] = true; + } + + if (idx($value, 'default')) { + if ($default === null) { + $default = $value; + } else { + $original_key = $default['key']; + throw new Exception( + pht( + 'Two different colors ("%s", "%s") are marked as the default '. + 'color. Only one color may be marked as the default.', + $key, + $original_key)); + } + } + } + + if ($default === null) { + throw new Exception( + pht( + 'Role colors must include one color marked as the "%s" color, '. + 'but no such color exists.', + 'default')); + } + + } + +} diff --git a/src/applications/roles/interface/PhabricatorRoleColumnProxyInterface.php b/src/applications/roles/interface/PhabricatorRoleColumnProxyInterface.php new file mode 100644 index 0000000000..3fbbf739d2 --- /dev/null +++ b/src/applications/roles/interface/PhabricatorRoleColumnProxyInterface.php @@ -0,0 +1,7 @@ + array( + '[role]', + '[role] [tion]', + '[action] [role]', + '[action] [role] [tion]', + ), + 'role' => array( + 'Backend', + 'Frontend', + 'Web', + 'Mobile', + 'Tablet', + 'Robot', + 'NUX', + 'Cars', + 'Drones', + 'Experience', + 'Swag', + 'Security', + 'Culture', + 'Revenue', + 'Ion Cannon', + 'Graphics Engine', + 'Drivers', + 'Audio Drivers', + 'Graphics Drivers', + 'Hardware', + 'Data Center', + '[role] [role]', + '[adjective] [role]', + '[adjective] [role]', + ), + 'adjective' => array( + 'Self-Driving', + 'Self-Flying', + 'Self-Immolating', + 'Secure', + 'Insecure', + 'Somewhat-Secure', + 'Orbital', + 'Next-Generation', + ), + 'tion' => array( + 'Automation', + 'Optimization', + 'Performance', + 'Improvement', + 'Growth', + 'Monetization', + ), + 'action' => array( + 'Monetize', + 'Monetize', + 'Triage', + 'Triaging', + 'Automate', + 'Automating', + 'Improve', + 'Improving', + 'Optimize', + 'Optimizing', + 'Accelerate', + 'Accelerating', + ), + ); + } + +} diff --git a/src/applications/roles/lipsum/PhabricatorRoleTestDataGenerator.php b/src/applications/roles/lipsum/PhabricatorRoleTestDataGenerator.php new file mode 100644 index 0000000000..8a6e1fd3b5 --- /dev/null +++ b/src/applications/roles/lipsum/PhabricatorRoleTestDataGenerator.php @@ -0,0 +1,72 @@ +loadRandomUser(); + $role = PhabricatorRole::initializeNewRole($author); + + $xactions = array(); + + $xactions[] = $this->newTransaction( + PhabricatorRoleNameTransaction::TRANSACTIONTYPE, + $this->newRoleTitle()); + + $xactions[] = $this->newTransaction( + PhabricatorRoleStatusTransaction::TRANSACTIONTYPE, + $this->newRoleStatus()); + + // Almost always make the author a member. + $members = array(); + if ($this->roll(1, 20) > 2) { + $members[] = $author->getPHID(); + } + + // Add a few other members. + $size = $this->roll(2, 6, -2); + for ($ii = 0; $ii < $size; $ii++) { + $members[] = $this->loadRandomUser()->getPHID(); + } + + $xactions[] = $this->newTransaction( + PhabricatorTransactions::TYPE_EDGE, + array( + '+' => array_fuse($members), + ), + array( + 'edge:type' => PhabricatorRoleRoleHasMemberEdgeType::EDGECONST, + )); + + $editor = id(new PhabricatorRoleTransactionEditor()) + ->setActor($author) + ->setContentSource($this->getLipsumContentSource()) + ->setContinueOnNoEffect(true) + ->applyTransactions($role, $xactions); + + return $role; + } + + protected function newEmptyTransaction() { + return new PhabricatorRoleTransaction(); + } + + public function newRoleTitle() { + return id(new PhabricatorRoleNameContextFreeGrammar()) + ->generate(); + } + + public function newRoleStatus() { + if ($this->roll(1, 20) > 5) { + return PhabricatorRoleStatus::STATUS_ACTIVE; + } else { + return PhabricatorRoleStatus::STATUS_ARCHIVED; + } + } +} diff --git a/src/applications/roles/mail/RoleReplyHandler.php b/src/applications/roles/mail/RoleReplyHandler.php new file mode 100644 index 0000000000..ebeab472e1 --- /dev/null +++ b/src/applications/roles/mail/RoleReplyHandler.php @@ -0,0 +1,21 @@ +getMenuItemProperty('name'); + + if ($name !== null && $name !== '') { + return $name; + } + + return $this->getDefaultName(); + } + + public function buildEditEngineFields( + PhabricatorProfileMenuItemConfiguration $config) { + return array( + id(new PhabricatorTextEditField()) + ->setKey('name') + ->setLabel(pht('Name')) + ->setPlaceholder($this->getDefaultName()) + ->setValue($config->getMenuItemProperty('name')), + ); + } + + protected function newMenuItemViewList( + PhabricatorProfileMenuItemConfiguration $config) { + + $role = $config->getProfileObject(); + + $id = $role->getID(); + $name = $role->getName(); + $icon = $role->getDisplayIconIcon(); + + $uri = "/role/profile/{$id}/"; + + $item = $this->newItemView() + ->setURI($uri) + ->setName($name) + ->setIcon($icon); + + return array( + $item, + ); + } + +} diff --git a/src/applications/roles/menuitem/PhabricatorRoleManageProfileMenuItem.php b/src/applications/roles/menuitem/PhabricatorRoleManageProfileMenuItem.php new file mode 100644 index 0000000000..85d881600f --- /dev/null +++ b/src/applications/roles/menuitem/PhabricatorRoleManageProfileMenuItem.php @@ -0,0 +1,73 @@ +getMenuItemProperty('name'); + + if ($name !== null && $name !== '') { + return $name; + } + + return $this->getDefaultName(); + } + + public function buildEditEngineFields( + PhabricatorProfileMenuItemConfiguration $config) { + return array( + id(new PhabricatorTextEditField()) + ->setKey('name') + ->setLabel(pht('Name')) + ->setPlaceholder($this->getDefaultName()) + ->setValue($config->getMenuItemProperty('name')), + ); + } + + protected function newMenuItemViewList( + PhabricatorProfileMenuItemConfiguration $config) { + + $role = $config->getProfileObject(); + + $id = $role->getID(); + + $name = $this->getDisplayName($config); + $icon = 'fa-gears'; + $uri = "/role/manage/{$id}/"; + + $item = $this->newItemView() + ->setURI($uri) + ->setName($name) + ->setIcon($icon); + + return array( + $item, + ); + } + +} diff --git a/src/applications/roles/menuitem/PhabricatorRoleMembersProfileMenuItem.php b/src/applications/roles/menuitem/PhabricatorRoleMembersProfileMenuItem.php new file mode 100644 index 0000000000..0e25d9bbae --- /dev/null +++ b/src/applications/roles/menuitem/PhabricatorRoleMembersProfileMenuItem.php @@ -0,0 +1,63 @@ +getMenuItemProperty('name'); + + if ($name !== null && $name !== '') { + return $name; + } + + return $this->getDefaultName(); + } + + public function buildEditEngineFields( + PhabricatorProfileMenuItemConfiguration $config) { + return array( + id(new PhabricatorTextEditField()) + ->setKey('name') + ->setLabel(pht('Name')) + ->setPlaceholder($this->getDefaultName()) + ->setValue($config->getMenuItemProperty('name')), + ); + } + + protected function newMenuItemViewList( + PhabricatorProfileMenuItemConfiguration $config) { + + $role = $config->getProfileObject(); + + $id = $role->getID(); + + $name = $this->getDisplayName($config); + $icon = 'fa-group'; + $uri = "/role/members/{$id}/"; + + $item = $this->newItemView() + ->setURI($uri) + ->setName($name) + ->setIcon($icon); + + return array( + $item, + ); + } + +} diff --git a/src/applications/roles/menuitem/PhabricatorRolePictureProfileMenuItem.php b/src/applications/roles/menuitem/PhabricatorRolePictureProfileMenuItem.php new file mode 100644 index 0000000000..06f320a030 --- /dev/null +++ b/src/applications/roles/menuitem/PhabricatorRolePictureProfileMenuItem.php @@ -0,0 +1,51 @@ +getDefaultName(); + } + + public function buildEditEngineFields( + PhabricatorProfileMenuItemConfiguration $config) { + return array(); + } + + protected function newMenuItemViewList( + PhabricatorProfileMenuItemConfiguration $config) { + + $role = $config->getProfileObject(); + $picture = $role->getProfileImageURI(); + + $item = $this->newItemView() + ->setDisabled($role->isArchived()); + + $item->newProfileImage($picture); + + return array( + $item, + ); + } + +} diff --git a/src/applications/roles/menuitem/PhabricatorRolePointsProfileMenuItem.php b/src/applications/roles/menuitem/PhabricatorRolePointsProfileMenuItem.php new file mode 100644 index 0000000000..2333269d05 --- /dev/null +++ b/src/applications/roles/menuitem/PhabricatorRolePointsProfileMenuItem.php @@ -0,0 +1,177 @@ +getViewer(); + + // Only render this element for milestones. + if (!$object->isMilestone()) { + return false; + } + + // Don't show if points aren't configured. + if (!ManiphestTaskPoints::getIsEnabled()) { + return false; + } + + // Points are only available if Maniphest is installed. + $class = 'PhabricatorManiphestApplication'; + if (!PhabricatorApplication::isClassInstalledForViewer($class, $viewer)) { + return false; + } + + return true; + } + + public function getDisplayName( + PhabricatorProfileMenuItemConfiguration $config) { + return $this->getDefaultName(); + } + + public function buildEditEngineFields( + PhabricatorProfileMenuItemConfiguration $config) { + return array( + id(new PhabricatorInstructionsEditField()) + ->setValue( + pht( + 'This is a progress bar which shows how many points of work '. + 'are complete within the milestone. It has no configurable '. + 'settings.')), + ); + } + + protected function newMenuItemViewList( + PhabricatorProfileMenuItemConfiguration $config) { + $viewer = $this->getViewer(); + $role = $config->getProfileObject(); + + $limit = 250; + + $tasks = id(new ManiphestTaskQuery()) + ->setViewer($viewer) + ->withEdgeLogicPHIDs( + PhabricatorRoleObjectHasRoleEdgeType::EDGECONST, + PhabricatorQueryConstraint::OPERATOR_AND, + array($role->getPHID())) + ->setLimit($limit + 1) + ->execute(); + + $error = array(); + if (count($tasks) > $limit) { + $error[] = + pht( + 'Too many tasks (%s).', + new PhutilNumber($limit)); + } + + if (!$tasks) { + $error[] = pht('This milestone has no tasks.'); + } + + $statuses = array(); + $points_done = 0; + $points_total = 0; + $no_points = 0; + foreach ($tasks as $task) { + $points = $task->getPoints(); + + if ($points === null) { + $no_points++; + continue; + } + + if (!$points) { + continue; + } + + $status = $task->getStatus(); + if (empty($statuses[$status])) { + $statuses[$status] = 0; + } + $statuses[$status] += $points; + + if (ManiphestTaskStatus::isClosedStatus($status)) { + $points_done += $points; + } + + $points_total += $points; + } + + if ($no_points == count($tasks)) { + $error[] = pht('No tasks have points assigned.'); + } + + if (!$points_total) { + $error[] = pht('No tasks have positive points.'); + } + + $label = pht( + '%s of %s %s', + new PhutilNumber($points_done), + new PhutilNumber($points_total), + ManiphestTaskPoints::getPointsLabel()); + + $bar = id(new PHUISegmentBarView()) + ->setLabel($label); + + $map = ManiphestTaskStatus::getTaskStatusMap(); + $statuses = array_select_keys($statuses, array_keys($map)); + + foreach ($statuses as $status => $points) { + if (!$points) { + continue; + } + + if (!ManiphestTaskStatus::isClosedStatus($status)) { + continue; + } + + $color = ManiphestTaskStatus::getStatusColor($status); + if (!$color) { + $color = 'sky'; + } + + $tooltip = pht( + '%s %s', + new PhutilNumber($points), + ManiphestTaskStatus::getTaskStatusName($status)); + + $bar->newSegment() + ->setWidth($points / $points_total) + ->setColor($color) + ->setTooltip($tooltip); + } + + if ($error) { + $bar->setLabel(head($error)); + } + + $bar = phutil_tag( + 'div', + array( + 'class' => 'phui-profile-segment-bar', + ), + $bar); + + $item = $this->newItemView(); + + $item->newProgressBar($bar); + + return array( + $item, + ); + } + +} diff --git a/src/applications/roles/menuitem/PhabricatorRoleReportsProfileMenuItem.php b/src/applications/roles/menuitem/PhabricatorRoleReportsProfileMenuItem.php new file mode 100644 index 0000000000..652d8d886e --- /dev/null +++ b/src/applications/roles/menuitem/PhabricatorRoleReportsProfileMenuItem.php @@ -0,0 +1,85 @@ +getViewer(); + + if (!PhabricatorEnv::getEnvConfig('phabricator.show-prototypes')) { + return false; + } + + $class = 'PhabricatorManiphestApplication'; + if (!PhabricatorApplication::isClassInstalledForViewer($class, $viewer)) { + return false; + } + + $class = 'PhabricatorFactApplication'; + if (!PhabricatorApplication::isClassInstalledForViewer($class, $viewer)) { + return false; + } + + return true; + } + + public function getDisplayName( + PhabricatorProfileMenuItemConfiguration $config) { + $name = $config->getMenuItemProperty('name'); + + if ($name !== null && $name !== '') { + return $name; + } + + return $this->getDefaultName(); + } + + public function buildEditEngineFields( + PhabricatorProfileMenuItemConfiguration $config) { + return array( + id(new PhabricatorTextEditField()) + ->setKey('name') + ->setLabel(pht('Name')) + ->setPlaceholder($this->getDefaultName()) + ->setValue($config->getMenuItemProperty('name')), + ); + } + + protected function newMenuItemViewList( + PhabricatorProfileMenuItemConfiguration $config) { + $role = $config->getProfileObject(); + + $id = $role->getID(); + $uri = $role->getReportsURI(); + $name = $this->getDisplayName($config); + + $item = $this->newItemView() + ->setURI($uri) + ->setName($name) + ->setIcon('fa-area-chart'); + + return array( + $item, + ); + } + +} diff --git a/src/applications/roles/menuitem/PhabricatorRoleSubrolesProfileMenuItem.php b/src/applications/roles/menuitem/PhabricatorRoleSubrolesProfileMenuItem.php new file mode 100644 index 0000000000..5e0c525249 --- /dev/null +++ b/src/applications/roles/menuitem/PhabricatorRoleSubrolesProfileMenuItem.php @@ -0,0 +1,70 @@ +isMilestone()) { + return false; + } + + return true; + } + + public function getDisplayName( + PhabricatorProfileMenuItemConfiguration $config) { + $name = $config->getMenuItemProperty('name'); + + if ($name !== null && $name !== '') { + return $name; + } + + return $this->getDefaultName(); + } + + public function buildEditEngineFields( + PhabricatorProfileMenuItemConfiguration $config) { + return array( + id(new PhabricatorTextEditField()) + ->setKey('name') + ->setLabel(pht('Name')) + ->setPlaceholder($this->getDefaultName()) + ->setValue($config->getMenuItemProperty('name')), + ); + } + + protected function newMenuItemViewList( + PhabricatorProfileMenuItemConfiguration $config) { + + $role = $config->getProfileObject(); + $id = $role->getID(); + + $name = $this->getDisplayName($config); + $icon = 'fa-sitemap'; + $uri = "/role/subroles/{$id}/"; + + $item = $this->newItemView() + ->setURI($uri) + ->setName($name) + ->setIcon($icon); + + return array( + $item, + ); + } + +} diff --git a/src/applications/roles/menuitem/PhabricatorRoleWorkboardProfileMenuItem.php b/src/applications/roles/menuitem/PhabricatorRoleWorkboardProfileMenuItem.php new file mode 100644 index 0000000000..4c49ce58b7 --- /dev/null +++ b/src/applications/roles/menuitem/PhabricatorRoleWorkboardProfileMenuItem.php @@ -0,0 +1,77 @@ +getViewer(); + + // Workboards are only available if Maniphest is installed. + $class = 'PhabricatorManiphestApplication'; + if (!PhabricatorApplication::isClassInstalledForViewer($class, $viewer)) { + return false; + } + + return true; + } + + public function getDisplayName( + PhabricatorProfileMenuItemConfiguration $config) { + $name = $config->getMenuItemProperty('name'); + + if ($name !== null && $name !== '') { + return $name; + } + + return $this->getDefaultName(); + } + + public function buildEditEngineFields( + PhabricatorProfileMenuItemConfiguration $config) { + return array( + id(new PhabricatorTextEditField()) + ->setKey('name') + ->setLabel(pht('Name')) + ->setPlaceholder($this->getDefaultName()) + ->setValue($config->getMenuItemProperty('name')), + ); + } + + protected function newMenuItemViewList( + PhabricatorProfileMenuItemConfiguration $config) { + $role = $config->getProfileObject(); + + $id = $role->getID(); + $uri = $role->getWorkboardURI(); + $name = $this->getDisplayName($config); + + $item = $this->newItemView() + ->setURI($uri) + ->setName($name) + ->setIcon('fa-columns'); + + return array( + $item, + ); + } + +} diff --git a/src/applications/roles/order/PhabricatorRoleColumnAuthorOrder.php b/src/applications/roles/order/PhabricatorRoleColumnAuthorOrder.php new file mode 100644 index 0000000000..127e69996d --- /dev/null +++ b/src/applications/roles/order/PhabricatorRoleColumnAuthorOrder.php @@ -0,0 +1,139 @@ +newHeaderKeyForAuthorPHID($object->getAuthorPHID()); + } + + private function newHeaderKeyForAuthorPHID($author_phid) { + return sprintf('author(%s)', $author_phid); + } + + protected function newSortVectorsForObjects(array $objects) { + $author_phids = mpull($objects, null, 'getAuthorPHID'); + $author_phids = array_keys($author_phids); + $author_phids = array_filter($author_phids); + + if ($author_phids) { + $author_users = id(new PhabricatorPeopleQuery()) + ->setViewer($this->getViewer()) + ->withPHIDs($author_phids) + ->execute(); + $author_users = mpull($author_users, null, 'getPHID'); + } else { + $author_users = array(); + } + + $vectors = array(); + foreach ($objects as $vector_key => $object) { + $author_phid = $object->getAuthorPHID(); + $author = idx($author_users, $author_phid); + if ($author) { + $vector = $this->newSortVectorForAuthor($author); + } else { + $vector = $this->newSortVectorForAuthorPHID($author_phid); + } + + $vectors[$vector_key] = $vector; + } + + return $vectors; + } + + private function newSortVectorForAuthor(PhabricatorUser $user) { + return array( + 1, + $user->getUsername(), + ); + } + + private function newSortVectorForAuthorPHID($author_phid) { + return array( + 2, + $author_phid, + ); + } + + protected function newHeadersForObjects(array $objects) { + $author_phids = mpull($objects, null, 'getAuthorPHID'); + $author_phids = array_keys($author_phids); + $author_phids = array_filter($author_phids); + + if ($author_phids) { + $author_users = id(new PhabricatorPeopleQuery()) + ->setViewer($this->getViewer()) + ->withPHIDs($author_phids) + ->needProfileImage(true) + ->execute(); + $author_users = mpull($author_users, null, 'getPHID'); + } else { + $author_users = array(); + } + + $headers = array(); + foreach ($author_phids as $author_phid) { + $header_key = $this->newHeaderKeyForAuthorPHID($author_phid); + + $author = idx($author_users, $author_phid); + if ($author) { + $sort_vector = $this->newSortVectorForAuthor($author); + $author_name = $author->getUsername(); + $author_image = $author->getProfileImageURI(); + } else { + $sort_vector = $this->newSortVectorForAuthorPHID($author_phid); + $author_name = pht('Unknown User ("%s")', $author_phid); + $author_image = null; + } + + $author_icon = 'fa-user'; + $author_color = 'bluegrey'; + + $icon_view = id(new PHUIIconView()); + + if ($author_image) { + $icon_view->setImage($author_image); + } else { + $icon_view->setIcon($author_icon, $author_color); + } + + $header = $this->newHeader() + ->setHeaderKey($header_key) + ->setSortVector($sort_vector) + ->setName($author_name) + ->setIcon($icon_view) + ->setEditProperties( + array( + 'value' => $author_phid, + )); + + $headers[] = $header; + } + + return $headers; + } + +} diff --git a/src/applications/roles/order/PhabricatorRoleColumnCreatedOrder.php b/src/applications/roles/order/PhabricatorRoleColumnCreatedOrder.php new file mode 100644 index 0000000000..59a8d5ad46 --- /dev/null +++ b/src/applications/roles/order/PhabricatorRoleColumnCreatedOrder.php @@ -0,0 +1,35 @@ +getDateCreated(), + -1 * (int)$object->getID(), + ); + } + +} diff --git a/src/applications/roles/order/PhabricatorRoleColumnHeader.php b/src/applications/roles/order/PhabricatorRoleColumnHeader.php new file mode 100644 index 0000000000..9af4c7e7aa --- /dev/null +++ b/src/applications/roles/order/PhabricatorRoleColumnHeader.php @@ -0,0 +1,110 @@ +orderKey = $order_key; + return $this; + } + + public function getOrderKey() { + return $this->orderKey; + } + + public function setHeaderKey($header_key) { + $this->headerKey = $header_key; + return $this; + } + + public function getHeaderKey() { + return $this->headerKey; + } + + public function setSortVector(array $sort_vector) { + $this->sortVector = $sort_vector; + return $this; + } + + public function getSortVector() { + return $this->sortVector; + } + + public function setName($name) { + $this->name = $name; + return $this; + } + + public function getName() { + return $this->name; + } + + public function setIcon(PHUIIconView$icon) { + $this->icon = $icon; + return $this; + } + + public function getIcon() { + return $this->icon; + } + + public function setEditProperties(array $edit_properties) { + $this->editProperties = $edit_properties; + return $this; + } + + public function getEditProperties() { + return $this->editProperties; + } + + public function addDropEffect(PhabricatorRoleDropEffect $effect) { + $this->dropEffects[] = $effect; + return $this; + } + + public function getDropEffects() { + return $this->dropEffects; + } + + public function toDictionary() { + return array( + 'order' => $this->getOrderKey(), + 'key' => $this->getHeaderKey(), + 'template' => hsprintf('%s', $this->newView()), + 'vector' => $this->getSortVector(), + 'editProperties' => $this->getEditProperties(), + 'effects' => mpull($this->getDropEffects(), 'toDictionary'), + ); + } + + private function newView() { + $icon_view = $this->getIcon(); + $name = $this->getName(); + + $template = phutil_tag( + 'li', + array( + 'class' => 'workboard-group-header', + ), + array( + $icon_view, + phutil_tag( + 'span', + array( + 'class' => 'workboard-group-header-name', + ), + $name), + )); + + return $template; + } + +} diff --git a/src/applications/roles/order/PhabricatorRoleColumnNaturalOrder.php b/src/applications/roles/order/PhabricatorRoleColumnNaturalOrder.php new file mode 100644 index 0000000000..05556cf3bf --- /dev/null +++ b/src/applications/roles/order/PhabricatorRoleColumnNaturalOrder.php @@ -0,0 +1,24 @@ +viewer = $viewer; + return $this; + } + + final public function getViewer() { + return $this->viewer; + } + + final public function getColumnOrderKey() { + return $this->getPhobjectClassConstant('ORDERKEY'); + } + + final public static function getAllOrders() { + return id(new PhutilClassMapQuery()) + ->setAncestorClass(__CLASS__) + ->setUniqueMethod('getColumnOrderKey') + ->setSortMethod('getMenuOrder') + ->execute(); + } + + final public static function getEnabledOrders() { + $map = self::getAllOrders(); + + foreach ($map as $key => $order) { + if (!$order->isEnabled()) { + unset($map[$key]); + } + } + + return $map; + } + + final public static function getOrderByKey($key) { + $map = self::getAllOrders(); + + if (!isset($map[$key])) { + throw new Exception( + pht( + 'No column ordering exists with key "%s".', + $key)); + } + + return $map[$key]; + } + + final public function getColumnTransactions($object, array $header) { + $result = $this->newColumnTransactions($object, $header); + + if (!is_array($result) && !is_null($result)) { + throw new Exception( + pht( + 'Expected "newColumnTransactions()" on "%s" to return "null" or a '. + 'list of transactions, but got "%s".', + get_class($this), + phutil_describe_type($result))); + } + + if ($result === null) { + $result = array(); + } + + assert_instances_of($result, 'PhabricatorApplicationTransaction'); + + return $result; + } + + final public function getMenuIconIcon() { + return $this->newMenuIconIcon(); + } + + protected function newMenuIconIcon() { + return 'fa-sort-amount-asc'; + } + + abstract public function getDisplayName(); + abstract public function getHasHeaders(); + abstract public function getCanReorder(); + + public function getMenuOrder() { + return 9000; + } + + public function isEnabled() { + return true; + } + + protected function newColumnTransactions($object, array $header) { + return array(); + } + + final public function getHeadersForObjects(array $objects) { + $headers = $this->newHeadersForObjects($objects); + + if (!is_array($headers)) { + throw new Exception( + pht( + 'Expected "newHeadersForObjects()" on "%s" to return a list '. + 'of headers, but got "%s".', + get_class($this), + phutil_describe_type($headers))); + } + + assert_instances_of($headers, 'PhabricatorRoleColumnHeader'); + + // Add a "0" to the end of each header. This makes them sort above object + // cards in the same group. + foreach ($headers as $header) { + $vector = $header->getSortVector(); + $vector[] = 0; + $header->setSortVector($vector); + } + + return $headers; + } + + protected function newHeadersForObjects(array $objects) { + return array(); + } + + final public function getSortVectorsForObjects(array $objects) { + $vectors = $this->newSortVectorsForObjects($objects); + + if (!is_array($vectors)) { + throw new Exception( + pht( + 'Expected "newSortVectorsForObjects()" on "%s" to return a '. + 'map of vectors, but got "%s".', + get_class($this), + phutil_describe_type($vectors))); + } + + assert_same_keys($objects, $vectors); + + return $vectors; + } + + protected function newSortVectorsForObjects(array $objects) { + $vectors = array(); + + foreach ($objects as $key => $object) { + $vectors[$key] = $this->newSortVectorForObject($object); + } + + return $vectors; + } + + protected function newSortVectorForObject($object) { + return array(); + } + + final public function getHeaderKeysForObjects(array $objects) { + $header_keys = $this->newHeaderKeysForObjects($objects); + + if (!is_array($header_keys)) { + throw new Exception( + pht( + 'Expected "newHeaderKeysForObject()" on "%s" to return a '. + 'map of header keys, but got "%s".', + get_class($this), + phutil_describe_type($header_keys))); + } + + assert_same_keys($objects, $header_keys); + + return $header_keys; + } + + protected function newHeaderKeysForObjects(array $objects) { + $header_keys = array(); + + foreach ($objects as $key => $object) { + $header_keys[$key] = $this->newHeaderKeyForObject($object); + } + + return $header_keys; + } + + protected function newHeaderKeyForObject($object) { + return null; + } + + final protected function newTransaction($object) { + return $object->getApplicationTransactionTemplate(); + } + + final protected function newHeader() { + return id(new PhabricatorRoleColumnHeader()) + ->setOrderKey($this->getColumnOrderKey()); + } + + final protected function newEffect() { + return new PhabricatorRoleDropEffect(); + } + + final public function toDictionary() { + return array( + 'orderKey' => $this->getColumnOrderKey(), + 'hasHeaders' => $this->getHasHeaders(), + 'canReorder' => $this->getCanReorder(), + ); + } + +} diff --git a/src/applications/roles/order/PhabricatorRoleColumnOwnerOrder.php b/src/applications/roles/order/PhabricatorRoleColumnOwnerOrder.php new file mode 100644 index 0000000000..abc72b4ebb --- /dev/null +++ b/src/applications/roles/order/PhabricatorRoleColumnOwnerOrder.php @@ -0,0 +1,199 @@ +newHeaderKeyForOwnerPHID($object->getOwnerPHID()); + } + + private function newHeaderKeyForOwnerPHID($owner_phid) { + if ($owner_phid === null) { + $owner_phid = ''; + } + + return sprintf('owner(%s)', $owner_phid); + } + + protected function newSortVectorsForObjects(array $objects) { + $owner_phids = mpull($objects, null, 'getOwnerPHID'); + $owner_phids = array_keys($owner_phids); + $owner_phids = array_filter($owner_phids); + + if ($owner_phids) { + $owner_users = id(new PhabricatorPeopleQuery()) + ->setViewer($this->getViewer()) + ->withPHIDs($owner_phids) + ->execute(); + $owner_users = mpull($owner_users, null, 'getPHID'); + } else { + $owner_users = array(); + } + + $vectors = array(); + foreach ($objects as $vector_key => $object) { + $owner_phid = $object->getOwnerPHID(); + if (!$owner_phid) { + $vector = $this->newSortVectorForUnowned(); + } else { + $owner = idx($owner_users, $owner_phid); + if ($owner) { + $vector = $this->newSortVectorForOwner($owner); + } else { + $vector = $this->newSortVectorForOwnerPHID($owner_phid); + } + } + + $vectors[$vector_key] = $vector; + } + + return $vectors; + } + + private function newSortVectorForUnowned() { + // Always put unasssigned tasks at the top. + return array( + 0, + ); + } + + private function newSortVectorForOwner(PhabricatorUser $user) { + // Put assigned tasks with a valid owner after "Unassigned", but above + // assigned tasks with an invalid owner. Sort these tasks by the owner's + // username. + return array( + 1, + $user->getUsername(), + ); + } + + private function newSortVectorForOwnerPHID($owner_phid) { + // If we have tasks with a nonempty owner but can't load the associated + // "User" object, move them to the bottom. We can only sort these by the + // PHID. + return array( + 2, + $owner_phid, + ); + } + + protected function newHeadersForObjects(array $objects) { + $owner_phids = mpull($objects, null, 'getOwnerPHID'); + $owner_phids = array_keys($owner_phids); + $owner_phids = array_filter($owner_phids); + + if ($owner_phids) { + $owner_users = id(new PhabricatorPeopleQuery()) + ->setViewer($this->getViewer()) + ->withPHIDs($owner_phids) + ->needProfileImage(true) + ->execute(); + $owner_users = mpull($owner_users, null, 'getPHID'); + } else { + $owner_users = array(); + } + + array_unshift($owner_phids, null); + + $headers = array(); + foreach ($owner_phids as $owner_phid) { + $header_key = $this->newHeaderKeyForOwnerPHID($owner_phid); + + $owner_image = null; + $effect_content = null; + if ($owner_phid === null) { + $owner = null; + $sort_vector = $this->newSortVectorForUnowned(); + $owner_name = pht('Not Assigned'); + + $effect_content = pht('Remove task assignee.'); + } else { + $owner = idx($owner_users, $owner_phid); + if ($owner) { + $sort_vector = $this->newSortVectorForOwner($owner); + $owner_name = $owner->getUsername(); + $owner_image = $owner->getProfileImageURI(); + + $effect_content = pht( + 'Assign task to %s.', + phutil_tag('strong', array(), $owner_name)); + } else { + $sort_vector = $this->newSortVectorForOwnerPHID($owner_phid); + $owner_name = pht('Unknown User ("%s")', $owner_phid); + } + } + + $owner_icon = 'fa-user'; + $owner_color = 'bluegrey'; + + $icon_view = id(new PHUIIconView()); + + if ($owner_image) { + $icon_view->setImage($owner_image); + } else { + $icon_view->setIcon($owner_icon, $owner_color); + } + + $header = $this->newHeader() + ->setHeaderKey($header_key) + ->setSortVector($sort_vector) + ->setName($owner_name) + ->setIcon($icon_view) + ->setEditProperties( + array( + 'value' => $owner_phid, + )); + + if ($effect_content !== null) { + $header->addDropEffect( + $this->newEffect() + ->setIcon($owner_icon) + ->setColor($owner_color) + ->addCondition('owner', '!=', $owner_phid) + ->setContent($effect_content)); + } + + $headers[] = $header; + } + + return $headers; + } + + protected function newColumnTransactions($object, array $header) { + $new_owner = idx($header, 'value'); + + if ($object->getOwnerPHID() === $new_owner) { + return null; + } + + $xactions = array(); + $xactions[] = $this->newTransaction($object) + ->setTransactionType(ManiphestTaskOwnerTransaction::TRANSACTIONTYPE) + ->setNewValue($new_owner); + + return $xactions; + } + +} diff --git a/src/applications/roles/order/PhabricatorRoleColumnPointsOrder.php b/src/applications/roles/order/PhabricatorRoleColumnPointsOrder.php new file mode 100644 index 0000000000..61debfd3d5 --- /dev/null +++ b/src/applications/roles/order/PhabricatorRoleColumnPointsOrder.php @@ -0,0 +1,50 @@ +getPoints(); + + // Put cards with no points on top. + $has_points = ($points !== null); + if (!$has_points) { + $overall_order = 0; + } else { + $overall_order = 1; + } + + return array( + $overall_order, + -1.0 * (double)$points, + -1 * (int)$object->getID(), + ); + } + +} diff --git a/src/applications/roles/order/PhabricatorRoleColumnPriorityOrder.php b/src/applications/roles/order/PhabricatorRoleColumnPriorityOrder.php new file mode 100644 index 0000000000..632fbfc0cd --- /dev/null +++ b/src/applications/roles/order/PhabricatorRoleColumnPriorityOrder.php @@ -0,0 +1,113 @@ +newHeaderKeyForPriority($object->getPriority()); + } + + private function newHeaderKeyForPriority($priority) { + return sprintf('priority(%d)', $priority); + } + + protected function newSortVectorForObject($object) { + return $this->newSortVectorForPriority($object->getPriority()); + } + + private function newSortVectorForPriority($priority) { + return array( + -1 * (int)$priority, + ); + } + + protected function newHeadersForObjects(array $objects) { + $priorities = ManiphestTaskPriority::getTaskPriorityMap(); + + // It's possible for tasks to have an invalid/unknown priority in the + // database. We still want to generate a header for these tasks so we + // don't break the workboard. + $priorities = $priorities + mpull($objects, null, 'getPriority'); + + $priorities = array_keys($priorities); + + $headers = array(); + foreach ($priorities as $priority) { + $header_key = $this->newHeaderKeyForPriority($priority); + $sort_vector = $this->newSortVectorForPriority($priority); + + $priority_name = ManiphestTaskPriority::getTaskPriorityName($priority); + $priority_color = ManiphestTaskPriority::getTaskPriorityColor($priority); + $priority_icon = ManiphestTaskPriority::getTaskPriorityIcon($priority); + + $icon_view = id(new PHUIIconView()) + ->setIcon($priority_icon, $priority_color); + + $drop_effect = $this->newEffect() + ->setIcon($priority_icon) + ->setColor($priority_color) + ->addCondition('priority', '!=', $priority) + ->setContent( + pht( + 'Change priority to %s.', + phutil_tag('strong', array(), $priority_name))); + + $header = $this->newHeader() + ->setHeaderKey($header_key) + ->setSortVector($sort_vector) + ->setName($priority_name) + ->setIcon($icon_view) + ->setEditProperties( + array( + 'value' => (int)$priority, + )) + ->addDropEffect($drop_effect); + + $headers[] = $header; + } + + return $headers; + } + + protected function newColumnTransactions($object, array $header) { + $new_priority = idx($header, 'value'); + + if ($object->getPriority() === $new_priority) { + return null; + } + + $keyword_map = ManiphestTaskPriority::getTaskPriorityKeywordsMap(); + $keyword = head(idx($keyword_map, $new_priority)); + + $xactions = array(); + $xactions[] = $this->newTransaction($object) + ->setTransactionType(ManiphestTaskPriorityTransaction::TRANSACTIONTYPE) + ->setNewValue($keyword); + + return $xactions; + } + + +} diff --git a/src/applications/roles/order/PhabricatorRoleColumnStatusOrder.php b/src/applications/roles/order/PhabricatorRoleColumnStatusOrder.php new file mode 100644 index 0000000000..1a8c19a31c --- /dev/null +++ b/src/applications/roles/order/PhabricatorRoleColumnStatusOrder.php @@ -0,0 +1,116 @@ +newHeaderKeyForStatus($object->getStatus()); + } + + private function newHeaderKeyForStatus($status) { + return sprintf('status(%s)', $status); + } + + protected function newSortVectorsForObjects(array $objects) { + $status_sequence = $this->newStatusSequence(); + + $vectors = array(); + foreach ($objects as $object_key => $object) { + $vectors[$object_key] = array( + (int)idx($status_sequence, $object->getStatus(), 0), + ); + } + + return $vectors; + } + + private function newStatusSequence() { + $statuses = ManiphestTaskStatus::getTaskStatusMap(); + return array_combine( + array_keys($statuses), + range(1, count($statuses))); + } + + protected function newHeadersForObjects(array $objects) { + $headers = array(); + + $statuses = ManiphestTaskStatus::getTaskStatusMap(); + $sequence = $this->newStatusSequence(); + + foreach ($statuses as $status_key => $status_name) { + $header_key = $this->newHeaderKeyForStatus($status_key); + + $sort_vector = array( + (int)idx($sequence, $status_key, 0), + ); + + $status_icon = ManiphestTaskStatus::getStatusIcon($status_key); + $status_color = ManiphestTaskStatus::getStatusColor($status_key); + + $icon_view = id(new PHUIIconView()) + ->setIcon($status_icon, $status_color); + + $drop_effect = $this->newEffect() + ->setIcon($status_icon) + ->setColor($status_color) + ->addCondition('status', '!=', $status_key) + ->setContent( + pht( + 'Change status to %s.', + phutil_tag('strong', array(), $status_name))); + + $header = $this->newHeader() + ->setHeaderKey($header_key) + ->setSortVector($sort_vector) + ->setName($status_name) + ->setIcon($icon_view) + ->setEditProperties( + array( + 'value' => $status_key, + )) + ->addDropEffect($drop_effect); + + $headers[] = $header; + } + + return $headers; + } + + protected function newColumnTransactions($object, array $header) { + $new_status = idx($header, 'value'); + + if ($object->getStatus() === $new_status) { + return null; + } + + $xactions = array(); + $xactions[] = $this->newTransaction($object) + ->setTransactionType(ManiphestTaskStatusTransaction::TRANSACTIONTYPE) + ->setNewValue($new_status); + + return $xactions; + } + +} diff --git a/src/applications/roles/order/PhabricatorRoleColumnTitleOrder.php b/src/applications/roles/order/PhabricatorRoleColumnTitleOrder.php new file mode 100644 index 0000000000..1bbceea5a4 --- /dev/null +++ b/src/applications/roles/order/PhabricatorRoleColumnTitleOrder.php @@ -0,0 +1,34 @@ +getTitle(), + ); + } + +} diff --git a/src/applications/roles/phid/PhabricatorRoleColumnPHIDType.php b/src/applications/roles/phid/PhabricatorRoleColumnPHIDType.php new file mode 100644 index 0000000000..a9956bab01 --- /dev/null +++ b/src/applications/roles/phid/PhabricatorRoleColumnPHIDType.php @@ -0,0 +1,48 @@ +withPHIDs($phids); + } + + public function loadHandles( + PhabricatorHandleQuery $query, + array $handles, + array $objects) { + + foreach ($handles as $phid => $handle) { + $column = $objects[$phid]; + + $handle->setName($column->getDisplayName()); + $handle->setURI($column->getWorkboardURI()); + + if ($column->isHidden()) { + $handle->setStatus(PhabricatorObjectHandle::STATUS_CLOSED); + } + } + } + +} diff --git a/src/applications/roles/phid/PhabricatorRoleRolePHIDType.php b/src/applications/roles/phid/PhabricatorRoleRolePHIDType.php new file mode 100644 index 0000000000..a71b5658ed --- /dev/null +++ b/src/applications/roles/phid/PhabricatorRoleRolePHIDType.php @@ -0,0 +1,121 @@ +withPHIDs($phids) + ->needImages(true); + } + + public function loadHandles( + PhabricatorHandleQuery $query, + array $handles, + array $objects) { + + foreach ($handles as $phid => $handle) { + $role = $objects[$phid]; + + $name = $role->getDisplayName(); + $id = $role->getID(); + $slug = $role->getPrimarySlug(); + + $handle->setName($name); + + if ($slug !== null && $slug !== '') { + $handle->setObjectName('#'.$slug); + $handle->setMailStampName('#'.$slug); + $handle->setURI("/tag/{$slug}/"); + } else { + // We set the name to the role's PHID to avoid a parse error when a + // role has no hashtag (as is the case with milestones by default). + // See T12659 for more details. + $handle->setCommandLineObjectName($role->getPHID()); + $handle->setURI("/role/view/{$id}/"); + } + + $handle->setImageURI($role->getProfileImageURI()); + $handle->setIcon($role->getDisplayIconIcon()); + $handle->setTagColor($role->getDisplayColor()); + + if ($role->isArchived()) { + $handle->setStatus(PhabricatorObjectHandle::STATUS_CLOSED); + } + } + } + + public static function getRoleMonogramPatternFragment() { + // NOTE: See some discussion in RoleRemarkupRule. + return '[^\s,#]+'; + } + + public function canLoadNamedObject($name) { + $fragment = self::getRoleMonogramPatternFragment(); + return preg_match('/^#'.$fragment.'$/i', $name); + } + + public function loadNamedObjects( + PhabricatorObjectQuery $query, + array $names) { + + // If the user types "#YoloSwag", we still want to match "#yoloswag", so + // we normalize, query, and then map back to the original inputs. + + $map = array(); + foreach ($names as $key => $slug) { + $map[$this->normalizeSlug(substr($slug, 1))][] = $slug; + } + + $roles = id(new PhabricatorRoleQuery()) + ->setViewer($query->getViewer()) + ->withSlugs(array_keys($map)) + ->needSlugs(true) + ->execute(); + + $result = array(); + foreach ($roles as $role) { + $slugs = $role->getSlugs(); + $slug_strs = mpull($slugs, 'getSlug'); + foreach ($slug_strs as $slug) { + $slug_map = idx($map, $slug, array()); + foreach ($slug_map as $original) { + $result[$original] = $role; + } + } + } + + return $result; + } + + private function normalizeSlug($slug) { + // NOTE: We're using phutil_utf8_strtolower() (and not PhabricatorSlug's + // normalize() method) because this normalization should be only somewhat + // liberal. We want "#YOLO" to match against "#yolo", but "#\\yo!!lo" + // should not. normalize() strips out most punctuation and leads to + // excessively aggressive matches. + + return phutil_utf8_strtolower($slug); + } + +} diff --git a/src/applications/roles/phid/PhabricatorRoleTriggerPHIDType.php b/src/applications/roles/phid/PhabricatorRoleTriggerPHIDType.php new file mode 100644 index 0000000000..767d0c1373 --- /dev/null +++ b/src/applications/roles/phid/PhabricatorRoleTriggerPHIDType.php @@ -0,0 +1,45 @@ +withPHIDs($phids); + } + + public function loadHandles( + PhabricatorHandleQuery $query, + array $handles, + array $objects) { + + foreach ($handles as $phid => $handle) { + $trigger = $objects[$phid]; + + $handle->setName($trigger->getDisplayName()); + $handle->setURI($trigger->getURI()); + } + } + +} diff --git a/src/applications/roles/policyrule/PhabricatorRoleMembersPolicyRule.php b/src/applications/roles/policyrule/PhabricatorRoleMembersPolicyRule.php new file mode 100644 index 0000000000..5ac39e8f3a --- /dev/null +++ b/src/applications/roles/policyrule/PhabricatorRoleMembersPolicyRule.php @@ -0,0 +1,97 @@ +getPHID(); + if (!$viewer_phid) { + return; + } + + if (empty($this->memberships[$viewer_phid])) { + $this->memberships[$viewer_phid] = array(); + } + + foreach ($objects as $key => $object) { + $cache = $this->getTransactionHint($object); + if ($cache === null) { + continue; + } + + unset($objects[$key]); + + if (isset($cache[$viewer_phid])) { + $this->memberships[$viewer_phid][$object->getPHID()] = true; + } + } + + if (!$objects) { + return; + } + + $object_phids = mpull($objects, 'getPHID'); + $edge_query = id(new PhabricatorEdgeQuery()) + ->withSourcePHIDs(array($viewer_phid)) + ->withDestinationPHIDs($object_phids) + ->withEdgeTypes( + array( + PhabricatorRoleMemberOfRoleEdgeType::EDGECONST, + )); + $edge_query->execute(); + + $memberships = $edge_query->getDestinationPHIDs(); + if (!$memberships) { + return; + } + + $this->memberships[$viewer_phid] += array_fill_keys($memberships, true); + } + + public function applyRule( + PhabricatorUser $viewer, + $value, + PhabricatorPolicyInterface $object) { + $viewer_phid = $viewer->getPHID(); + if (!$viewer_phid) { + return false; + } + + $memberships = idx($this->memberships, $viewer_phid); + return isset($memberships[$object->getPHID()]); + } + + public function getValueControlType() { + return self::CONTROL_TYPE_NONE; + } + + public function canApplyToObject(PhabricatorPolicyInterface $object) { + return ($object instanceof PhabricatorRole); + } + + public function getObjectPolicyKey() { + return 'role.members'; + } + + public function getObjectPolicyName() { + return pht('Role Members'); + } + + public function getObjectPolicyIcon() { + return 'fa-users'; + } + + public function getPolicyExplanation() { + return pht('Role members can take this action.'); + } + +} diff --git a/src/applications/roles/policyrule/PhabricatorRolesAllPolicyRule.php b/src/applications/roles/policyrule/PhabricatorRolesAllPolicyRule.php new file mode 100644 index 0000000000..25c9b1c5b6 --- /dev/null +++ b/src/applications/roles/policyrule/PhabricatorRolesAllPolicyRule.php @@ -0,0 +1,29 @@ +getMemberships($viewer->getPHID()); + foreach ($value as $role_phid) { + if (empty($memberships[$role_phid])) { + return false; + } + } + + return true; + } + + public function getRuleOrder() { + return 205; + } + +} diff --git a/src/applications/roles/policyrule/PhabricatorRolesBasePolicyRule.php b/src/applications/roles/policyrule/PhabricatorRolesBasePolicyRule.php new file mode 100644 index 0000000000..13add00d85 --- /dev/null +++ b/src/applications/roles/policyrule/PhabricatorRolesBasePolicyRule.php @@ -0,0 +1,64 @@ +memberships, $viewer_phid, array()); + } + + public function willApplyRules( + PhabricatorUser $viewer, + array $values, + array $objects) { + + $values = array_unique(array_filter(array_mergev($values))); + if (!$values) { + return; + } + + $roles = id(new PhabricatorRoleQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withMemberPHIDs(array($viewer->getPHID())) + ->withPHIDs($values) + ->execute(); + foreach ($roles as $role) { + $this->memberships[$viewer->getPHID()][$role->getPHID()] = true; + } + } + + public function getValueControlType() { + return self::CONTROL_TYPE_TOKENIZER; + } + + public function getValueControlTemplate() { + $datasource = id(new PhabricatorRoleDatasource()) + ->setParameters( + array( + 'policy' => 1, + )); + + return $this->getDatasourceTemplate($datasource); + } + + public function getValueForStorage($value) { + PhutilTypeSpec::newFromString('list')->check($value); + return array_values($value); + } + + public function getValueForDisplay(PhabricatorUser $viewer, $value) { + $handles = id(new PhabricatorHandleQuery()) + ->setViewer($viewer) + ->withPHIDs($value) + ->execute(); + + return mpull($handles, 'getFullName', 'getPHID'); + } + + public function ruleHasEffect($value) { + return (bool)$value; + } + +} diff --git a/src/applications/roles/policyrule/PhabricatorRolesPolicyRule.php b/src/applications/roles/policyrule/PhabricatorRolesPolicyRule.php new file mode 100644 index 0000000000..b34d5a6727 --- /dev/null +++ b/src/applications/roles/policyrule/PhabricatorRolesPolicyRule.php @@ -0,0 +1,29 @@ +getMemberships($viewer->getPHID()); + foreach ($value as $role_phid) { + if (isset($memberships[$role_phid])) { + return true; + } + } + + return false; + } + + public function getRuleOrder() { + return 200; + } + +} diff --git a/src/applications/roles/query/PhabricatorRoleColumnPositionQuery.php b/src/applications/roles/query/PhabricatorRoleColumnPositionQuery.php new file mode 100644 index 0000000000..3c0584d7da --- /dev/null +++ b/src/applications/roles/query/PhabricatorRoleColumnPositionQuery.php @@ -0,0 +1,77 @@ +ids = $ids; + return $this; + } + + public function withBoardPHIDs(array $board_phids) { + $this->boardPHIDs = $board_phids; + return $this; + } + + public function withObjectPHIDs(array $object_phids) { + $this->objectPHIDs = $object_phids; + return $this; + } + + public function withColumnPHIDs(array $column_phids) { + $this->columnPHIDs = $column_phids; + return $this; + } + + public function newResultObject() { + return new PhabricatorRoleColumnPosition(); + } + + protected function loadPage() { + return $this->loadStandardPage($this->newResultObject()); + } + + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { + $where = array(); + + if ($this->ids !== null) { + $where[] = qsprintf( + $conn, + 'id IN (%Ld)', + $this->ids); + } + + if ($this->boardPHIDs !== null) { + $where[] = qsprintf( + $conn, + 'boardPHID IN (%Ls)', + $this->boardPHIDs); + } + + if ($this->objectPHIDs !== null) { + $where[] = qsprintf( + $conn, + 'objectPHID IN (%Ls)', + $this->objectPHIDs); + } + + if ($this->columnPHIDs !== null) { + $where[] = qsprintf( + $conn, + 'columnPHID IN (%Ls)', + $this->columnPHIDs); + } + + return $where; + } + + public function getQueryApplicationClass() { + return 'PhabricatorRoleApplication'; + } + +} diff --git a/src/applications/roles/query/PhabricatorRoleColumnQuery.php b/src/applications/roles/query/PhabricatorRoleColumnQuery.php new file mode 100644 index 0000000000..1c2e5ce654 --- /dev/null +++ b/src/applications/roles/query/PhabricatorRoleColumnQuery.php @@ -0,0 +1,235 @@ +ids = $ids; + return $this; + } + + public function withPHIDs(array $phids) { + $this->phids = $phids; + return $this; + } + + public function withRolePHIDs(array $role_phids) { + $this->rolePHIDs = $role_phids; + return $this; + } + + public function withProxyPHIDs(array $proxy_phids) { + $this->proxyPHIDs = $proxy_phids; + return $this; + } + + public function withStatuses(array $status) { + $this->statuses = $status; + return $this; + } + + public function withIsProxyColumn($is_proxy) { + $this->isProxyColumn = $is_proxy; + return $this; + } + + public function withTriggerPHIDs(array $trigger_phids) { + $this->triggerPHIDs = $trigger_phids; + return $this; + } + + public function needTriggers($need_triggers) { + $this->needTriggers = true; + return $this; + } + + public function newResultObject() { + return new PhabricatorRoleColumn(); + } + + protected function loadPage() { + return $this->loadStandardPage($this->newResultObject()); + } + + protected function willFilterPage(array $page) { + $roles = array(); + + $role_phids = array_filter(mpull($page, 'getRolePHID')); + if ($role_phids) { + $roles = id(new PhabricatorRoleQuery()) + ->setParentQuery($this) + ->setViewer($this->getViewer()) + ->withPHIDs($role_phids) + ->execute(); + $roles = mpull($roles, null, 'getPHID'); + } + + foreach ($page as $key => $column) { + $phid = $column->getRolePHID(); + $role = idx($roles, $phid); + if (!$role) { + $this->didRejectResult($page[$key]); + unset($page[$key]); + continue; + } + $column->attachRole($role); + } + + $proxy_phids = array_filter(mpull($page, 'getRolePHID')); + + return $page; + } + + protected function didFilterPage(array $page) { + $proxy_phids = array(); + foreach ($page as $column) { + $proxy_phid = $column->getProxyPHID(); + if ($proxy_phid !== null) { + $proxy_phids[$proxy_phid] = $proxy_phid; + } + } + + if ($proxy_phids) { + $proxies = id(new PhabricatorObjectQuery()) + ->setParentQuery($this) + ->setViewer($this->getViewer()) + ->withPHIDs($proxy_phids) + ->execute(); + $proxies = mpull($proxies, null, 'getPHID'); + } else { + $proxies = array(); + } + + foreach ($page as $key => $column) { + $proxy_phid = $column->getProxyPHID(); + + if ($proxy_phid !== null) { + $proxy = idx($proxies, $proxy_phid); + + // Only attach valid proxies, so we don't end up getting surprised if + // an install somehow gets junk into their database. + if (!($proxy instanceof PhabricatorRoleColumnProxyInterface)) { + $proxy = null; + } + + if (!$proxy) { + $this->didRejectResult($column); + unset($page[$key]); + continue; + } + } else { + $proxy = null; + } + + $column->attachProxy($proxy); + } + + if ($this->needTriggers) { + $trigger_phids = array(); + foreach ($page as $column) { + if ($column->canHaveTrigger()) { + $trigger_phid = $column->getTriggerPHID(); + if ($trigger_phid) { + $trigger_phids[] = $trigger_phid; + } + } + } + + if ($trigger_phids) { + $triggers = id(new PhabricatorRoleTriggerQuery()) + ->setViewer($this->getViewer()) + ->setParentQuery($this) + ->withPHIDs($trigger_phids) + ->execute(); + $triggers = mpull($triggers, null, 'getPHID'); + } else { + $triggers = array(); + } + + foreach ($page as $column) { + $trigger = null; + + if ($column->canHaveTrigger()) { + $trigger_phid = $column->getTriggerPHID(); + if ($trigger_phid) { + $trigger = idx($triggers, $trigger_phid); + } + } + + $column->attachTrigger($trigger); + } + } + + return $page; + } + + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { + $where = parent::buildWhereClauseParts($conn); + + if ($this->ids !== null) { + $where[] = qsprintf( + $conn, + 'id IN (%Ld)', + $this->ids); + } + + if ($this->phids !== null) { + $where[] = qsprintf( + $conn, + 'phid IN (%Ls)', + $this->phids); + } + + if ($this->rolePHIDs !== null) { + $where[] = qsprintf( + $conn, + 'rolePHID IN (%Ls)', + $this->rolePHIDs); + } + + if ($this->proxyPHIDs !== null) { + $where[] = qsprintf( + $conn, + 'proxyPHID IN (%Ls)', + $this->proxyPHIDs); + } + + if ($this->statuses !== null) { + $where[] = qsprintf( + $conn, + 'status IN (%Ld)', + $this->statuses); + } + + if ($this->triggerPHIDs !== null) { + $where[] = qsprintf( + $conn, + 'triggerPHID IN (%Ls)', + $this->triggerPHIDs); + } + + if ($this->isProxyColumn !== null) { + if ($this->isProxyColumn) { + $where[] = qsprintf($conn, 'proxyPHID IS NOT NULL'); + } else { + $where[] = qsprintf($conn, 'proxyPHID IS NULL'); + } + } + + return $where; + } + + public function getQueryApplicationClass() { + return 'PhabricatorRoleApplication'; + } + +} diff --git a/src/applications/roles/query/PhabricatorRoleColumnSearchEngine.php b/src/applications/roles/query/PhabricatorRoleColumnSearchEngine.php new file mode 100644 index 0000000000..48fc63dd87 --- /dev/null +++ b/src/applications/roles/query/PhabricatorRoleColumnSearchEngine.php @@ -0,0 +1,78 @@ +setLabel(pht('Roles')) + ->setKey('rolePHIDs') + ->setConduitKey('roles') + ->setAliases(array('role', 'roles', 'rolePHID')), + ); + } + + protected function buildQueryFromParameters(array $map) { + $query = $this->newQuery(); + + if ($map['rolePHIDs']) { + $query->withRolePHIDs($map['rolePHIDs']); + } + + return $query; + } + + protected function getURI($path) { + // NOTE: There's no way to query columns in the web UI, at least for + // the moment. + return null; + } + + protected function getBuiltinQueryNames() { + $names = array(); + + $names['all'] = pht('All'); + + return $names; + } + + public function buildSavedQueryFromBuiltin($query_key) { + $query = $this->newSavedQuery(); + $query->setQueryKey($query_key); + + switch ($query_key) { + case 'all': + return $query; + } + + return parent::buildSavedQueryFromBuiltin($query_key); + } + + protected function renderResultList( + array $roles, + PhabricatorSavedQuery $query, + array $handles) { + assert_instances_of($roles, 'PhabricatorRoleColumn'); + $viewer = $this->requireViewer(); + + return null; + } + +} diff --git a/src/applications/roles/query/PhabricatorRoleColumnTransactionQuery.php b/src/applications/roles/query/PhabricatorRoleColumnTransactionQuery.php new file mode 100644 index 0000000000..5e22685172 --- /dev/null +++ b/src/applications/roles/query/PhabricatorRoleColumnTransactionQuery.php @@ -0,0 +1,10 @@ +ids = $ids; + return $this; + } + + public function withPHIDs(array $phids) { + $this->phids = $phids; + return $this; + } + + public function withStatus($status) { + $this->status = $status; + return $this; + } + + public function withStatuses(array $statuses) { + $this->statuses = $statuses; + return $this; + } + + public function withMemberPHIDs(array $member_phids) { + $this->memberPHIDs = $member_phids; + return $this; + } + + public function withWatcherPHIDs(array $watcher_phids) { + $this->watcherPHIDs = $watcher_phids; + return $this; + } + + public function withSlugs(array $slugs) { + $this->slugs = $slugs; + return $this; + } + + public function withNames(array $names) { + $this->names = $names; + return $this; + } + + public function withNamePrefixes(array $prefixes) { + $this->namePrefixes = $prefixes; + return $this; + } + + public function withNameTokens(array $tokens) { + $this->nameTokens = array_values($tokens); + return $this; + } + + public function withIcons(array $icons) { + $this->icons = $icons; + return $this; + } + + public function withColors(array $colors) { + $this->colors = $colors; + return $this; + } + + public function withParentRolePHIDs($parent_phids) { + $this->parentPHIDs = $parent_phids; + return $this; + } + + public function withAncestorRolePHIDs($ancestor_phids) { + $this->ancestorPHIDs = $ancestor_phids; + return $this; + } + + public function withIsMilestone($is_milestone) { + $this->isMilestone = $is_milestone; + return $this; + } + + public function withHasSubroles($has_subroles) { + $this->hasSubroles = $has_subroles; + return $this; + } + + public function withDepthBetween($min, $max) { + $this->minDepth = $min; + $this->maxDepth = $max; + return $this; + } + + public function withMilestoneNumberBetween($min, $max) { + $this->minMilestoneNumber = $min; + $this->maxMilestoneNumber = $max; + return $this; + } + + public function withSubtypes(array $subtypes) { + $this->subtypes = $subtypes; + return $this; + } + + public function needMembers($need_members) { + $this->needMembers = $need_members; + return $this; + } + + public function needAncestorMembers($need_ancestor_members) { + $this->needAncestorMembers = $need_ancestor_members; + return $this; + } + + public function needWatchers($need_watchers) { + $this->needWatchers = $need_watchers; + return $this; + } + + public function needImages($need_images) { + $this->needImages = $need_images; + return $this; + } + + public function needSlugs($need_slugs) { + $this->needSlugs = $need_slugs; + return $this; + } + + public function newResultObject() { + return new PhabricatorRole(); + } + + protected function getDefaultOrderVector() { + return array('name'); + } + + public function getBuiltinOrders() { + return array( + 'name' => array( + 'vector' => array('name'), + 'name' => pht('Name'), + ), + ) + parent::getBuiltinOrders(); + } + + public function getOrderableColumns() { + return parent::getOrderableColumns() + array( + 'name' => array( + 'table' => $this->getPrimaryTableAlias(), + 'column' => 'name', + 'reverse' => true, + 'type' => 'string', + 'unique' => true, + ), + 'milestoneNumber' => array( + 'table' => $this->getPrimaryTableAlias(), + 'column' => 'milestoneNumber', + 'type' => 'int', + ), + 'status' => array( + 'table' => $this->getPrimaryTableAlias(), + 'column' => 'status', + 'type' => 'int', + ), + ); + } + + protected function newPagingMapFromPartialObject($object) { + return array( + 'id' => (int)$object->getID(), + 'name' => $object->getName(), + 'status' => $object->getStatus(), + ); + } + + public function getSlugMap() { + if ($this->slugMap === null) { + throw new PhutilInvalidStateException('execute'); + } + return $this->slugMap; + } + + protected function willExecute() { + $this->slugMap = array(); + $this->slugNormals = array(); + $this->allSlugs = array(); + if ($this->slugs) { + foreach ($this->slugs as $slug) { + if (PhabricatorSlug::isValidProjectSlug($slug)) { + $normal = PhabricatorSlug::normalizeProjectSlug($slug); + $this->slugNormals[$slug] = $normal; + $this->allSlugs[$normal] = $normal; + } + + // NOTE: At least for now, we query for the normalized slugs but also + // for the slugs exactly as entered. This allows older roles with + // slugs that are no longer valid to continue to work. + $this->allSlugs[$slug] = $slug; + } + } + } + + protected function loadPage() { + return $this->loadStandardPage($this->newResultObject()); + } + + protected function willFilterPage(array $roles) { + $ancestor_paths = array(); + foreach ($roles as $role) { + foreach ($role->getAncestorRolePaths() as $path) { + $ancestor_paths[$path] = $path; + } + } + + if ($ancestor_paths) { + $ancestors = id(new PhabricatorRole())->loadAllWhere( + 'rolePath IN (%Ls)', + $ancestor_paths); + } else { + $ancestors = array(); + } + + $roles = $this->linkRoleGraph($roles, $ancestors); + + $viewer_phid = $this->getViewer()->getPHID(); + + $material_type = PhabricatorRoleMaterializedMemberEdgeType::EDGECONST; + $watcher_type = PhabricatorObjectHasWatcherEdgeType::EDGECONST; + + $types = array(); + $types[] = $material_type; + if ($this->needWatchers) { + $types[] = $watcher_type; + } + + $all_graph = $this->getAllReachableAncestors($roles); + + // See T13484. If the graph is damaged (and contains a cycle or an edge + // pointing at a role which has been destroyed), some of the nodes we + // started with may be filtered out by reachability tests. If any of the + // roles we are linking up don't have available ancestors, filter them + // out. + + foreach ($roles as $key => $role) { + $role_phid = $role->getPHID(); + if (!isset($all_graph[$role_phid])) { + $this->didRejectResult($role); + unset($roles[$key]); + continue; + } + } + + if (!$roles) { + return array(); + } + + // NOTE: Although we may not need much information about ancestors, we + // always need to test if the viewer is a member, because we will return + // ancestor roles to the policy filter via ExtendedPolicy calls. If + // we skip populating membership data on a parent, the policy framework + // will think the user is not a member of the parent role. + + $all_sources = array(); + foreach ($all_graph as $role) { + // For milestones, we need parent members. + if ($role->isMilestone()) { + $parent_phid = $role->getParentRolePHID(); + $all_sources[$parent_phid] = $parent_phid; + } + + $phid = $role->getPHID(); + $all_sources[$phid] = $phid; + } + + $edge_query = id(new PhabricatorEdgeQuery()) + ->withSourcePHIDs($all_sources) + ->withEdgeTypes($types); + + $need_all_edges = + $this->needMembers || + $this->needWatchers || + $this->needAncestorMembers; + + // If we only need to know if the viewer is a member, we can restrict + // the query to just their PHID. + $any_edges = true; + if (!$need_all_edges) { + if ($viewer_phid) { + $edge_query->withDestinationPHIDs(array($viewer_phid)); + } else { + // If we don't need members or watchers and don't have a viewer PHID + // (viewer is logged-out or omnipotent), they'll never be a member + // so we don't need to issue this query at all. + $any_edges = false; + } + } + + if ($any_edges) { + $edge_query->execute(); + } + + $membership_roles = array(); + foreach ($all_graph as $role) { + $role_phid = $role->getPHID(); + + if ($role->isMilestone()) { + $source_phids = array($role->getParentRolePHID()); + } else { + $source_phids = array($role_phid); + } + + if ($any_edges) { + $member_phids = $edge_query->getDestinationPHIDs( + $source_phids, + array($material_type)); + } else { + $member_phids = array(); + } + + if (in_array($viewer_phid, $member_phids)) { + $membership_roles[$role_phid] = $role; + } + + if ($this->needMembers || $this->needAncestorMembers) { + $role->attachMemberPHIDs($member_phids); + } + + if ($this->needWatchers) { + $watcher_phids = $edge_query->getDestinationPHIDs( + array($role_phid), + array($watcher_type)); + $role->attachWatcherPHIDs($watcher_phids); + $role->setIsUserWatcher( + $viewer_phid, + in_array($viewer_phid, $watcher_phids)); + } + } + + // If we loaded ancestor members, we've already populated membership + // lists above, so we can skip this step. + if (!$this->needAncestorMembers) { + $member_graph = $this->getAllReachableAncestors($membership_roles); + + foreach ($all_graph as $phid => $role) { + $is_member = isset($member_graph[$phid]); + $role->setIsUserMember($viewer_phid, $is_member); + } + } + + return $roles; + } + + protected function didFilterPage(array $roles) { + $viewer = $this->getViewer(); + + if ($this->needImages) { + $need_images = $roles; + + // First, try to load custom profile images for any roles with custom + // images. + $file_phids = array(); + foreach ($need_images as $key => $role) { + $image_phid = $role->getProfileImagePHID(); + if ($image_phid) { + $file_phids[$key] = $image_phid; + } + } + + if ($file_phids) { + $files = id(new PhabricatorFileQuery()) + ->setParentQuery($this) + ->setViewer($viewer) + ->withPHIDs($file_phids) + ->execute(); + $files = mpull($files, null, 'getPHID'); + + foreach ($file_phids as $key => $image_phid) { + $file = idx($files, $image_phid); + if (!$file) { + continue; + } + + $need_images[$key]->attachProfileImageFile($file); + unset($need_images[$key]); + } + } + + // For roles with default images, or roles where the custom image + // failed to load, load a builtin image. + if ($need_images) { + $builtin_map = array(); + $builtins = array(); + foreach ($need_images as $key => $role) { + $icon = $role->getIcon(); + + $builtin_name = PhabricatorRoleIconSet::getIconImage($icon); + $builtin_name = 'roles/'.$builtin_name; + + $builtin = id(new PhabricatorFilesOnDiskBuiltinFile()) + ->setName($builtin_name); + + $builtin_key = $builtin->getBuiltinFileKey(); + + $builtins[] = $builtin; + $builtin_map[$key] = $builtin_key; + } + + $builtin_files = PhabricatorFile::loadBuiltins( + $viewer, + $builtins); + + foreach ($need_images as $key => $role) { + $builtin_key = $builtin_map[$key]; + $builtin_file = $builtin_files[$builtin_key]; + $role->attachProfileImageFile($builtin_file); + } + } + } + + $this->loadSlugs($roles); + + return $roles; + } + + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { + $where = parent::buildWhereClauseParts($conn); + + if ($this->status != self::STATUS_ANY) { + switch ($this->status) { + case self::STATUS_OPEN: + case self::STATUS_ACTIVE: + $filter = array( + PhabricatorRoleStatus::STATUS_ACTIVE, + ); + break; + case self::STATUS_CLOSED: + case self::STATUS_ARCHIVED: + $filter = array( + PhabricatorRoleStatus::STATUS_ARCHIVED, + ); + break; + default: + throw new Exception( + pht( + "Unknown role status '%s'!", + $this->status)); + } + $where[] = qsprintf( + $conn, + 'role.status IN (%Ld)', + $filter); + } + + if ($this->statuses !== null) { + $where[] = qsprintf( + $conn, + 'role.status IN (%Ls)', + $this->statuses); + } + + if ($this->ids !== null) { + $where[] = qsprintf( + $conn, + 'role.id IN (%Ld)', + $this->ids); + } + + if ($this->phids !== null) { + $where[] = qsprintf( + $conn, + 'role.phid IN (%Ls)', + $this->phids); + } + + if ($this->memberPHIDs !== null) { + $where[] = qsprintf( + $conn, + 'e.dst IN (%Ls)', + $this->memberPHIDs); + } + + if ($this->watcherPHIDs !== null) { + $where[] = qsprintf( + $conn, + 'w.dst IN (%Ls)', + $this->watcherPHIDs); + } + + if ($this->slugs !== null) { + $where[] = qsprintf( + $conn, + 'slug.slug IN (%Ls)', + $this->allSlugs); + } + + if ($this->names !== null) { + $where[] = qsprintf( + $conn, + 'role.name IN (%Ls)', + $this->names); + } + + if ($this->namePrefixes) { + $parts = array(); + foreach ($this->namePrefixes as $name_prefix) { + $parts[] = qsprintf( + $conn, + 'role.name LIKE %>', + $name_prefix); + } + $where[] = qsprintf($conn, '%LO', $parts); + } + + if ($this->icons !== null) { + $where[] = qsprintf( + $conn, + 'role.icon IN (%Ls)', + $this->icons); + } + + if ($this->colors !== null) { + $where[] = qsprintf( + $conn, + 'role.color IN (%Ls)', + $this->colors); + } + + if ($this->parentPHIDs !== null) { + $where[] = qsprintf( + $conn, + 'role.parentRolePHID IN (%Ls)', + $this->parentPHIDs); + } + + if ($this->ancestorPHIDs !== null) { + $ancestor_paths = queryfx_all( + $conn, + 'SELECT rolePath, roleDepth FROM %T WHERE phid IN (%Ls)', + id(new PhabricatorRole())->getTableName(), + $this->ancestorPHIDs); + if (!$ancestor_paths) { + throw new PhabricatorEmptyQueryException(); + } + + $sql = array(); + foreach ($ancestor_paths as $ancestor_path) { + $sql[] = qsprintf( + $conn, + '(role.rolePath LIKE %> AND role.roleDepth > %d)', + $ancestor_path['rolePath'], + $ancestor_path['roleDepth']); + } + + $where[] = qsprintf($conn, '%LO', $sql); + + $where[] = qsprintf( + $conn, + 'role.parentRolePHID IS NOT NULL'); + } + + if ($this->isMilestone !== null) { + if ($this->isMilestone) { + $where[] = qsprintf( + $conn, + 'role.milestoneNumber IS NOT NULL'); + } else { + $where[] = qsprintf( + $conn, + 'role.milestoneNumber IS NULL'); + } + } + + + if ($this->hasSubroles !== null) { + $where[] = qsprintf( + $conn, + 'role.hasSubroles = %d', + (int)$this->hasSubroles); + } + + if ($this->minDepth !== null) { + $where[] = qsprintf( + $conn, + 'role.roleDepth >= %d', + $this->minDepth); + } + + if ($this->maxDepth !== null) { + $where[] = qsprintf( + $conn, + 'role.roleDepth <= %d', + $this->maxDepth); + } + + if ($this->minMilestoneNumber !== null) { + $where[] = qsprintf( + $conn, + 'role.milestoneNumber >= %d', + $this->minMilestoneNumber); + } + + if ($this->maxMilestoneNumber !== null) { + $where[] = qsprintf( + $conn, + 'role.milestoneNumber <= %d', + $this->maxMilestoneNumber); + } + + if ($this->subtypes !== null) { + $where[] = qsprintf( + $conn, + 'role.subtype IN (%Ls)', + $this->subtypes); + } + + return $where; + } + + protected function shouldGroupQueryResultRows() { + if ($this->memberPHIDs || $this->watcherPHIDs || $this->nameTokens) { + return true; + } + + if ($this->slugs) { + return true; + } + + return parent::shouldGroupQueryResultRows(); + } + + protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) { + $joins = parent::buildJoinClauseParts($conn); + + if ($this->memberPHIDs !== null) { + $joins[] = qsprintf( + $conn, + 'JOIN %T e ON e.src = role.phid AND e.type = %d', + PhabricatorEdgeConfig::TABLE_NAME_EDGE, + PhabricatorRoleMaterializedMemberEdgeType::EDGECONST); + } + + if ($this->watcherPHIDs !== null) { + $joins[] = qsprintf( + $conn, + 'JOIN %T w ON w.src = role.phid AND w.type = %d', + PhabricatorEdgeConfig::TABLE_NAME_EDGE, + PhabricatorObjectHasWatcherEdgeType::EDGECONST); + } + + if ($this->slugs !== null) { + $joins[] = qsprintf( + $conn, + 'JOIN %T slug on slug.rolePHID = role.phid', + id(new PhabricatorRoleSlug())->getTableName()); + } + + if ($this->nameTokens !== null) { + $name_tokens = $this->getNameTokensForQuery($this->nameTokens); + foreach ($name_tokens as $key => $token) { + $token_table = 'token_'.$key; + $joins[] = qsprintf( + $conn, + 'JOIN %T %T ON %T.roleID = role.id AND %T.token LIKE %>', + PhabricatorRole::TABLE_DATASOURCE_TOKEN, + $token_table, + $token_table, + $token_table, + $token); + } + } + + return $joins; + } + + public function getQueryApplicationClass() { + return 'PhabricatorRoleApplication'; + } + + protected function getPrimaryTableAlias() { + return 'role'; + } + + private function linkRoleGraph(array $roles, array $ancestors) { + $ancestor_map = mpull($ancestors, null, 'getPHID'); + $roles_map = mpull($roles, null, 'getPHID'); + + $all_map = $roles_map + $ancestor_map; + + $done = array(); + foreach ($roles as $key => $role) { + $seen = array($role->getPHID() => true); + + if (!$this->linkRole($role, $all_map, $done, $seen)) { + $this->didRejectResult($role); + unset($roles[$key]); + continue; + } + + foreach ($role->getAncestorRoles() as $ancestor) { + $seen[$ancestor->getPHID()] = true; + } + } + + return $roles; + } + + private function linkRole($role, array $all, array $done, array $seen) { + $parent_phid = $role->getParentRolePHID(); + + // This role has no parent, so just attach `null` and return. + if (!$parent_phid) { + $role->attachParentRole(null); + return true; + } + + // This role has a parent, but it failed to load. + if (empty($all[$parent_phid])) { + return false; + } + + // Test for graph cycles. If we encounter one, we're going to hide the + // entire cycle since we can't meaningfully resolve it. + if (isset($seen[$parent_phid])) { + return false; + } + + $seen[$parent_phid] = true; + + $parent = $all[$parent_phid]; + $role->attachParentRole($parent); + + if (!empty($done[$parent_phid])) { + return true; + } + + return $this->linkRole($parent, $all, $done, $seen); + } + + private function getAllReachableAncestors(array $roles) { + $ancestors = array(); + + $seen = mpull($roles, null, 'getPHID'); + + $stack = $roles; + while ($stack) { + $role = array_pop($stack); + + $phid = $role->getPHID(); + $ancestors[$phid] = $role; + + $parent_phid = $role->getParentRolePHID(); + if (!$parent_phid) { + continue; + } + + if (isset($seen[$parent_phid])) { + continue; + } + + $seen[$parent_phid] = true; + $stack[] = $role->getParentRole(); + } + + return $ancestors; + } + + private function loadSlugs(array $roles) { + // Build a map from primary slugs to roles. + $primary_map = array(); + foreach ($roles as $role) { + $primary_slug = $role->getPrimarySlug(); + if ($primary_slug === null) { + continue; + } + + $primary_map[$primary_slug] = $role; + } + + // Link up all of the queried slugs which correspond to primary + // slugs. If we can link up everything from this (no slugs were queried, + // or only primary slugs were queried) we don't need to load anything + // else. + $unknown = $this->slugNormals; + foreach ($unknown as $input => $normal) { + if (isset($primary_map[$input])) { + $match = $input; + } else if (isset($primary_map[$normal])) { + $match = $normal; + } else { + continue; + } + + $this->slugMap[$input] = array( + 'slug' => $match, + 'rolePHID' => $primary_map[$match]->getPHID(), + ); + + unset($unknown[$input]); + } + + // If we need slugs, we have to load everything. + // If we still have some queried slugs which we haven't mapped, we only + // need to look for them. + // If we've mapped everything, we don't have to do any work. + $role_phids = mpull($roles, 'getPHID'); + if ($this->needSlugs) { + $slugs = id(new PhabricatorRoleSlug())->loadAllWhere( + 'rolePHID IN (%Ls)', + $role_phids); + } else if ($unknown) { + $slugs = id(new PhabricatorRoleSlug())->loadAllWhere( + 'rolePHID IN (%Ls) AND slug IN (%Ls)', + $role_phids, + $unknown); + } else { + $slugs = array(); + } + + // Link up any slugs we were not able to link up earlier. + $extra_map = mpull($slugs, 'getRolePHID', 'getSlug'); + foreach ($unknown as $input => $normal) { + if (isset($extra_map[$input])) { + $match = $input; + } else if (isset($extra_map[$normal])) { + $match = $normal; + } else { + continue; + } + + $this->slugMap[$input] = array( + 'slug' => $match, + 'rolePHID' => $extra_map[$match], + ); + + unset($unknown[$input]); + } + + if ($this->needSlugs) { + $slug_groups = mgroup($slugs, 'getRolePHID'); + foreach ($roles as $role) { + $role_slugs = idx($slug_groups, $role->getPHID(), array()); + $role->attachSlugs($role_slugs); + } + } + } + + private function getNameTokensForQuery(array $tokens) { + // When querying for roles by name, only actually search for the five + // longest tokens. MySQL can get grumpy with a large number of JOINs + // with LIKEs and queries for more than 5 tokens are essentially never + // legitimate searches for roles, but users copy/pasting nonsense. + // See also PHI47. + + $length_map = array(); + foreach ($tokens as $token) { + $length_map[$token] = strlen($token); + } + arsort($length_map); + + $length_map = array_slice($length_map, 0, 5, true); + + return array_keys($length_map); + } + +} diff --git a/src/applications/roles/query/PhabricatorRoleSearchEngine.php b/src/applications/roles/query/PhabricatorRoleSearchEngine.php new file mode 100644 index 0000000000..37746bf69c --- /dev/null +++ b/src/applications/roles/query/PhabricatorRoleSearchEngine.php @@ -0,0 +1,359 @@ +needImages(true) + ->needMembers(true) + ->needWatchers(true); + } + + protected function buildCustomSearchFields() { + $subtype_map = id(new PhabricatorRole())->newEditEngineSubtypeMap(); + $hide_subtypes = ($subtype_map->getCount() == 1); + + return array( + id(new PhabricatorSearchTextField()) + ->setLabel(pht('Name')) + ->setKey('name') + ->setDescription( + pht( + '(Deprecated.) Search for roles with a given name or '. + 'hashtag using tokenizer/datasource query matching rules. This '. + 'is deprecated in favor of the more powerful "query" '. + 'constraint.')), + id(new PhabricatorSearchStringListField()) + ->setLabel(pht('Slugs')) + ->setIsHidden(true) + ->setKey('slugs') + ->setDescription( + pht( + 'Search for roles with particular slugs. (Slugs are the same '. + 'as role hashtags.)')), + id(new PhabricatorUsersSearchField()) + ->setLabel(pht('Members')) + ->setKey('memberPHIDs') + ->setConduitKey('members') + ->setAliases(array('member', 'members')), + id(new PhabricatorUsersSearchField()) + ->setLabel(pht('Watchers')) + ->setKey('watcherPHIDs') + ->setConduitKey('watchers') + ->setAliases(array('watcher', 'watchers')), + id(new PhabricatorSearchSelectField()) + ->setLabel(pht('Status')) + ->setKey('status') + ->setOptions($this->getStatusOptions()), + id(new PhabricatorSearchThreeStateField()) + ->setLabel(pht('Milestones')) + ->setKey('isMilestone') + ->setOptions( + pht('(Show All)'), + pht('Show Only Milestones'), + pht('Hide Milestones')) + ->setDescription( + pht( + 'Pass true to find only milestones, or false to omit '. + 'milestones.')), + id(new PhabricatorSearchThreeStateField()) + ->setLabel(pht('Root Roles')) + ->setKey('isRoot') + ->setOptions( + pht('(Show All)'), + pht('Show Only Root Roles'), + pht('Hide Root Roles')) + ->setDescription( + pht( + 'Pass true to find only root roles, or false to omit '. + 'root roles.')), + id(new PhabricatorSearchIntField()) + ->setLabel(pht('Minimum Depth')) + ->setKey('minDepth') + ->setIsHidden(true) + ->setDescription( + pht( + 'Find roles with a given minimum depth. Root roles '. + 'have depth 0, their immediate children have depth 1, and '. + 'so on.')), + id(new PhabricatorSearchIntField()) + ->setLabel(pht('Maximum Depth')) + ->setKey('maxDepth') + ->setIsHidden(true) + ->setDescription( + pht( + 'Find roles with a given maximum depth. Root roles '. + 'have depth 0, their immediate children have depth 1, and '. + 'so on.')), + id(new PhabricatorSearchDatasourceField()) + ->setLabel(pht('Subtypes')) + ->setKey('subtypes') + ->setAliases(array('subtype')) + ->setDescription( + pht('Search for roles with given subtypes.')) + ->setDatasource(new PhabricatorRoleSubtypeDatasource()) + ->setIsHidden($hide_subtypes), + id(new PhabricatorSearchCheckboxesField()) + ->setLabel(pht('Icons')) + ->setKey('icons') + ->setOptions($this->getIconOptions()), + id(new PhabricatorSearchCheckboxesField()) + ->setLabel(pht('Colors')) + ->setKey('colors') + ->setOptions($this->getColorOptions()), + id(new PhabricatorPHIDsSearchField()) + ->setLabel(pht('Parent Roles')) + ->setKey('parentPHIDs') + ->setConduitKey('parents') + ->setAliases(array('parent', 'parents', 'parentPHID')) + ->setDescription(pht('Find direct subroles of specified parents.')), + id(new PhabricatorPHIDsSearchField()) + ->setLabel(pht('Ancestor Roles')) + ->setKey('ancestorPHIDs') + ->setConduitKey('ancestors') + ->setAliases(array('ancestor', 'ancestors', 'ancestorPHID')) + ->setDescription( + pht('Find all subroles beneath specified ancestors.')), + ); + } + + + protected function buildQueryFromParameters(array $map) { + $query = $this->newQuery(); + + if ($map['name'] !== null && $map['name'] !== '') { + $tokens = PhabricatorTypeaheadDatasource::tokenizeString($map['name']); + $query->withNameTokens($tokens); + } + + if ($map['slugs']) { + $query->withSlugs($map['slugs']); + } + + if ($map['memberPHIDs']) { + $query->withMemberPHIDs($map['memberPHIDs']); + } + + if ($map['watcherPHIDs']) { + $query->withWatcherPHIDs($map['watcherPHIDs']); + } + + if ($map['status']) { + $status = idx($this->getStatusValues(), $map['status']); + if ($status) { + $query->withStatus($status); + } + } + + if ($map['icons']) { + $query->withIcons($map['icons']); + } + + if ($map['colors']) { + $query->withColors($map['colors']); + } + + if ($map['isMilestone'] !== null) { + $query->withIsMilestone($map['isMilestone']); + } + + $min_depth = $map['minDepth']; + $max_depth = $map['maxDepth']; + + if ($min_depth !== null || $max_depth !== null) { + if ($min_depth !== null && $max_depth !== null) { + if ($min_depth > $max_depth) { + throw new Exception( + pht( + 'Search constraint "minDepth" must be no larger than '. + 'search constraint "maxDepth".')); + } + } + } + + if ($map['isRoot'] !== null) { + if ($map['isRoot']) { + if ($max_depth === null) { + $max_depth = 0; + } else { + $max_depth = min(0, $max_depth); + } + + $query->withDepthBetween(null, 0); + } else { + if ($min_depth === null) { + $min_depth = 1; + } else { + $min_depth = max($min_depth, 1); + } + } + } + + if ($min_depth !== null || $max_depth !== null) { + $query->withDepthBetween($min_depth, $max_depth); + } + + if ($map['parentPHIDs']) { + $query->withParentRolePHIDs($map['parentPHIDs']); + } + + if ($map['ancestorPHIDs']) { + $query->withAncestorRolePHIDs($map['ancestorPHIDs']); + } + + if ($map['subtypes']) { + $query->withSubtypes($map['subtypes']); + } + + return $query; + } + + protected function getURI($path) { + return '/role/'.$path; + } + + protected function getBuiltinQueryNames() { + $names = array(); + + if ($this->requireViewer()->isLoggedIn()) { + $names['joined'] = pht('Joined'); + } + + if ($this->requireViewer()->isLoggedIn()) { + $names['watching'] = pht('Watching'); + } + + $names['active'] = pht('Active'); + $names['all'] = pht('All'); + + return $names; + } + + public function buildSavedQueryFromBuiltin($query_key) { + $query = $this->newSavedQuery(); + $query->setQueryKey($query_key); + + $viewer_phid = $this->requireViewer()->getPHID(); + + // By default, do not show milestones in the list view. + $query->setParameter('isMilestone', false); + + switch ($query_key) { + case 'all': + return $query; + case 'active': + return $query + ->setParameter('status', 'active'); + case 'joined': + return $query + ->setParameter('memberPHIDs', array($viewer_phid)) + ->setParameter('status', 'active'); + case 'watching': + return $query + ->setParameter('watcherPHIDs', array($viewer_phid)) + ->setParameter('status', 'active'); + } + + return parent::buildSavedQueryFromBuiltin($query_key); + } + + private function getStatusOptions() { + return array( + 'active' => pht('Show Only Active Roles'), + 'archived' => pht('Show Only Archived Roles'), + 'all' => pht('Show All Roles'), + ); + } + + private function getStatusValues() { + return array( + 'active' => PhabricatorRoleQuery::STATUS_ACTIVE, + 'archived' => PhabricatorRoleQuery::STATUS_ARCHIVED, + 'all' => PhabricatorRoleQuery::STATUS_ANY, + ); + } + + private function getIconOptions() { + $options = array(); + + $set = new PhabricatorRoleIconSet(); + foreach ($set->getIcons() as $icon) { + if ($icon->getIsDisabled()) { + continue; + } + + $options[$icon->getKey()] = array( + id(new PHUIIconView()) + ->setIcon($icon->getIcon()), + ' ', + $icon->getLabel(), + ); + } + + return $options; + } + + private function getColorOptions() { + $options = array(); + + foreach (PhabricatorRoleIconSet::getColorMap() as $color => $name) { + $options[$color] = array( + id(new PHUITagView()) + ->setType(PHUITagView::TYPE_SHADE) + ->setColor($color) + ->setName($name), + ); + } + + return $options; + } + + protected function renderResultList( + array $roles, + PhabricatorSavedQuery $query, + array $handles) { + assert_instances_of($roles, 'PhabricatorRole'); + $viewer = $this->requireViewer(); + + $list = id(new PhabricatorRoleListView()) + ->setUser($viewer) + ->setRoles($roles) + ->setShowWatching(true) + ->setShowMember(true) + ->renderList(); + + return id(new PhabricatorApplicationSearchResultView()) + ->setObjectList($list) + ->setNoDataString(pht('No roles found.')); + } + + protected function getNewUserBody() { + $create_button = id(new PHUIButtonView()) + ->setTag('a') + ->setText(pht('Create a Role')) + ->setHref('/role/edit/') + ->setColor(PHUIButtonView::GREEN); + + $icon = $this->getApplication()->getIcon(); + $app_name = $this->getApplication()->getName(); + $view = id(new PHUIBigInfoView()) + ->setIcon($icon) + ->setTitle(pht('Welcome to %s', $app_name)) + ->setDescription( + pht('Roles are flexible storage containers used as '. + 'tags, teams, roles, or anything you need to group.')) + ->addAction($create_button); + + return $view; + } + +} diff --git a/src/applications/roles/query/PhabricatorRoleTransactionQuery.php b/src/applications/roles/query/PhabricatorRoleTransactionQuery.php new file mode 100644 index 0000000000..49335eb6ee --- /dev/null +++ b/src/applications/roles/query/PhabricatorRoleTransactionQuery.php @@ -0,0 +1,10 @@ +ids = $ids; + return $this; + } + + public function withPHIDs(array $phids) { + $this->phids = $phids; + return $this; + } + + public function needUsage($need_usage) { + $this->needUsage = $need_usage; + return $this; + } + + public function withActiveColumnCountBetween($min, $max) { + $this->activeColumnMin = $min; + $this->activeColumnMax = $max; + return $this; + } + + public function newResultObject() { + return new PhabricatorRoleTrigger(); + } + + protected function loadPage() { + return $this->loadStandardPage($this->newResultObject()); + } + + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { + $where = parent::buildWhereClauseParts($conn); + + if ($this->ids !== null) { + $where[] = qsprintf( + $conn, + 'trigger.id IN (%Ld)', + $this->ids); + } + + if ($this->phids !== null) { + $where[] = qsprintf( + $conn, + 'trigger.phid IN (%Ls)', + $this->phids); + } + + if ($this->activeColumnMin !== null) { + $where[] = qsprintf( + $conn, + 'trigger_usage.activeColumnCount >= %d', + $this->activeColumnMin); + } + + if ($this->activeColumnMax !== null) { + $where[] = qsprintf( + $conn, + 'trigger_usage.activeColumnCount <= %d', + $this->activeColumnMax); + } + + return $where; + } + + protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) { + $joins = parent::buildJoinClauseParts($conn); + + if ($this->shouldJoinUsageTable()) { + $joins[] = qsprintf( + $conn, + 'JOIN %R trigger_usage ON trigger.phid = trigger_usage.triggerPHID', + new PhabricatorRoleTriggerUsage()); + } + + return $joins; + } + + private function shouldJoinUsageTable() { + if ($this->activeColumnMin !== null) { + return true; + } + + if ($this->activeColumnMax !== null) { + return true; + } + + return false; + } + + protected function didFilterPage(array $triggers) { + if ($this->needUsage) { + $usage_map = id(new PhabricatorRoleTriggerUsage())->loadAllWhere( + 'triggerPHID IN (%Ls)', + mpull($triggers, 'getPHID')); + $usage_map = mpull($usage_map, null, 'getTriggerPHID'); + + foreach ($triggers as $trigger) { + $trigger_phid = $trigger->getPHID(); + + $usage = idx($usage_map, $trigger_phid); + if (!$usage) { + $usage = id(new PhabricatorRoleTriggerUsage()) + ->setTriggerPHID($trigger_phid) + ->setExamplePHID(null) + ->setColumnCount(0) + ->setActiveColumnCount(0); + } + + $trigger->attachUsage($usage); + } + } + + return $triggers; + } + + public function getQueryApplicationClass() { + return 'PhabricatorRoleApplication'; + } + + protected function getPrimaryTableAlias() { + return 'trigger'; + } + +} diff --git a/src/applications/roles/query/PhabricatorRoleTriggerSearchEngine.php b/src/applications/roles/query/PhabricatorRoleTriggerSearchEngine.php new file mode 100644 index 0000000000..0cc009f3eb --- /dev/null +++ b/src/applications/roles/query/PhabricatorRoleTriggerSearchEngine.php @@ -0,0 +1,155 @@ +needUsage(true); + } + + protected function buildCustomSearchFields() { + return array( + id(new PhabricatorSearchThreeStateField()) + ->setLabel(pht('Active')) + ->setKey('isActive') + ->setOptions( + pht('(Show All)'), + pht('Show Only Active Triggers'), + pht('Show Only Inactive Triggers')), + ); + } + + protected function buildQueryFromParameters(array $map) { + $query = $this->newQuery(); + + if ($map['isActive'] !== null) { + if ($map['isActive']) { + $query->withActiveColumnCountBetween(1, null); + } else { + $query->withActiveColumnCountBetween(null, 0); + } + } + + return $query; + } + + protected function getURI($path) { + return '/role/trigger/'.$path; + } + + protected function getBuiltinQueryNames() { + $names = array(); + + $names['active'] = pht('Active Triggers'); + $names['all'] = pht('All Triggers'); + + return $names; + } + + public function buildSavedQueryFromBuiltin($query_key) { + $query = $this->newSavedQuery(); + $query->setQueryKey($query_key); + + switch ($query_key) { + case 'active': + return $query->setParameter('isActive', true); + case 'all': + return $query; + } + + return parent::buildSavedQueryFromBuiltin($query_key); + } + + protected function renderResultList( + array $triggers, + PhabricatorSavedQuery $query, + array $handles) { + assert_instances_of($triggers, 'PhabricatorRoleTrigger'); + $viewer = $this->requireViewer(); + + $example_phids = array(); + foreach ($triggers as $trigger) { + $example_phid = $trigger->getUsage()->getExamplePHID(); + if ($example_phid) { + $example_phids[] = $example_phid; + } + } + + $handles = $viewer->loadHandles($example_phids); + + $list = id(new PHUIObjectItemListView()) + ->setViewer($viewer); + foreach ($triggers as $trigger) { + $usage = $trigger->getUsage(); + + $column_handle = null; + $have_column = false; + $example_phid = $usage->getExamplePHID(); + if ($example_phid) { + $column_handle = $handles[$example_phid]; + if ($column_handle->isComplete()) { + if (!$column_handle->getPolicyFiltered()) { + $have_column = true; + } + } + } + + $column_count = $usage->getColumnCount(); + $active_count = $usage->getActiveColumnCount(); + + if ($have_column) { + if ($active_count > 1) { + $usage_description = pht( + 'Used on %s and %s other active column(s).', + $column_handle->renderLink(), + new PhutilNumber($active_count - 1)); + } else if ($column_count > 1) { + $usage_description = pht( + 'Used on %s and %s other column(s).', + $column_handle->renderLink(), + new PhutilNumber($column_count - 1)); + } else { + $usage_description = pht( + 'Used on %s.', + $column_handle->renderLink()); + } + } else { + if ($active_count) { + $usage_description = pht( + 'Used on %s active column(s).', + new PhutilNumber($active_count)); + } else if ($column_count) { + $usage_description = pht( + 'Used on %s column(s).', + new PhutilNumber($column_count)); + } else { + $usage_description = pht( + 'Unused trigger.'); + } + } + + $item = id(new PHUIObjectItemView()) + ->setObjectName($trigger->getObjectName()) + ->setHeader($trigger->getDisplayName()) + ->setHref($trigger->getURI()) + ->addAttribute($usage_description) + ->setDisabled(!$active_count); + + $list->addItem($item); + } + + return id(new PhabricatorApplicationSearchResultView()) + ->setObjectList($list) + ->setNoDataString(pht('No triggers found.')); + } + +} diff --git a/src/applications/roles/query/PhabricatorRoleTriggerTransactionQuery.php b/src/applications/roles/query/PhabricatorRoleTriggerTransactionQuery.php new file mode 100644 index 0000000000..8465f38c57 --- /dev/null +++ b/src/applications/roles/query/PhabricatorRoleTriggerTransactionQuery.php @@ -0,0 +1,10 @@ +getEngine()->isTextMode()) { + return '#'.$id; + } + + $tag = $handle->renderTag(); + $tag->setPHID($handle->getPHID()); + return $tag; + } + + protected function getObjectIDPattern() { + // NOTE: The latter half of this rule matches monograms with internal + // periods, like `#domain.com`, but does not match monograms with terminal + // periods, because they're probably just punctuation. + + // Broadly, this will not match every possible role monogram, and we + // accept some false negatives -- like `#dot.` -- in order to avoid a bunch + // of false positives on general use of the `#` character. + + // In other contexts, the PhabricatorRoleRolePHIDType pattern is + // controlling and these names should parse correctly. + + // These characters may never appear anywhere in a hashtag. + $never = '\s?!,:;{}#\\(\\)"\'\\*/~'; + + // These characters may not appear at the edge of the string. + $never_edge = '.'; + + return + '[^'.$never_edge.$never.']+'. + '(?:'. + '[^'.$never.']*'. + '[^'.$never_edge.$never.']+'. + ')*'; + } + + protected function loadObjects(array $ids) { + $viewer = $this->getEngine()->getConfig('viewer'); + + // Put the "#" back on the front of these IDs. + $names = array(); + foreach ($ids as $id) { + $names[] = '#'.$id; + } + + // Issue a query by object name. + $query = id(new PhabricatorObjectQuery()) + ->setViewer($viewer) + ->withNames($names); + + $query->execute(); + $roles = $query->getNamedResults(); + + // Slice the "#" off again. + $result = array(); + foreach ($roles as $name => $role) { + $result[substr($name, 1)] = $role; + } + + return $result; + } + +} diff --git a/src/applications/roles/remarkup/__tests__/RoleRemarkupRuleTestCase.php b/src/applications/roles/remarkup/__tests__/RoleRemarkupRuleTestCase.php new file mode 100644 index 0000000000..346805b714 --- /dev/null +++ b/src/applications/roles/remarkup/__tests__/RoleRemarkupRuleTestCase.php @@ -0,0 +1,147 @@ + array( + 'embed' => array(), + 'ref' => array( + array( + 'offset' => 8, + 'id' => 'ducks', + ), + ), + ), + 'We should make a post on #blog.example.com tomorrow.' => array( + 'embed' => array(), + 'ref' => array( + array( + 'offset' => 26, + 'id' => 'blog.example.com', + ), + ), + ), + 'We should make a post on #blog.example.com.' => array( + 'embed' => array(), + 'ref' => array( + array( + 'offset' => 26, + 'id' => 'blog.example.com', + ), + ), + ), + '#123' => array( + 'embed' => array(), + 'ref' => array( + array( + 'offset' => 1, + 'id' => '123', + ), + ), + ), + '#2x4' => array( + 'embed' => array(), + 'ref' => array( + array( + 'offset' => 1, + 'id' => '2x4', + ), + ), + ), + '#security#123' => array( + 'embed' => array(), + 'ref' => array( + array( + 'offset' => 1, + 'id' => 'security', + 'tail' => '123', + ), + ), + ), + + // Don't match a terminal parenthesis. This fixes these constructs in + // natural language. + 'There is some documentation (see #guides).' => array( + 'embed' => array(), + 'ref' => array( + array( + 'offset' => 34, + 'id' => 'guides', + ), + ), + ), + + // Don't match internal parentheses either. This makes the terminal + // parenthesis behavior less arbitrary (otherwise, we match open + // parentheses but not closing parentheses, which is surprising). + '#a(b)c' => array( + 'embed' => array(), + 'ref' => array( + array( + 'offset' => 1, + 'id' => 'a', + ), + ), + ), + + '#s3' => array( + 'embed' => array(), + 'ref' => array( + array( + 'offset' => 1, + 'id' => 's3', + ), + ), + ), + + 'Is this #urgent?' => array( + 'embed' => array(), + 'ref' => array( + array( + 'offset' => 9, + 'id' => 'urgent', + ), + ), + ), + + 'This is "#urgent".' => array( + 'embed' => array(), + 'ref' => array( + array( + 'offset' => 10, + 'id' => 'urgent', + ), + ), + ), + + "This is '#urgent'." => array( + 'embed' => array(), + 'ref' => array( + array( + 'offset' => 10, + 'id' => 'urgent', + ), + ), + ), + + '**#orbital**' => array( + 'embed' => array(), + 'ref' => array( + array( + 'offset' => 3, + 'id' => 'orbital', + ), + ), + ), + + ); + + foreach ($cases as $input => $expect) { + $rule = new RoleRemarkupRule(); + $matches = $rule->extractReferences($input); + $this->assertEqual($expect, $matches, $input); + } + } + +} diff --git a/src/applications/roles/search/PhabricatorRoleFerretEngine.php b/src/applications/roles/search/PhabricatorRoleFerretEngine.php new file mode 100644 index 0000000000..11daadc37c --- /dev/null +++ b/src/applications/roles/search/PhabricatorRoleFerretEngine.php @@ -0,0 +1,18 @@ +getViewer(); + + // Reload the role to get slugs. + $role = id(new PhabricatorRoleQuery()) + ->withIDs(array($role->getID())) + ->setViewer($viewer) + ->needSlugs(true) + ->executeOne(); + + $role->updateDatasourceTokens(); + + $slugs = array(); + foreach ($role->getSlugs() as $slug) { + $slugs[] = $slug->getSlug(); + } + $body = implode("\n", $slugs); + + $document + ->setDocumentTitle($role->getDisplayName()) + ->addField(PhabricatorSearchDocumentFieldType::FIELD_BODY, $body); + + $document->addRelationship( + $role->isArchived() + ? PhabricatorSearchRelationship::RELATIONSHIP_CLOSED + : PhabricatorSearchRelationship::RELATIONSHIP_OPEN, + $role->getPHID(), + PhabricatorRoleRolePHIDType::TYPECONST, + PhabricatorTime::getNow()); + } + +} diff --git a/src/applications/roles/searchfield/PhabricatorRoleSearchField.php b/src/applications/roles/searchfield/PhabricatorRoleSearchField.php new file mode 100644 index 0000000000..96030b9b36 --- /dev/null +++ b/src/applications/roles/searchfield/PhabricatorRoleSearchField.php @@ -0,0 +1,54 @@ +getListFromRequest($request, $key); + + $phids = array(); + $slugs = array(); + $role_type = PhabricatorRoleRolePHIDType::TYPECONST; + foreach ($list as $item) { + $type = phid_get_type($item); + if ($type == $role_type) { + $phids[] = $item; + } else { + if (PhabricatorTypeaheadDatasource::isFunctionToken($item)) { + // If this is a function, pass it through unchanged; we'll evaluate + // it later. + $phids[] = $item; + } else { + $slugs[] = $item; + } + } + } + + if ($slugs) { + $roles = id(new PhabricatorRoleQuery()) + ->setViewer($this->getViewer()) + ->withSlugs($slugs) + ->execute(); + foreach ($roles as $role) { + $phids[] = $role->getPHID(); + } + $phids = array_unique($phids); + } + + return $phids; + + } + + protected function newConduitParameterType() { + return new ConduitRoleListParameterType(); + } + +} diff --git a/src/applications/roles/state/PhabricatorWorkboardViewState.php b/src/applications/roles/state/PhabricatorWorkboardViewState.php new file mode 100644 index 0000000000..95de1b8a19 --- /dev/null +++ b/src/applications/roles/state/PhabricatorWorkboardViewState.php @@ -0,0 +1,292 @@ +role = $role; + return $this; + } + + public function getRole() { + return $this->role; + } + + public function readFromRequest(AphrontRequest $request) { + if ($request->getExists('hidden')) { + $this->requestState['hidden'] = $request->getBool('hidden'); + } + + if ($request->getExists('order')) { + $this->requestState['order'] = $request->getStr('order'); + } + + // On some pathways, the search engine query key may be specified with + // either a "?filter=X" query parameter or with a "/query/X/" URI + // component. If both are present, the URI component is controlling. + + // In particular, the "queryKey" URI parameter is used by + // "buildSavedQueryFromRequest()" when we are building custom board filters + // by invoking SearchEngine code. + + if ($request->getExists('filter')) { + $this->requestState['filter'] = $request->getStr('filter'); + } + + $query_key = $request->getURIData('queryKey'); + if ($query_key !== null && $query_key !== '') { + $this->requestState['filter'] = $request->getURIData('queryKey'); + } + + $this->viewer = $request->getViewer(); + + return $this; + } + + public function getViewer() { + return $this->viewer; + } + + public function getSavedQuery() { + if ($this->savedQuery === null) { + $this->savedQuery = $this->newSavedQuery(); + } + + return $this->savedQuery; + } + + private function newSavedQuery() { + $search_engine = $this->getSearchEngine(); + $query_key = $this->getQueryKey(); + $viewer = $this->getViewer(); + + if ($search_engine->isBuiltinQuery($query_key)) { + $saved_query = $search_engine->buildSavedQueryFromBuiltin($query_key); + } else { + $saved_query = id(new PhabricatorSavedQueryQuery()) + ->setViewer($viewer) + ->withQueryKeys(array($query_key)) + ->executeOne(); + } + + return $saved_query; + } + + public function getSearchEngine() { + if ($this->searchEngine === null) { + $this->searchEngine = $this->newSearchEngine(); + } + + return $this->searchEngine; + } + + private function newSearchEngine() { + $viewer = $this->getViewer(); + + // TODO: This URI is not fully state-preserving, because "SearchEngine" + // does not preserve URI parameters when constructing some URIs at time of + // writing. + $board_uri = $this->getRole()->getWorkboardURI(); + + return id(new ManiphestTaskSearchEngine()) + ->setViewer($viewer) + ->setBaseURI($board_uri) + ->setIsBoardView(true); + } + + public function newWorkboardURI($path = null) { + $role = $this->getRole(); + $uri = urisprintf('%s%s', $role->getWorkboardURI(), $path); + return $this->newURI($uri); + } + + public function newURI($path) { + $role = $this->getRole(); + $uri = new PhutilURI($path); + + $request_order = $this->getOrder(); + $default_order = $this->getDefaultOrder(); + if ($request_order !== $default_order) { + $request_value = idx($this->requestState, 'order'); + if ($request_value !== null) { + $uri->replaceQueryParam('order', $request_value); + } else { + $uri->removeQueryParam('order'); + } + } else { + $uri->removeQueryParam('order'); + } + + $request_query = $this->getQueryKey(); + $default_query = $this->getDefaultQueryKey(); + if ($request_query !== $default_query) { + $request_value = idx($this->requestState, 'filter'); + if ($request_value !== null) { + $uri->replaceQueryParam('filter', $request_value); + } else { + $uri->removeQueryParam('filter'); + } + } else { + $uri->removeQueryParam('filter'); + } + + if ($this->getShowHidden()) { + $uri->replaceQueryParam('hidden', 'true'); + } else { + $uri->removeQueryParam('hidden'); + } + + return $uri; + } + + public function getShowHidden() { + $request_show = idx($this->requestState, 'hidden'); + + if ($request_show !== null) { + return $request_show; + } + + return false; + } + + public function getOrder() { + $request_order = idx($this->requestState, 'order'); + if ($request_order !== null) { + if ($this->isValidOrder($request_order)) { + return $request_order; + } + } + + return $this->getDefaultOrder(); + } + + public function getQueryKey() { + $request_query = idx($this->requestState, 'filter'); + if ($request_query !== null && $request_query !== '') { + return $request_query; + } + + return $this->getDefaultQueryKey(); + } + + public function setQueryKey($query_key) { + $this->requestState['filter'] = $query_key; + return $this; + } + + private function isValidOrder($order) { + $map = PhabricatorRoleColumnOrder::getEnabledOrders(); + return isset($map[$order]); + } + + private function getDefaultOrder() { + $role = $this->getRole(); + + $default_order = $role->getDefaultWorkboardSort(); + + if ($this->isValidOrder($default_order)) { + return $default_order; + } + + return PhabricatorRoleColumnNaturalOrder::ORDERKEY; + } + + private function getDefaultQueryKey() { + $role = $this->getRole(); + + $default_query = $role->getDefaultWorkboardFilter(); + + if ($default_query !== null && $default_query !== '') { + return $default_query; + } + + return 'open'; + } + + public function getQueryParameters() { + return $this->requestState; + } + + public function getLayoutEngine() { + if ($this->layoutEngine === null) { + $this->layoutEngine = $this->newLayoutEngine(); + } + return $this->layoutEngine; + } + + private function newLayoutEngine() { + $role = $this->getRole(); + $viewer = $this->getViewer(); + + $board_phid = $role->getPHID(); + $objects = $this->getObjects(); + + // Regardless of display order, pass tasks to the layout engine in ID order + // so layout is consistent. + $objects = msort($objects, 'getID'); + + $layout_engine = id(new PhabricatorRoleBoardLayoutEngine()) + ->setViewer($viewer) + ->setObjectPHIDs(array_keys($objects)) + ->setBoardPHIDs(array($board_phid)) + ->setFetchAllBoards(true) + ->executeLayout(); + + return $layout_engine; + } + + public function getBoardContainerPHIDs() { + $role = $this->getRole(); + $viewer = $this->getViewer(); + + $container_phids = array($role->getPHID()); + if ($role->getHasSubroles() || $role->getHasMilestones()) { + $descendants = id(new PhabricatorRoleQuery()) + ->setViewer($viewer) + ->withAncestorRolePHIDs($container_phids) + ->execute(); + foreach ($descendants as $descendant) { + $container_phids[] = $descendant->getPHID(); + } + } + + return $container_phids; + } + + public function getObjects() { + if ($this->objects === null) { + $this->objects = $this->newObjects(); + } + + return $this->objects; + } + + private function newObjects() { + $viewer = $this->getViewer(); + $saved_query = $this->getSavedQuery(); + $search_engine = $this->getSearchEngine(); + + $container_phids = $this->getBoardContainerPHIDs(); + + $task_query = $search_engine->buildQueryFromSavedQuery($saved_query) + ->setViewer($viewer) + ->withEdgeLogicPHIDs( + PhabricatorRoleObjectHasRoleEdgeType::EDGECONST, + PhabricatorQueryConstraint::OPERATOR_ANCESTOR, + array($container_phids)); + + $tasks = $task_query->execute(); + $tasks = mpull($tasks, null, 'getPHID'); + + return $tasks; + } + +} diff --git a/src/applications/roles/storage/PhabricatorRole.php b/src/applications/roles/storage/PhabricatorRole.php new file mode 100644 index 0000000000..8012b59258 --- /dev/null +++ b/src/applications/roles/storage/PhabricatorRole.php @@ -0,0 +1,918 @@ +setViewer(PhabricatorUser::getOmnipotentUser()) + ->withClasses(array('PhabricatorRoleApplication')) + ->executeOne(); + + $view_policy = $app->getPolicy( + RoleDefaultViewCapability::CAPABILITY); + $edit_policy = $app->getPolicy( + RoleDefaultEditCapability::CAPABILITY); + $join_policy = $app->getPolicy( + RoleDefaultJoinCapability::CAPABILITY); + + // If this is the child of some other role, default the Space to the + // Space of the parent. + if ($parent) { + $space_phid = $parent->getSpacePHID(); + } else { + $space_phid = $actor->getDefaultSpacePHID(); + } + + $default_icon = PhabricatorRoleIconSet::getDefaultIconKey(); + $default_color = PhabricatorRoleIconSet::getDefaultColorKey(); + + return id(new PhabricatorRole()) + ->setAuthorPHID($actor->getPHID()) + ->setIcon($default_icon) + ->setColor($default_color) + ->setViewPolicy($view_policy) + ->setEditPolicy($edit_policy) + ->setJoinPolicy($join_policy) + ->setSpacePHID($space_phid) + ->setIsMembershipLocked(0) + ->attachMemberPHIDs(array()) + ->attachSlugs(array()) + ->setHasWorkboard(0) + ->setHasMilestones(0) + ->setHasSubroles(0) + ->setSubtype(PhabricatorEditEngineSubtype::SUBTYPE_DEFAULT) + ->attachParentRole($parent); + } + + public function getCapabilities() { + return array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + PhabricatorPolicyCapability::CAN_JOIN, + ); + } + + public function getPolicy($capability) { + if ($this->isMilestone()) { + return $this->getParentRole()->getPolicy($capability); + } + + switch ($capability) { + case PhabricatorPolicyCapability::CAN_VIEW: + return $this->getViewPolicy(); + case PhabricatorPolicyCapability::CAN_EDIT: + return $this->getEditPolicy(); + case PhabricatorPolicyCapability::CAN_JOIN: + return $this->getJoinPolicy(); + } + } + + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + if ($this->isMilestone()) { + return $this->getParentRole()->hasAutomaticCapability( + $capability, + $viewer); + } + + $can_edit = PhabricatorPolicyCapability::CAN_EDIT; + + switch ($capability) { + case PhabricatorPolicyCapability::CAN_VIEW: + if ($this->isUserMember($viewer->getPHID())) { + // Role members can always view a role. + return true; + } + break; + case PhabricatorPolicyCapability::CAN_EDIT: + $parent = $this->getParentRole(); + if ($parent) { + $can_edit_parent = PhabricatorPolicyFilter::hasCapability( + $viewer, + $parent, + $can_edit); + if ($can_edit_parent) { + return true; + } + } + break; + case PhabricatorPolicyCapability::CAN_JOIN: + if (PhabricatorPolicyFilter::hasCapability($viewer, $this, $can_edit)) { + // Role editors can always join a role. + return true; + } + break; + } + + return false; + } + + public function describeAutomaticCapability($capability) { + + // TODO: Clarify the additional rules that parent and subroles imply. + + switch ($capability) { + case PhabricatorPolicyCapability::CAN_VIEW: + return pht('Members of a role can always view it.'); + case PhabricatorPolicyCapability::CAN_JOIN: + return pht('Users who can edit a role can always join it.'); + } + return null; + } + + public function getExtendedPolicy($capability, PhabricatorUser $viewer) { + $extended = array(); + + switch ($capability) { + case PhabricatorPolicyCapability::CAN_VIEW: + $parent = $this->getParentRole(); + if ($parent) { + $extended[] = array( + $parent, + PhabricatorPolicyCapability::CAN_VIEW, + ); + } + break; + } + + return $extended; + } + + public function isUserMember($user_phid) { + if ($this->memberPHIDs !== self::ATTACHABLE) { + return in_array($user_phid, $this->memberPHIDs); + } + return $this->assertAttachedKey($this->sparseMembers, $user_phid); + } + + public function setIsUserMember($user_phid, $is_member) { + if ($this->sparseMembers === self::ATTACHABLE) { + $this->sparseMembers = array(); + } + $this->sparseMembers[$user_phid] = $is_member; + return $this; + } + + protected function getConfiguration() { + return array( + self::CONFIG_AUX_PHID => true, + self::CONFIG_SERIALIZATION => array( + 'properties' => self::SERIALIZATION_JSON, + ), + self::CONFIG_COLUMN_SCHEMA => array( + 'name' => 'sort128', + 'status' => 'text32', + 'primarySlug' => 'text128?', + 'isMembershipLocked' => 'bool', + 'profileImagePHID' => 'phid?', + 'icon' => 'text32', + 'color' => 'text32', + 'mailKey' => 'bytes20', + 'joinPolicy' => 'policy', + 'parentRolePHID' => 'phid?', + 'hasWorkboard' => 'bool', + 'hasMilestones' => 'bool', + 'hasSubroles' => 'bool', + 'milestoneNumber' => 'uint32?', + 'rolePath' => 'hashpath64', + 'roleDepth' => 'uint32', + 'rolePathKey' => 'bytes4', + 'subtype' => 'text64', + ), + self::CONFIG_KEY_SCHEMA => array( + 'key_icon' => array( + 'columns' => array('icon'), + ), + 'key_color' => array( + 'columns' => array('color'), + ), + 'key_milestone' => array( + 'columns' => array('parentRolePHID', 'milestoneNumber'), + 'unique' => true, + ), + 'key_primaryslug' => array( + 'columns' => array('primarySlug'), + 'unique' => true, + ), + 'key_path' => array( + 'columns' => array('rolePath', 'roleDepth'), + ), + 'key_pathkey' => array( + 'columns' => array('rolePathKey'), + 'unique' => true, + ), + ), + ) + parent::getConfiguration(); + } + + public function generatePHID() { + return PhabricatorPHID::generateNewPHID( + PhabricatorRoleRolePHIDType::TYPECONST); + } + + public function attachMemberPHIDs(array $phids) { + $this->memberPHIDs = $phids; + return $this; + } + + public function getMemberPHIDs() { + return $this->assertAttached($this->memberPHIDs); + } + + public function isArchived() { + return ($this->getStatus() == PhabricatorRoleStatus::STATUS_ARCHIVED); + } + + public function getProfileImageURI() { + return $this->getProfileImageFile()->getBestURI(); + } + + public function attachProfileImageFile(PhabricatorFile $file) { + $this->profileImageFile = $file; + return $this; + } + + public function getProfileImageFile() { + return $this->assertAttached($this->profileImageFile); + } + + + public function isUserWatcher($user_phid) { + if ($this->watcherPHIDs !== self::ATTACHABLE) { + return in_array($user_phid, $this->watcherPHIDs); + } + return $this->assertAttachedKey($this->sparseWatchers, $user_phid); + } + + public function isUserAncestorWatcher($user_phid) { + $is_watcher = $this->isUserWatcher($user_phid); + + if (!$is_watcher) { + $parent = $this->getParentRole(); + if ($parent) { + return $parent->isUserWatcher($user_phid); + } + } + + return $is_watcher; + } + + public function getWatchedAncestorPHID($user_phid) { + if ($this->isUserWatcher($user_phid)) { + return $this->getPHID(); + } + + $parent = $this->getParentRole(); + if ($parent) { + return $parent->getWatchedAncestorPHID($user_phid); + } + + return null; + } + + public function setIsUserWatcher($user_phid, $is_watcher) { + if ($this->sparseWatchers === self::ATTACHABLE) { + $this->sparseWatchers = array(); + } + $this->sparseWatchers[$user_phid] = $is_watcher; + return $this; + } + + public function attachWatcherPHIDs(array $phids) { + $this->watcherPHIDs = $phids; + return $this; + } + + public function getWatcherPHIDs() { + return $this->assertAttached($this->watcherPHIDs); + } + + public function getAllAncestorWatcherPHIDs() { + $parent = $this->getParentRole(); + if ($parent) { + $watchers = $parent->getAllAncestorWatcherPHIDs(); + } else { + $watchers = array(); + } + + foreach ($this->getWatcherPHIDs() as $phid) { + $watchers[$phid] = $phid; + } + + return $watchers; + } + + public function attachSlugs(array $slugs) { + $this->slugs = $slugs; + return $this; + } + + public function getSlugs() { + return $this->assertAttached($this->slugs); + } + + public function getColor() { + if ($this->isArchived()) { + return PHUITagView::COLOR_DISABLED; + } + + return $this->color; + } + + public function getURI() { + $id = $this->getID(); + return "/role/view/{$id}/"; + } + + public function getProfileURI() { + $id = $this->getID(); + return "/role/profile/{$id}/"; + } + + public function getWorkboardURI() { + return urisprintf('/role/board/%d/', $this->getID()); + } + + public function getReportsURI() { + return urisprintf('/role/reports/%d/', $this->getID()); + } + + public function save() { + if (!$this->getMailKey()) { + $this->setMailKey(Filesystem::readRandomCharacters(20)); + } + + if ($this->getPHID() === null || $this->getPHID() === '') { + $this->setPHID($this->generatePHID()); + } + + if ($this->getRolePathKey() === null || $this->getRolePathKey() === '') { + $hash = PhabricatorHash::digestForIndex($this->getPHID()); + $hash = substr($hash, 0, 4); + $this->setRolePathKey($hash); + } + + $path = array(); + $depth = 0; + if ($this->parentRolePHID) { + $parent = $this->getParentRole(); + $path[] = $parent->getRolePath(); + $depth = $parent->getRoleDepth() + 1; + } + $path[] = $this->getRolePathKey(); + $path = implode('', $path); + + $limit = self::getRoleDepthLimit(); + if ($depth >= $limit) { + throw new Exception(pht('Role depth is too great.')); + } + + $this->setRolePath($path); + $this->setRoleDepth($depth); + + $this->openTransaction(); + $result = parent::save(); + $this->updateDatasourceTokens(); + $this->saveTransaction(); + + return $result; + } + + public static function getRoleDepthLimit() { + // This is limited by how many path hashes we can fit in the path + // column. + return 16; + } + + public function updateDatasourceTokens() { + $table = self::TABLE_DATASOURCE_TOKEN; + $conn_w = $this->establishConnection('w'); + $id = $this->getID(); + + $slugs = queryfx_all( + $conn_w, + 'SELECT * FROM %T WHERE rolePHID = %s', + id(new PhabricatorRoleSlug())->getTableName(), + $this->getPHID()); + + $all_strings = ipull($slugs, 'slug'); + $all_strings[] = $this->getDisplayName(); + $all_strings = implode(' ', $all_strings); + + $tokens = PhabricatorTypeaheadDatasource::tokenizeString($all_strings); + + $sql = array(); + foreach ($tokens as $token) { + $sql[] = qsprintf($conn_w, '(%d, %s)', $id, $token); + } + + $this->openTransaction(); + queryfx( + $conn_w, + 'DELETE FROM %T WHERE roleID = %d', + $table, + $id); + + foreach (PhabricatorLiskDAO::chunkSQL($sql) as $chunk) { + queryfx( + $conn_w, + 'INSERT INTO %T (roleID, token) VALUES %LQ', + $table, + $chunk); + } + $this->saveTransaction(); + } + + public function isMilestone() { + return ($this->getMilestoneNumber() !== null); + } + + public function getParentRole() { + return $this->assertAttached($this->parentRole); + } + + public function attachParentRole(PhabricatorRole $role = null) { + $this->parentRole = $role; + return $this; + } + + public function getAncestorRolePaths() { + $parts = array(); + + $path = $this->getRolePath(); + $parent_length = (strlen($path) - 4); + + for ($ii = $parent_length; $ii > 0; $ii -= 4) { + $parts[] = substr($path, 0, $ii); + } + + return $parts; + } + + public function getAncestorRoles() { + $ancestors = array(); + + $cursor = $this->getParentRole(); + while ($cursor) { + $ancestors[] = $cursor; + $cursor = $cursor->getParentRole(); + } + + return $ancestors; + } + + public function supportsEditMembers() { + if ($this->isMilestone()) { + return false; + } + + if ($this->getHasSubroles()) { + return false; + } + + return true; + } + + public function supportsMilestones() { + if ($this->isMilestone()) { + return false; + } + + return true; + } + + public function supportsSubroles() { + if ($this->isMilestone()) { + return false; + } + + return true; + } + + public function loadNextMilestoneNumber() { + $current = queryfx_one( + $this->establishConnection('w'), + 'SELECT MAX(milestoneNumber) n + FROM %T + WHERE parentRolePHID = %s', + $this->getTableName(), + $this->getPHID()); + + if (!$current) { + $number = 1; + } else { + $number = (int)$current['n'] + 1; + } + + return $number; + } + + public function getDisplayName() { + $name = $this->getName(); + + // If this is a milestone, show it as "Parent > Sprint 99". + if ($this->isMilestone()) { + $name = pht( + '%s (%s)', + $this->getParentRole()->getName(), + $name); + } + + return $name; + } + + public function getDisplayIconKey() { + if ($this->isMilestone()) { + $key = PhabricatorRoleIconSet::getMilestoneIconKey(); + } else { + $key = $this->getIcon(); + } + + return $key; + } + + public function getDisplayIconIcon() { + $key = $this->getDisplayIconKey(); + return PhabricatorRoleIconSet::getIconIcon($key); + } + + public function getDisplayIconName() { + $key = $this->getDisplayIconKey(); + return PhabricatorRoleIconSet::getIconName($key); + } + + public function getDisplayColor() { + if ($this->isMilestone()) { + return $this->getParentRole()->getColor(); + } + + return $this->getColor(); + } + + public function getDisplayIconComposeIcon() { + $icon = $this->getDisplayIconIcon(); + return $icon; + } + + public function getDisplayIconComposeColor() { + $color = $this->getDisplayColor(); + + $map = array( + 'grey' => 'charcoal', + 'checkered' => 'backdrop', + ); + + return idx($map, $color, $color); + } + + public function getProperty($key, $default = null) { + return idx($this->properties, $key, $default); + } + + public function setProperty($key, $value) { + $this->properties[$key] = $value; + return $this; + } + + public function getDefaultWorkboardSort() { + return $this->getProperty('workboard.sort.default'); + } + + public function setDefaultWorkboardSort($sort) { + return $this->setProperty('workboard.sort.default', $sort); + } + + public function getDefaultWorkboardFilter() { + return $this->getProperty('workboard.filter.default'); + } + + public function setDefaultWorkboardFilter($filter) { + return $this->setProperty('workboard.filter.default', $filter); + } + + public function getWorkboardBackgroundColor() { + return $this->getProperty('workboard.background'); + } + + public function setWorkboardBackgroundColor($color) { + return $this->setProperty('workboard.background', $color); + } + + public function getDisplayWorkboardBackgroundColor() { + $color = $this->getWorkboardBackgroundColor(); + + if ($color === null) { + $parent = $this->getParentRole(); + if ($parent) { + return $parent->getDisplayWorkboardBackgroundColor(); + } + } + + if ($color === 'none') { + $color = null; + } + + return $color; + } + + +/* -( PhabricatorCustomFieldInterface )------------------------------------ */ + + + public function getCustomFieldSpecificationForRole($role) { + return PhabricatorEnv::getEnvConfig('roles.fields'); + } + + public function getCustomFieldBaseClass() { + return 'PhabricatorRoleCustomField'; + } + + public function getCustomFields() { + return $this->assertAttached($this->customFields); + } + + public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) { + $this->customFields = $fields; + return $this; + } + + +/* -( PhabricatorApplicationTransactionInterface )------------------------- */ + + + public function getApplicationTransactionEditor() { + return new PhabricatorRoleTransactionEditor(); + } + + public function getApplicationTransactionTemplate() { + return new PhabricatorRoleTransaction(); + } + + +/* -( PhabricatorSpacesInterface )----------------------------------------- */ + + + public function getSpacePHID() { + if ($this->isMilestone()) { + return $this->getParentRole()->getSpacePHID(); + } + return $this->spacePHID; + } + + +/* -( PhabricatorDestructibleInterface )----------------------------------- */ + + + public function destroyObjectPermanently( + PhabricatorDestructionEngine $engine) { + + $this->openTransaction(); + $this->delete(); + + $columns = id(new PhabricatorRoleColumn()) + ->loadAllWhere('rolePHID = %s', $this->getPHID()); + foreach ($columns as $column) { + $engine->destroyObject($column); + } + + $slugs = id(new PhabricatorRoleSlug()) + ->loadAllWhere('rolePHID = %s', $this->getPHID()); + foreach ($slugs as $slug) { + $slug->delete(); + } + + $this->saveTransaction(); + } + + +/* -( PhabricatorFulltextInterface )--------------------------------------- */ + + + public function newFulltextEngine() { + return new PhabricatorRoleFulltextEngine(); + } + + +/* -( PhabricatorFerretInterface )--------------------------------------- */ + + + public function newFerretEngine() { + return new PhabricatorRoleFerretEngine(); + } + + +/* -( PhabricatorConduitResultInterface )---------------------------------- */ + + + public function getFieldSpecificationsForConduit() { + return array( + id(new PhabricatorConduitSearchFieldSpecification()) + ->setKey('name') + ->setType('string') + ->setDescription(pht('The name of the role.')), + id(new PhabricatorConduitSearchFieldSpecification()) + ->setKey('slug') + ->setType('string') + ->setDescription(pht('Primary slug/hashtag.')), + id(new PhabricatorConduitSearchFieldSpecification()) + ->setKey('subtype') + ->setType('string') + ->setDescription(pht('Subtype of the role.')), + id(new PhabricatorConduitSearchFieldSpecification()) + ->setKey('milestone') + ->setType('int?') + ->setDescription(pht('For milestones, milestone sequence number.')), + id(new PhabricatorConduitSearchFieldSpecification()) + ->setKey('parent') + ->setType('map?') + ->setDescription( + pht( + 'For subroles and milestones, a brief description of the '. + 'parent role.')), + id(new PhabricatorConduitSearchFieldSpecification()) + ->setKey('depth') + ->setType('int') + ->setDescription( + pht( + 'For subroles and milestones, depth of this role in the '. + 'tree. Root roles have depth 0.')), + id(new PhabricatorConduitSearchFieldSpecification()) + ->setKey('icon') + ->setType('map') + ->setDescription(pht('Information about the role icon.')), + id(new PhabricatorConduitSearchFieldSpecification()) + ->setKey('color') + ->setType('map') + ->setDescription(pht('Information about the role color.')), + ); + } + + public function getFieldValuesForConduit() { + $color_key = $this->getColor(); + $color_name = PhabricatorRoleIconSet::getColorName($color_key); + + if ($this->isMilestone()) { + $milestone = (int)$this->getMilestoneNumber(); + } else { + $milestone = null; + } + + $parent = $this->getParentRole(); + if ($parent) { + $parent_ref = $parent->getRefForConduit(); + } else { + $parent_ref = null; + } + + return array( + 'name' => $this->getName(), + 'slug' => $this->getPrimarySlug(), + 'subtype' => $this->getSubtype(), + 'milestone' => $milestone, + 'depth' => (int)$this->getRoleDepth(), + 'parent' => $parent_ref, + 'icon' => array( + 'key' => $this->getDisplayIconKey(), + 'name' => $this->getDisplayIconName(), + 'icon' => $this->getDisplayIconIcon(), + ), + 'color' => array( + 'key' => $color_key, + 'name' => $color_name, + ), + ); + } + + public function getConduitSearchAttachments() { + return array( + id(new PhabricatorRolesMembersSearchEngineAttachment()) + ->setAttachmentKey('members'), + id(new PhabricatorRolesWatchersSearchEngineAttachment()) + ->setAttachmentKey('watchers'), + id(new PhabricatorRolesAncestorsSearchEngineAttachment()) + ->setAttachmentKey('ancestors'), + ); + } + + /** + * Get an abbreviated representation of this role for use in providing + * "parent" and "ancestor" information. + */ + public function getRefForConduit() { + return array( + 'id' => (int)$this->getID(), + 'phid' => $this->getPHID(), + 'name' => $this->getName(), + ); + } + + +/* -( PhabricatorRoleColumnProxyInterface )------------------------------------ */ + + + public function getProxyRoleColumnName() { + return $this->getName(); + } + + public function getProxyColumnIcon() { + return $this->getDisplayIconIcon(); + } + + public function getProxyColumnClass() { + if ($this->isMilestone()) { + return 'phui-workboard-column-milestone'; + } + + return null; + } + + +/* -( PhabricatorEditEngineSubtypeInterface )------------------------------ */ + + + public function getEditEngineSubtype() { + return $this->getSubtype(); + } + + public function setEditEngineSubtype($value) { + return $this->setSubtype($value); + } + + public function newEditEngineSubtypeMap() { + $config = PhabricatorEnv::getEnvConfig('roles.subtypes'); + return PhabricatorEditEngineSubtype::newSubtypeMap($config) + ->setDatasource(new PhabricatorRoleSubtypeDatasource()); + } + + public function newSubtypeObject() { + $subtype_key = $this->getEditEngineSubtype(); + $subtype_map = $this->newEditEngineSubtypeMap(); + return $subtype_map->getSubtype($subtype_key); + } + +} diff --git a/src/applications/roles/storage/PhabricatorRoleColumn.php b/src/applications/roles/storage/PhabricatorRoleColumn.php new file mode 100644 index 0000000000..be6d89ddbc --- /dev/null +++ b/src/applications/roles/storage/PhabricatorRoleColumn.php @@ -0,0 +1,362 @@ +setName('') + ->setStatus(self::STATUS_ACTIVE) + ->attachProxy(null); + } + + protected function getConfiguration() { + return array( + self::CONFIG_AUX_PHID => true, + self::CONFIG_SERIALIZATION => array( + 'properties' => self::SERIALIZATION_JSON, + ), + self::CONFIG_COLUMN_SCHEMA => array( + 'name' => 'text255', + 'status' => 'uint32', + 'sequence' => 'uint32', + 'proxyPHID' => 'phid?', + 'triggerPHID' => 'phid?', + ), + self::CONFIG_KEY_SCHEMA => array( + 'key_status' => array( + 'columns' => array('rolePHID', 'status', 'sequence'), + ), + 'key_sequence' => array( + 'columns' => array('rolePHID', 'sequence'), + ), + 'key_proxy' => array( + 'columns' => array('rolePHID', 'proxyPHID'), + 'unique' => true, + ), + 'key_trigger' => array( + 'columns' => array('triggerPHID'), + ), + ), + ) + parent::getConfiguration(); + } + + public function generatePHID() { + return PhabricatorPHID::generateNewPHID( + PhabricatorRoleColumnPHIDType::TYPECONST); + } + + public function attachRole(PhabricatorRole $role) { + $this->role = $role; + return $this; + } + + public function getRole() { + return $this->assertAttached($this->role); + } + + public function attachProxy($proxy) { + $this->proxy = $proxy; + return $this; + } + + public function getProxy() { + return $this->assertAttached($this->proxy); + } + + public function isDefaultColumn() { + return (bool)$this->getProperty('isDefault'); + } + + public function isHidden() { + $proxy = $this->getProxy(); + if ($proxy) { + return $proxy->isArchived(); + } + + return ($this->getStatus() == self::STATUS_HIDDEN); + } + + public function getDisplayName() { + $proxy = $this->getProxy(); + if ($proxy) { + return $proxy->getProxyColumnName(); + } + + $name = $this->getName(); + if ($name !== null && $name !== '') { + return $name; + } + + if ($this->isDefaultColumn()) { + return pht('Backlog'); + } + + return pht('Unnamed Column'); + } + + public function getDisplayType() { + if ($this->isDefaultColumn()) { + return pht('(Default)'); + } + if ($this->isHidden()) { + return pht('(Hidden)'); + } + + return null; + } + + public function getDisplayClass() { + $proxy = $this->getProxy(); + if ($proxy) { + return $proxy->getProxyColumnClass(); + } + + return null; + } + + public function getHeaderIcon() { + $proxy = $this->getProxy(); + if ($proxy) { + return $proxy->getProxyColumnIcon(); + } + + if ($this->isHidden()) { + return 'fa-eye-slash'; + } + + return null; + } + + public function getProperty($key, $default = null) { + return idx($this->properties, $key, $default); + } + + public function setProperty($key, $value) { + $this->properties[$key] = $value; + return $this; + } + + public function getPointLimit() { + return $this->getProperty('pointLimit'); + } + + public function setPointLimit($limit) { + $this->setProperty('pointLimit', $limit); + return $this; + } + + public function getOrderingKey() { + $proxy = $this->getProxy(); + + // Normal columns and subrole columns go first, in a user-controlled + // order. + + // All the milestone columns go last, in their sequential order. + + if (!$proxy || !$proxy->isMilestone()) { + $group = 'A'; + $sequence = $this->getSequence(); + } else { + $group = 'B'; + $sequence = $proxy->getMilestoneNumber(); + } + + return sprintf('%s%012d', $group, $sequence); + } + + public function attachTrigger(PhabricatorRoleTrigger $trigger = null) { + $this->trigger = $trigger; + return $this; + } + + public function getTrigger() { + return $this->assertAttached($this->trigger); + } + + public function canHaveTrigger() { + // Backlog columns and proxy (subrole / milestone) columns can't have + // triggers because cards routinely end up in these columns through tag + // edits rather than drag-and-drop and it would likely be confusing to + // have these triggers act only a small fraction of the time. + + if ($this->isDefaultColumn()) { + return false; + } + + if ($this->getProxy()) { + return false; + } + + return true; + } + + public function getWorkboardURI() { + return $this->getRole()->getWorkboardURI(); + } + + public function getDropEffects() { + $effects = array(); + + $proxy = $this->getProxy(); + if ($proxy && $proxy->isMilestone()) { + $effects[] = id(new PhabricatorRoleDropEffect()) + ->setIcon($proxy->getProxyColumnIcon()) + ->setColor('violet') + ->setContent( + pht( + 'Move to milestone %s.', + phutil_tag('strong', array(), $this->getDisplayName()))); + } else { + $effects[] = id(new PhabricatorRoleDropEffect()) + ->setIcon('fa-columns') + ->setColor('blue') + ->setContent( + pht( + 'Move to column %s.', + phutil_tag('strong', array(), $this->getDisplayName()))); + } + + + if ($this->canHaveTrigger()) { + $trigger = $this->getTrigger(); + if ($trigger) { + foreach ($trigger->getDropEffects() as $trigger_effect) { + $effects[] = $trigger_effect; + } + } + } + + return $effects; + } + + +/* -( PhabricatorConduitResultInterface )---------------------------------- */ + + public function getFieldSpecificationsForConduit() { + return array( + id(new PhabricatorConduitSearchFieldSpecification()) + ->setKey('name') + ->setType('string') + ->setDescription(pht('The display name of the column.')), + id(new PhabricatorConduitSearchFieldSpecification()) + ->setKey('role') + ->setType('map') + ->setDescription(pht('The role the column belongs to.')), + id(new PhabricatorConduitSearchFieldSpecification()) + ->setKey('proxyPHID') + ->setType('phid?') + ->setDescription( + pht( + 'For columns that proxy another object (like a subrole or '. + 'milestone), the PHID of the object they proxy.')), + ); + } + + public function getFieldValuesForConduit() { + return array( + 'name' => $this->getDisplayName(), + 'proxyPHID' => $this->getProxyPHID(), + 'role' => $this->getRole()->getRefForConduit(), + ); + } + + public function getConduitSearchAttachments() { + return array(); + } + + public function getRefForConduit() { + return array( + 'id' => (int)$this->getID(), + 'phid' => $this->getPHID(), + 'name' => $this->getDisplayName(), + ); + } + + +/* -( PhabricatorApplicationTransactionInterface )------------------------- */ + + + public function getApplicationTransactionEditor() { + return new PhabricatorRoleColumnTransactionEditor(); + } + + public function getApplicationTransactionTemplate() { + return new PhabricatorRoleColumnTransaction(); + } + + +/* -( PhabricatorPolicyInterface )----------------------------------------- */ + + + public function getCapabilities() { + return array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + ); + } + + public function getPolicy($capability) { + // NOTE: Column policies are enforced as an extended policy which makes + // them the same as the role's policies. + switch ($capability) { + case PhabricatorPolicyCapability::CAN_VIEW: + return PhabricatorPolicies::getMostOpenPolicy(); + case PhabricatorPolicyCapability::CAN_EDIT: + return PhabricatorPolicies::POLICY_USER; + } + } + + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + return $this->getRole()->hasAutomaticCapability( + $capability, + $viewer); + } + + public function describeAutomaticCapability($capability) { + return pht('Users must be able to see a role to see its board.'); + } + + +/* -( PhabricatorExtendedPolicyInterface )--------------------------------- */ + + + public function getExtendedPolicy($capability, PhabricatorUser $viewer) { + return array( + array($this->getRole(), $capability), + ); + } + + +/* -( PhabricatorDestructibleInterface )----------------------------------- */ + + public function destroyObjectPermanently( + PhabricatorDestructionEngine $engine) { + + $this->openTransaction(); + $this->delete(); + $this->saveTransaction(); + } + +} diff --git a/src/applications/roles/storage/PhabricatorRoleColumnPosition.php b/src/applications/roles/storage/PhabricatorRoleColumnPosition.php new file mode 100644 index 0000000000..6605b53892 --- /dev/null +++ b/src/applications/roles/storage/PhabricatorRoleColumnPosition.php @@ -0,0 +1,89 @@ + false, + self::CONFIG_COLUMN_SCHEMA => array( + 'sequence' => 'uint32', + ), + self::CONFIG_KEY_SCHEMA => array( + 'boardPHID' => array( + 'columns' => array('boardPHID', 'columnPHID', 'objectPHID'), + 'unique' => true, + ), + 'objectPHID' => array( + 'columns' => array('objectPHID', 'boardPHID'), + ), + 'boardPHID_2' => array( + 'columns' => array('boardPHID', 'columnPHID', 'sequence'), + ), + ), + ) + parent::getConfiguration(); + } + + public function getColumn() { + return $this->assertAttached($this->column); + } + + public function attachColumn(PhabricatorRoleColumn $column) { + $this->column = $column; + return $this; + } + + public function setViewSequence($view_sequence) { + $this->viewSequence = $view_sequence; + return $this; + } + + public function newColumnPositionOrderVector() { + // We're ordering both real positions and "virtual" positions which we have + // created but not saved yet. + + // Low sequence numbers go above high sequence numbers. Virtual positions + // will have sequence number 0. + + // High virtual sequence numbers go above low virtual sequence numbers. + // The layout engine gets objects in ID order, and this puts them in + // reverse ID order. + + // High IDs go above low IDs. + + // Broadly, this collectively makes newly added stuff float to the top. + + return id(new PhutilSortVector()) + ->addInt($this->getSequence()) + ->addInt(-1 * $this->viewSequence) + ->addInt(-1 * $this->getID()); + } + +/* -( PhabricatorPolicyInterface )----------------------------------------- */ + + public function getCapabilities() { + return array( + PhabricatorPolicyCapability::CAN_VIEW, + ); + } + + public function getPolicy($capability) { + switch ($capability) { + case PhabricatorPolicyCapability::CAN_VIEW: + return PhabricatorPolicies::getMostOpenPolicy(); + } + } + + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + return false; + } + +} diff --git a/src/applications/roles/storage/PhabricatorRoleColumnTransaction.php b/src/applications/roles/storage/PhabricatorRoleColumnTransaction.php new file mode 100644 index 0000000000..1c791a17e9 --- /dev/null +++ b/src/applications/roles/storage/PhabricatorRoleColumnTransaction.php @@ -0,0 +1,18 @@ +buildEdgeSchemata(new PhabricatorRole()); + + $this->buildRawSchema( + id(new PhabricatorRole())->getApplicationName(), + PhabricatorRole::TABLE_DATASOURCE_TOKEN, + array( + 'id' => 'auto', + 'roleID' => 'id', + 'token' => 'text128', + ), + array( + 'PRIMARY' => array( + 'columns' => array('id'), + 'unique' => true, + ), + 'token' => array( + 'columns' => array('token', 'roleID'), + 'unique' => true, + ), + 'roleID' => array( + 'columns' => array('roleID'), + ), + )); + + + } + +} diff --git a/src/applications/roles/storage/PhabricatorRoleSlug.php b/src/applications/roles/storage/PhabricatorRoleSlug.php new file mode 100644 index 0000000000..3c097275a1 --- /dev/null +++ b/src/applications/roles/storage/PhabricatorRoleSlug.php @@ -0,0 +1,25 @@ + array( + 'slug' => 'text128', + ), + self::CONFIG_KEY_SCHEMA => array( + 'key_slug' => array( + 'columns' => array('slug'), + 'unique' => true, + ), + 'key_rolePHID' => array( + 'columns' => array('rolePHID'), + ), + ), + ) + parent::getConfiguration(); + } + +} diff --git a/src/applications/roles/storage/PhabricatorRoleTransaction.php b/src/applications/roles/storage/PhabricatorRoleTransaction.php new file mode 100644 index 0000000000..b5e69a9e16 --- /dev/null +++ b/src/applications/roles/storage/PhabricatorRoleTransaction.php @@ -0,0 +1,164 @@ +getOldValue(); + $new = $this->getNewValue(); + + $req_phids = array(); + switch ($this->getTransactionType()) { + case self::TYPE_MEMBERS: + $add = array_diff($new, $old); + $rem = array_diff($old, $new); + $req_phids = array_merge($add, $rem); + break; + } + + return array_merge($req_phids, parent::getRequiredHandlePHIDs()); + } + + public function shouldHide() { + switch ($this->getTransactionType()) { + case PhabricatorTransactions::TYPE_EDGE: + $edge_type = $this->getMetadataValue('edge:type'); + switch ($edge_type) { + case PhabricatorRoleSilencedEdgeType::EDGECONST: + return true; + default: + break; + } + } + + return parent::shouldHide(); + } + + public function shouldHideForMail(array $xactions) { + switch ($this->getTransactionType()) { + case PhabricatorRoleWorkboardTransaction::TRANSACTIONTYPE: + case PhabricatorRoleSortTransaction::TRANSACTIONTYPE: + case PhabricatorRoleFilterTransaction::TRANSACTIONTYPE: + case PhabricatorRoleWorkboardBackgroundTransaction::TRANSACTIONTYPE: + return true; + } + + return parent::shouldHideForMail($xactions); + } + + public function getIcon() { + switch ($this->getTransactionType()) { + case self::TYPE_MEMBERS: + return 'fa-user'; + } + return parent::getIcon(); + } + + public function getTitle() { + $old = $this->getOldValue(); + $new = $this->getNewValue(); + $author_phid = $this->getAuthorPHID(); + $author_handle = $this->renderHandleLink($author_phid); + + switch ($this->getTransactionType()) { + case PhabricatorTransactions::TYPE_CREATE: + return pht( + '%s created this role.', + $this->renderHandleLink($author_phid)); + + case self::TYPE_MEMBERS: + $add = array_diff($new, $old); + $rem = array_diff($old, $new); + + if ($add && $rem) { + return pht( + '%s changed role member(s), added %d: %s; removed %d: %s.', + $author_handle, + count($add), + $this->renderHandleList($add), + count($rem), + $this->renderHandleList($rem)); + } else if ($add) { + if (count($add) == 1 && (head($add) == $this->getAuthorPHID())) { + return pht( + '%s joined this role.', + $author_handle); + } else { + return pht( + '%s added %d role member(s): %s.', + $author_handle, + count($add), + $this->renderHandleList($add)); + } + } else if ($rem) { + if (count($rem) == 1 && (head($rem) == $this->getAuthorPHID())) { + return pht( + '%s left this role.', + $author_handle); + } else { + return pht( + '%s removed %d role member(s): %s.', + $author_handle, + count($rem), + $this->renderHandleList($rem)); + } + } + break; + } + + return parent::getTitle(); + } + + public function getMailTags() { + $tags = array(); + switch ($this->getTransactionType()) { + case PhabricatorRoleNameTransaction::TRANSACTIONTYPE: + case PhabricatorRoleSlugsTransaction::TRANSACTIONTYPE: + case PhabricatorRoleImageTransaction::TRANSACTIONTYPE: + case PhabricatorRoleIconTransaction::TRANSACTIONTYPE: + case PhabricatorRoleColorTransaction::TRANSACTIONTYPE: + $tags[] = self::MAILTAG_METADATA; + break; + case PhabricatorTransactions::TYPE_EDGE: + $type = $this->getMetadata('edge:type'); + $type = head($type); + $type_member = PhabricatorRoleRoleHasMemberEdgeType::EDGECONST; + $type_watcher = PhabricatorObjectHasWatcherEdgeType::EDGECONST; + if ($type == $type_member) { + $tags[] = self::MAILTAG_MEMBERS; + } else if ($type == $type_watcher) { + $tags[] = self::MAILTAG_WATCHERS; + } else { + $tags[] = self::MAILTAG_OTHER; + } + break; + case PhabricatorRoleStatusTransaction::TRANSACTIONTYPE: + case PhabricatorRoleLockTransaction::TRANSACTIONTYPE: + default: + $tags[] = self::MAILTAG_OTHER; + break; + } + return $tags; + } + +} diff --git a/src/applications/roles/storage/PhabricatorRoleTrigger.php b/src/applications/roles/storage/PhabricatorRoleTrigger.php new file mode 100644 index 0000000000..b172937484 --- /dev/null +++ b/src/applications/roles/storage/PhabricatorRoleTrigger.php @@ -0,0 +1,352 @@ +setName('') + ->setEditPolicy($default_edit); + } + + protected function getConfiguration() { + return array( + self::CONFIG_AUX_PHID => true, + self::CONFIG_SERIALIZATION => array( + 'ruleset' => self::SERIALIZATION_JSON, + ), + self::CONFIG_COLUMN_SCHEMA => array( + 'name' => 'text255', + ), + self::CONFIG_KEY_SCHEMA => array( + ), + ) + parent::getConfiguration(); + } + + public function getPHIDType() { + return PhabricatorRoleTriggerPHIDType::TYPECONST; + } + + public function getViewer() { + return $this->viewer; + } + + public function setViewer(PhabricatorUser $user) { + $this->viewer = $user; + return $this; + } + + public function getDisplayName() { + $name = $this->getName(); + if ($name !== null && $name !== '') { + return $name; + } + + return $this->getDefaultName(); + } + + public function getDefaultName() { + return pht('Custom Trigger'); + } + + public function getURI() { + return urisprintf( + '/role/trigger/%d/', + $this->getID()); + } + + public function getObjectName() { + return pht('Trigger %d', $this->getID()); + } + + public function setRuleset(array $ruleset) { + // Clear any cached trigger rules, since we're changing the ruleset + // for the trigger. + $this->triggerRules = null; + + parent::setRuleset($ruleset); + } + + public function getTriggerRules($viewer = null) { + if ($this->triggerRules === null) { + if (!$viewer) { + $viewer = $this->getViewer(); + } + + $trigger_rules = self::newTriggerRulesFromRuleSpecifications( + $this->getRuleset(), + $allow_invalid = true, + $viewer); + + $this->triggerRules = $trigger_rules; + } + + return $this->triggerRules; + } + + public static function newTriggerRulesFromRuleSpecifications( + array $list, + $allow_invalid, + PhabricatorUser $viewer) { + + // NOTE: With "$allow_invalid" set, we're trying to preserve the database + // state in the rule structure, even if it includes rule types we don't + // have implementations for, or rules with invalid rule values. + + // If an administrator adds or removes extensions which add rules, or + // an upgrade affects rule validity, existing rules may become invalid. + // When they do, we still want the UI to reflect the ruleset state + // accurately and "Edit" + "Save" shouldn't destroy data unless the + // user explicitly modifies the ruleset. + + // In this mode, when we run into rules which are structured correctly but + // which have types we don't know about, we replace them with "Unknown + // Rules". If we know about the type of a rule but the value doesn't + // validate, we replace it with "Invalid Rules". These two rule types don't + // take any actions when a card is dropped into the column, but they show + // the user what's wrong with the ruleset and can be saved without causing + // any collateral damage. + + $rule_map = PhabricatorRoleTriggerRule::getAllTriggerRules(); + + // If the stored rule data isn't a list of rules (or we encounter other + // fundamental structural problems, below), there isn't much we can do + // to try to represent the state. + if (!is_array($list)) { + throw new PhabricatorRoleTriggerCorruptionException( + pht( + 'Trigger ruleset is corrupt: expected a list of rule '. + 'specifications, found "%s".', + phutil_describe_type($list))); + } + + $trigger_rules = array(); + foreach ($list as $key => $rule) { + if (!is_array($rule)) { + throw new PhabricatorRoleTriggerCorruptionException( + pht( + 'Trigger ruleset is corrupt: rule (at index "%s") should be a '. + 'rule specification, but is actually "%s".', + $key, + phutil_describe_type($rule))); + } + + try { + PhutilTypeSpec::checkMap( + $rule, + array( + 'type' => 'string', + 'value' => 'wild', + )); + } catch (PhutilTypeCheckException $ex) { + throw new PhabricatorRoleTriggerCorruptionException( + pht( + 'Trigger ruleset is corrupt: rule (at index "%s") is not a '. + 'valid rule specification: %s', + $key, + $ex->getMessage())); + } + + $record = id(new PhabricatorRoleTriggerRuleRecord()) + ->setType(idx($rule, 'type')) + ->setValue(idx($rule, 'value')); + + if (!isset($rule_map[$record->getType()])) { + if (!$allow_invalid) { + throw new PhabricatorRoleTriggerCorruptionException( + pht( + 'Trigger ruleset is corrupt: rule type "%s" is unknown.', + $record->getType())); + } + + $rule = new PhabricatorRoleTriggerUnknownRule(); + } else { + $rule = clone $rule_map[$record->getType()]; + } + + try { + $rule->setRecord($record); + } catch (Exception $ex) { + if (!$allow_invalid) { + throw new PhabricatorRoleTriggerCorruptionException( + pht( + 'Trigger ruleset is corrupt, rule (of type "%s") does not '. + 'validate: %s', + $record->getType(), + $ex->getMessage())); + } + + $rule = id(new PhabricatorRoleTriggerInvalidRule()) + ->setRecord($record) + ->setException($ex); + } + $rule->setViewer($viewer); + + $trigger_rules[] = $rule; + } + + return $trigger_rules; + } + + + public function getDropEffects() { + $effects = array(); + + $rules = $this->getTriggerRules(); + foreach ($rules as $rule) { + foreach ($rule->getDropEffects() as $effect) { + $effects[] = $effect; + } + } + + return $effects; + } + + public function newDropTransactions( + PhabricatorUser $viewer, + PhabricatorRoleColumn $column, + $object) { + + $trigger_xactions = array(); + foreach ($this->getTriggerRules($viewer) as $rule) { + $rule + ->setTrigger($this) + ->setColumn($column) + ->setObject($object); + + $xactions = $rule->getDropTransactions( + $object, + $rule->getRecord()->getValue()); + + if (!is_array($xactions)) { + throw new Exception( + pht( + 'Expected trigger rule (of class "%s") to return a list of '. + 'transactions from "newDropTransactions()", but got "%s".', + get_class($rule), + phutil_describe_type($xactions))); + } + + $expect_type = get_class($object->getApplicationTransactionTemplate()); + assert_instances_of($xactions, $expect_type); + + foreach ($xactions as $xaction) { + $trigger_xactions[] = $xaction; + } + } + + return $trigger_xactions; + } + + public function getPreviewEffect() { + $header = pht('Trigger: %s', $this->getDisplayName()); + + return id(new PhabricatorRoleDropEffect()) + ->setIcon('fa-cogs') + ->setColor('blue') + ->setIsHeader(true) + ->setContent($header); + } + + public function getSoundEffects() { + $sounds = array(); + + foreach ($this->getTriggerRules() as $rule) { + foreach ($rule->getSoundEffects() as $effect) { + $sounds[] = $effect; + } + } + + return $sounds; + } + + public function getUsage() { + return $this->assertAttached($this->usage); + } + + public function attachUsage(PhabricatorRoleTriggerUsage $usage) { + $this->usage = $usage; + return $this; + } + + +/* -( PhabricatorApplicationTransactionInterface )------------------------- */ + + + public function getApplicationTransactionEditor() { + return new PhabricatorRoleTriggerEditor(); + } + + public function getApplicationTransactionTemplate() { + return new PhabricatorRoleTriggerTransaction(); + } + + +/* -( PhabricatorPolicyInterface )----------------------------------------- */ + + + public function getCapabilities() { + return array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + ); + } + + public function getPolicy($capability) { + switch ($capability) { + case PhabricatorPolicyCapability::CAN_VIEW: + return PhabricatorPolicies::getMostOpenPolicy(); + case PhabricatorPolicyCapability::CAN_EDIT: + return $this->getEditPolicy(); + } + } + + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + return false; + } + + +/* -( PhabricatorDestructibleInterface )----------------------------------- */ + + + public function destroyObjectPermanently( + PhabricatorDestructionEngine $engine) { + + $this->openTransaction(); + $conn = $this->establishConnection('w'); + + // Remove the reference to this trigger from any columns which use it. + queryfx( + $conn, + 'UPDATE %R SET triggerPHID = null WHERE triggerPHID = %s', + new PhabricatorRoleColumn(), + $this->getPHID()); + + // Remove the usage index row for this trigger, if one exists. + queryfx( + $conn, + 'DELETE FROM %R WHERE triggerPHID = %s', + new PhabricatorRoleTriggerUsage(), + $this->getPHID()); + + $this->delete(); + + $this->saveTransaction(); + } + +} diff --git a/src/applications/roles/storage/PhabricatorRoleTriggerTransaction.php b/src/applications/roles/storage/PhabricatorRoleTriggerTransaction.php new file mode 100644 index 0000000000..9bfaafc15e --- /dev/null +++ b/src/applications/roles/storage/PhabricatorRoleTriggerTransaction.php @@ -0,0 +1,18 @@ + false, + self::CONFIG_COLUMN_SCHEMA => array( + 'examplePHID' => 'phid?', + 'columnCount' => 'uint32', + 'activeColumnCount' => 'uint32', + ), + self::CONFIG_KEY_SCHEMA => array( + 'key_trigger' => array( + 'columns' => array('triggerPHID'), + 'unique' => true, + ), + ), + ) + parent::getConfiguration(); + } + +} diff --git a/src/applications/roles/trigger/PhabricatorRoleTriggerAddRolesRule.php b/src/applications/roles/trigger/PhabricatorRoleTriggerAddRolesRule.php new file mode 100644 index 0000000000..69aeb599c4 --- /dev/null +++ b/src/applications/roles/trigger/PhabricatorRoleTriggerAddRolesRule.php @@ -0,0 +1,111 @@ +getDatasource()->getWireTokens($this->getValue()); + } + + protected function assertValidRuleRecordFormat($value) { + if (!is_array($value)) { + throw new Exception( + pht( + 'Add role rule value should be a list, but is not '. + '(value is "%s").', + phutil_describe_type($value))); + } + } + + protected function assertValidRuleRecordValue($value) { + if (!$value) { + throw new Exception( + pht( + 'You must select at least one role tag to add.')); + } + } + + protected function newDropTransactions($object, $value) { + $role_edge_type = PhabricatorRoleObjectHasRoleEdgeType::EDGECONST; + + $xaction = $object->getApplicationTransactionTemplate() + ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) + ->setMetadataValue('edge:type', $role_edge_type) + ->setNewValue( + array( + '+' => array_fuse($value), + )); + + return array($xaction); + } + + protected function newDropEffects($value) { + return array( + $this->newEffect() + ->setIcon('fa-briefcase') + ->setContent($this->getRuleViewDescription($value)), + ); + } + + protected function getDefaultValue() { + return null; + } + + protected function getPHUIXControlType() { + return 'tokenizer'; + } + + private function getDatasource() { + return id(new PhabricatorRoleDatasource()) + ->setViewer($this->getViewer()); + } + + protected function getPHUIXControlSpecification() { + $template = id(new AphrontTokenizerTemplateView()) + ->setViewer($this->getViewer()); + + $template_markup = $template->render(); + $datasource = $this->getDatasource(); + + return array( + 'markup' => (string)hsprintf('%s', $template_markup), + 'config' => array( + 'src' => $datasource->getDatasourceURI(), + 'browseURI' => $datasource->getBrowseURI(), + 'placeholder' => $datasource->getPlaceholderText(), + 'limit' => $datasource->getLimit(), + ), + 'value' => null, + ); + } + + public function getRuleViewLabel() { + return pht('Add Role Tags'); + } + + public function getRuleViewDescription($value) { + return pht( + 'Add role tags: %s.', + phutil_tag( + 'strong', + array(), + $this->getViewer() + ->renderHandleList($value) + ->setAsInline(true) + ->render())); + } + + public function getRuleViewIcon($value) { + return id(new PHUIIconView()) + ->setIcon('fa-briefcase', 'green'); + } + + + +} diff --git a/src/applications/roles/trigger/PhabricatorRoleTriggerInvalidRule.php b/src/applications/roles/trigger/PhabricatorRoleTriggerInvalidRule.php new file mode 100644 index 0000000000..0acccf96ba --- /dev/null +++ b/src/applications/roles/trigger/PhabricatorRoleTriggerInvalidRule.php @@ -0,0 +1,93 @@ +exception = $exception; + return $this; + } + + public function getException() { + return $this->exception; + } + + public function getSelectControlName() { + return pht('(Invalid Rule)'); + } + + protected function isSelectableRule() { + return false; + } + + protected function assertValidRuleRecordFormat($value) { + return; + } + + protected function newDropTransactions($object, $value) { + return array(); + } + + protected function newDropEffects($value) { + return array(); + } + + protected function isValidRule() { + return false; + } + + protected function newInvalidView() { + return array( + id(new PHUIIconView()) + ->setIcon('fa-exclamation-triangle red'), + ' ', + pht( + 'This is a trigger rule with a valid type ("%s") but an invalid '. + 'value.', + $this->getRecord()->getType()), + ); + } + + protected function getDefaultValue() { + return null; + } + + protected function getPHUIXControlType() { + return null; + } + + protected function getPHUIXControlSpecification() { + return null; + } + + public function getRuleViewLabel() { + return pht('Invalid Rule'); + } + + public function getRuleViewDescription($value) { + $record = $this->getRecord(); + $type = $record->getType(); + + $exception = $this->getException(); + if ($exception) { + return pht( + 'This rule (of type "%s") is invalid: %s', + $type, + $exception->getMessage()); + } else { + return pht( + 'This rule (of type "%s") is invalid.', + $type); + } + } + + public function getRuleViewIcon($value) { + return id(new PHUIIconView()) + ->setIcon('fa-exclamation-triangle', 'red'); + } + +} diff --git a/src/applications/roles/trigger/PhabricatorRoleTriggerManiphestOwnerRule.php b/src/applications/roles/trigger/PhabricatorRoleTriggerManiphestOwnerRule.php new file mode 100644 index 0000000000..a111db2d14 --- /dev/null +++ b/src/applications/roles/trigger/PhabricatorRoleTriggerManiphestOwnerRule.php @@ -0,0 +1,148 @@ +getDatasource()->getWireTokens($this->getValue()); + } + + private function convertTokenizerValueToOwner($value) { + $value = head($value); + if ($value === PhabricatorPeopleNoOwnerDatasource::FUNCTION_TOKEN) { + $value = null; + } + return $value; + } + + protected function assertValidRuleRecordFormat($value) { + if (!is_array($value)) { + throw new Exception( + pht( + 'Owner rule value should be a list, but is not (value is "%s").', + phutil_describe_type($value))); + } + } + + protected function assertValidRuleRecordValue($value) { + if (!$value) { + throw new Exception( + pht( + 'Owner rule value is required. Specify a user to assign tasks '. + 'to, or the token "none()" to unassign tasks.')); + } + + if (count($value) > 1) { + throw new Exception( + pht( + 'Owner rule value must have only one elmement (value is "%s").', + implode(', ', $value))); + } + + $owner_phid = $this->convertTokenizerValueToOwner($value); + if ($owner_phid !== null) { + $user = id(new PhabricatorPeopleQuery()) + ->setViewer($this->getViewer()) + ->withPHIDs(array($owner_phid)) + ->executeOne(); + if (!$user) { + throw new Exception( + pht( + 'User PHID ("%s") is not a valid user.', + $owner_phid)); + } + } + } + + protected function newDropTransactions($object, $value) { + $value = $this->convertTokenizerValueToOwner($value); + return array( + $this->newTransaction() + ->setTransactionType(ManiphestTaskOwnerTransaction::TRANSACTIONTYPE) + ->setNewValue($value), + ); + } + + protected function newDropEffects($value) { + $owner_value = $this->convertTokenizerValueToOwner($value); + + return array( + $this->newEffect() + ->setIcon('fa-user') + ->setContent($this->getRuleViewDescription($value)) + ->addCondition('owner', '!=', $owner_value), + ); + } + + protected function getDefaultValue() { + return null; + } + + protected function getPHUIXControlType() { + return 'tokenizer'; + } + + private function getDatasource() { + $datasource = id(new ManiphestAssigneeDatasource()) + ->setLimit(1); + + if ($this->getViewer()) { + $datasource->setViewer($this->getViewer()); + } + + return $datasource; + } + + protected function getPHUIXControlSpecification() { + $template = id(new AphrontTokenizerTemplateView()) + ->setViewer($this->getViewer()); + + $template_markup = $template->render(); + $datasource = $this->getDatasource(); + + return array( + 'markup' => (string)hsprintf('%s', $template_markup), + 'config' => array( + 'src' => $datasource->getDatasourceURI(), + 'browseURI' => $datasource->getBrowseURI(), + 'placeholder' => $datasource->getPlaceholderText(), + 'limit' => $datasource->getLimit(), + ), + 'value' => null, + ); + } + + public function getRuleViewLabel() { + return pht('Change Owner'); + } + + public function getRuleViewDescription($value) { + $value = $this->convertTokenizerValueToOwner($value); + + if (!$value) { + return pht('Unassign task.'); + } else { + return pht( + 'Assign task to %s.', + phutil_tag( + 'strong', + array(), + $this->getViewer() + ->renderHandle($value) + ->render())); + } + } + + public function getRuleViewIcon($value) { + return id(new PHUIIconView()) + ->setIcon('fa-user', 'green'); + } + + +} diff --git a/src/applications/roles/trigger/PhabricatorRoleTriggerManiphestPriorityRule.php b/src/applications/roles/trigger/PhabricatorRoleTriggerManiphestPriorityRule.php new file mode 100644 index 0000000000..750fc7bee2 --- /dev/null +++ b/src/applications/roles/trigger/PhabricatorRoleTriggerManiphestPriorityRule.php @@ -0,0 +1,98 @@ +newTransaction() + ->setTransactionType(ManiphestTaskPriorityTransaction::TRANSACTIONTYPE) + ->setNewValue($value), + ); + } + + protected function newDropEffects($value) { + $priority_name = ManiphestTaskPriority::getTaskPriorityName($value); + $priority_icon = ManiphestTaskPriority::getTaskPriorityIcon($value); + $priority_color = ManiphestTaskPriority::getTaskPriorityColor($value); + + $content = pht( + 'Change priority to %s.', + phutil_tag('strong', array(), $priority_name)); + + return array( + $this->newEffect() + ->setIcon($priority_icon) + ->setColor($priority_color) + ->addCondition('priority', '!=', $value) + ->setContent($content), + ); + } + + protected function getDefaultValue() { + return ManiphestTaskPriority::getDefaultPriority(); + } + + protected function getPHUIXControlType() { + return 'select'; + } + + protected function getPHUIXControlSpecification() { + $map = ManiphestTaskPriority::getTaskPriorityMap(); + + return array( + 'options' => $map, + 'order' => array_keys($map), + ); + } + + public function getRuleViewLabel() { + return pht('Change Priority'); + } + + public function getRuleViewDescription($value) { + $priority_name = ManiphestTaskPriority::getTaskPriorityName($value); + + return pht( + 'Change task priority to %s.', + phutil_tag('strong', array(), $priority_name)); + } + + public function getRuleViewIcon($value) { + $priority_icon = ManiphestTaskPriority::getTaskPriorityIcon($value); + $priority_color = ManiphestTaskPriority::getTaskPriorityColor($value); + + return id(new PHUIIconView()) + ->setIcon($priority_icon, $priority_color); + } + + +} diff --git a/src/applications/roles/trigger/PhabricatorRoleTriggerManiphestStatusRule.php b/src/applications/roles/trigger/PhabricatorRoleTriggerManiphestStatusRule.php new file mode 100644 index 0000000000..7d870fbc39 --- /dev/null +++ b/src/applications/roles/trigger/PhabricatorRoleTriggerManiphestStatusRule.php @@ -0,0 +1,97 @@ +newTransaction() + ->setTransactionType(ManiphestTaskStatusTransaction::TRANSACTIONTYPE) + ->setNewValue($value), + ); + } + + protected function newDropEffects($value) { + $status_name = ManiphestTaskStatus::getTaskStatusName($value); + $status_icon = ManiphestTaskStatus::getStatusIcon($value); + $status_color = ManiphestTaskStatus::getStatusColor($value); + + $content = pht( + 'Change status to %s.', + phutil_tag('strong', array(), $status_name)); + + return array( + $this->newEffect() + ->setIcon($status_icon) + ->setColor($status_color) + ->addCondition('status', '!=', $value) + ->setContent($content), + ); + } + + protected function getDefaultValue() { + return ManiphestTaskStatus::getDefaultClosedStatus(); + } + + protected function getPHUIXControlType() { + return 'select'; + } + + protected function getPHUIXControlSpecification() { + $map = ManiphestTaskStatus::getTaskStatusMap(); + + return array( + 'options' => $map, + 'order' => array_keys($map), + ); + } + + public function getRuleViewLabel() { + return pht('Change Status'); + } + + public function getRuleViewDescription($value) { + $status_name = ManiphestTaskStatus::getTaskStatusName($value); + + return pht( + 'Change task status to %s.', + phutil_tag('strong', array(), $status_name)); + } + + public function getRuleViewIcon($value) { + $status_icon = ManiphestTaskStatus::getStatusIcon($value); + $status_color = ManiphestTaskStatus::getStatusColor($value); + + return id(new PHUIIconView()) + ->setIcon($status_icon, $status_color); + } + + +} diff --git a/src/applications/roles/trigger/PhabricatorRoleTriggerPlaySoundRule.php b/src/applications/roles/trigger/PhabricatorRoleTriggerPlaySoundRule.php new file mode 100644 index 0000000000..6a30b80578 --- /dev/null +++ b/src/applications/roles/trigger/PhabricatorRoleTriggerPlaySoundRule.php @@ -0,0 +1,123 @@ +newEffect() + ->setIcon($sound_icon) + ->setColor($sound_color) + ->setContent($content), + ); + } + + protected function getDefaultValue() { + return head_key(self::getSoundMap()); + } + + protected function getPHUIXControlType() { + return 'select'; + } + + protected function getPHUIXControlSpecification() { + $map = self::getSoundMap(); + $map = ipull($map, 'name'); + + return array( + 'options' => $map, + 'order' => array_keys($map), + ); + } + + public function getRuleViewLabel() { + return pht('Play Sound'); + } + + public function getRuleViewDescription($value) { + $sound_name = self::getSoundName($value); + + return pht( + 'Play sound %s.', + phutil_tag('strong', array(), $sound_name)); + } + + public function getRuleViewIcon($value) { + $sound_icon = 'fa-volume-up'; + $sound_color = 'blue'; + + return id(new PHUIIconView()) + ->setIcon($sound_icon, $sound_color); + } + + private static function getSoundName($value) { + $map = self::getSoundMap(); + $spec = idx($map, $value, array()); + return idx($spec, 'name', $value); + } + + private static function getSoundMap() { + return array( + 'bing' => array( + 'name' => pht('Bing'), + 'uri' => celerity_get_resource_uri('/rsrc/audio/basic/bing.mp3'), + ), + 'glass' => array( + 'name' => pht('Glass'), + 'uri' => celerity_get_resource_uri('/rsrc/audio/basic/ting.mp3'), + ), + ); + } + + public function getSoundEffects() { + $value = $this->getValue(); + + $map = self::getSoundMap(); + $spec = idx($map, $value, array()); + + $uris = array(); + if (isset($spec['uri'])) { + $uris[] = $spec['uri']; + } + + return $uris; + } + +} diff --git a/src/applications/roles/trigger/PhabricatorRoleTriggerRemoveProjectsRule.php b/src/applications/roles/trigger/PhabricatorRoleTriggerRemoveProjectsRule.php new file mode 100644 index 0000000000..06d92324c4 --- /dev/null +++ b/src/applications/roles/trigger/PhabricatorRoleTriggerRemoveProjectsRule.php @@ -0,0 +1,111 @@ +getDatasource()->getWireTokens($this->getValue()); + } + + protected function assertValidRuleRecordFormat($value) { + if (!is_array($value)) { + throw new Exception( + pht( + 'Remove role rule value should be a list, but is not '. + '(value is "%s").', + phutil_describe_type($value))); + } + } + + protected function assertValidRuleRecordValue($value) { + if (!$value) { + throw new Exception( + pht( + 'You must select at least one role tag to remove.')); + } + } + + protected function newDropTransactions($object, $value) { + $role_edge_type = PhabricatorRoleObjectHasRoleEdgeType::EDGECONST; + + $xaction = $object->getApplicationTransactionTemplate() + ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) + ->setMetadataValue('edge:type', $role_edge_type) + ->setNewValue( + array( + '-' => array_fuse($value), + )); + + return array($xaction); + } + + protected function newDropEffects($value) { + return array( + $this->newEffect() + ->setIcon('fa-briefcase', 'red') + ->setContent($this->getRuleViewDescription($value)), + ); + } + + protected function getDefaultValue() { + return null; + } + + protected function getPHUIXControlType() { + return 'tokenizer'; + } + + private function getDatasource() { + return id(new PhabricatorRoleDatasource()) + ->setViewer($this->getViewer()); + } + + protected function getPHUIXControlSpecification() { + $template = id(new AphrontTokenizerTemplateView()) + ->setViewer($this->getViewer()); + + $template_markup = $template->render(); + $datasource = $this->getDatasource(); + + return array( + 'markup' => (string)hsprintf('%s', $template_markup), + 'config' => array( + 'src' => $datasource->getDatasourceURI(), + 'browseURI' => $datasource->getBrowseURI(), + 'placeholder' => $datasource->getPlaceholderText(), + 'limit' => $datasource->getLimit(), + ), + 'value' => null, + ); + } + + public function getRuleViewLabel() { + return pht('Remove Role Tags'); + } + + public function getRuleViewDescription($value) { + return pht( + 'Remove role tags: %s.', + phutil_tag( + 'strong', + array(), + $this->getViewer() + ->renderHandleList($value) + ->setAsInline(true) + ->render())); + } + + public function getRuleViewIcon($value) { + return id(new PHUIIconView()) + ->setIcon('fa-briefcase', 'red'); + } + + + +} diff --git a/src/applications/roles/trigger/PhabricatorRoleTriggerRule.php b/src/applications/roles/trigger/PhabricatorRoleTriggerRule.php new file mode 100644 index 0000000000..f466c49dda --- /dev/null +++ b/src/applications/roles/trigger/PhabricatorRoleTriggerRule.php @@ -0,0 +1,172 @@ +getPhobjectClassConstant('TRIGGERTYPE', 64); + } + + final public static function getAllTriggerRules() { + return id(new PhutilClassMapQuery()) + ->setAncestorClass(__CLASS__) + ->setUniqueMethod('getTriggerType') + ->execute(); + } + + final public function setRecord(PhabricatorRoleTriggerRuleRecord $record) { + $value = $record->getValue(); + + $this->assertValidRuleRecordFormat($value); + + $this->record = $record; + return $this; + } + + final public function getRecord() { + return $this->record; + } + + final protected function getValue() { + return $this->getRecord()->getValue(); + } + + protected function getValueForEditorField() { + return $this->getValue(); + } + + abstract public function getSelectControlName(); + abstract public function getRuleViewLabel(); + abstract public function getRuleViewDescription($value); + abstract public function getRuleViewIcon($value); + abstract protected function assertValidRuleRecordFormat($value); + + final public function getRuleRecordValueValidationException() { + try { + $this->assertValidRuleRecordValue($this->getRecord()->getValue()); + } catch (Exception $ex) { + return $ex; + } + + return null; + } + + protected function assertValidRuleRecordValue($value) { + return; + } + + abstract protected function newDropTransactions($object, $value); + abstract protected function newDropEffects($value); + abstract protected function getDefaultValue(); + abstract protected function getPHUIXControlType(); + abstract protected function getPHUIXControlSpecification(); + + protected function isSelectableRule() { + return true; + } + + protected function isValidRule() { + return true; + } + + protected function newInvalidView() { + return null; + } + + public function getSoundEffects() { + return array(); + } + + final public function getDropTransactions($object, $value) { + return $this->newDropTransactions($object, $value); + } + + final public function setViewer(PhabricatorUser $viewer) { + $this->viewer = $viewer; + return $this; + } + + final public function getViewer() { + return $this->viewer; + } + + final public function setColumn(PhabricatorRoleColumn $column) { + $this->column = $column; + return $this; + } + + final public function getColumn() { + return $this->column; + } + + final public function setTrigger(PhabricatorRoleTrigger $trigger) { + $this->trigger = $trigger; + return $this; + } + + final public function getTrigger() { + return $this->trigger; + } + + final public function setObject( + PhabricatorApplicationTransactionInterface $object) { + $this->object = $object; + return $this; + } + + final public function getObject() { + return $this->object; + } + + final protected function newTransaction() { + return $this->getObject()->getApplicationTransactionTemplate(); + } + + final public function getDropEffects() { + return $this->newDropEffects($this->getValue()); + } + + final protected function newEffect() { + return id(new PhabricatorRoleDropEffect()) + ->setIsTriggerEffect(true); + } + + final public function toDictionary() { + $record = $this->getRecord(); + + $is_valid = $this->isValidRule(); + if (!$is_valid) { + $invalid_view = hsprintf('%s', $this->newInvalidView()); + } else { + $invalid_view = null; + } + + return array( + 'type' => $record->getType(), + 'value' => $this->getValueForEditorField(), + 'isValidRule' => $is_valid, + 'invalidView' => $invalid_view, + ); + } + + final public function newTemplate() { + return array( + 'type' => $this->getTriggerType(), + 'name' => $this->getSelectControlName(), + 'selectable' => $this->isSelectableRule(), + 'defaultValue' => $this->getDefaultValue(), + 'control' => array( + 'type' => $this->getPHUIXControlType(), + 'specification' => $this->getPHUIXControlSpecification(), + ), + ); + } + + +} diff --git a/src/applications/roles/trigger/PhabricatorRoleTriggerRuleRecord.php b/src/applications/roles/trigger/PhabricatorRoleTriggerRuleRecord.php new file mode 100644 index 0000000000..9b27add0f7 --- /dev/null +++ b/src/applications/roles/trigger/PhabricatorRoleTriggerRuleRecord.php @@ -0,0 +1,27 @@ +type = $type; + return $this; + } + + public function getType() { + return $this->type; + } + + public function setValue($value) { + $this->value = $value; + return $this; + } + + public function getValue() { + return $this->value; + } + +} diff --git a/src/applications/roles/trigger/PhabricatorRoleTriggerUnknownRule.php b/src/applications/roles/trigger/PhabricatorRoleTriggerUnknownRule.php new file mode 100644 index 0000000000..1056b4e827 --- /dev/null +++ b/src/applications/roles/trigger/PhabricatorRoleTriggerUnknownRule.php @@ -0,0 +1,71 @@ +setIcon('fa-exclamation-triangle yellow'), + ' ', + pht( + 'This is a trigger rule with a unknown type ("%s").', + $this->getRecord()->getType()), + ); + } + + protected function getDefaultValue() { + return null; + } + + protected function getPHUIXControlType() { + return null; + } + + protected function getPHUIXControlSpecification() { + return null; + } + + public function getRuleViewLabel() { + return pht('Unknown Rule'); + } + + public function getRuleViewDescription($value) { + return pht( + 'This is an unknown rule of type "%s". An administrator may have '. + 'edited or removed an extension which implements this rule type.', + $this->getRecord()->getType()); + } + + public function getRuleViewIcon($value) { + return id(new PHUIIconView()) + ->setIcon('fa-question-circle', 'yellow'); + } + +} diff --git a/src/applications/roles/typeahead/PhabricatorRoleDatasource.php b/src/applications/roles/typeahead/PhabricatorRoleDatasource.php new file mode 100644 index 0000000000..53b0bd0701 --- /dev/null +++ b/src/applications/roles/typeahead/PhabricatorRoleDatasource.php @@ -0,0 +1,157 @@ +getViewer(); + + $raw_query = $this->getRawQuery(); + + // Allow users to type "#qa" or "qa" to find "Quality Assurance". + $raw_query = ltrim($raw_query, '#'); + $tokens = self::tokenizeString($raw_query); + + $query = id(new PhabricatorRoleQuery()) + ->needImages(true) + ->needSlugs(true) + ->setOrderVector(array('-status', 'id')); + + if ($this->getPhase() == self::PHASE_PREFIX) { + $prefix = $this->getPrefixQuery(); + $query->withNamePrefixes(array($prefix)); + } else if ($tokens) { + $query->withNameTokens($tokens); + } + + // If this is for policy selection, prevent users from using milestones. + $for_policy = $this->getParameter('policy'); + if ($for_policy) { + $query->withIsMilestone(false); + } + + $for_autocomplete = $this->getParameter('autocomplete'); + + $roles = $this->executeQuery($query); + + $roles = mpull($roles, null, 'getPHID'); + + $must_have_cols = $this->getParameter('mustHaveColumns', false); + if ($must_have_cols) { + $columns = id(new PhabricatorRoleColumnQuery()) + ->setViewer($viewer) + ->withRolePHIDs(array_keys($roles)) + ->withIsProxyColumn(false) + ->execute(); + $has_cols = mgroup($columns, 'getRolePHID'); + } else { + $has_cols = array_fill_keys(array_keys($roles), true); + } + + $is_browse = $this->getIsBrowse(); + if ($is_browse && $roles) { + // TODO: This is a little ad-hoc, but we don't currently have + // infrastructure for bulk querying custom fields efficiently. + $table = new PhabricatorRoleCustomFieldStorage(); + $descriptions = $table->loadAllWhere( + 'objectPHID IN (%Ls) AND fieldIndex = %s', + array_keys($roles), + PhabricatorHash::digestForIndex('std:role:internal:description')); + $descriptions = mpull($descriptions, 'getFieldValue', 'getObjectPHID'); + } else { + $descriptions = array(); + } + + $results = array(); + foreach ($roles as $role) { + $phid = $role->getPHID(); + + if (!isset($has_cols[$phid])) { + continue; + } + + $slug = $role->getPrimarySlug(); + if ($slug === null || $slug === '') { + foreach ($role->getSlugs() as $slug_object) { + $slug = $slug_object->getSlug(); + if ($slug !== null && $slug !== '') { + break; + } + } + } + + // If we're building results for the autocompleter and this role + // doesn't have any usable slugs, don't return it as a result. + if ($for_autocomplete && !strlen($slug)) { + continue; + } + + $closed = null; + if ($role->isArchived()) { + $closed = pht('Archived'); + } + + $all_strings = array(); + + // NOTE: We list the role's name first because results will be + // sorted into prefix vs content phases incorrectly if we don't: it + // will look like "Parent (Milestone)" matched "Parent" as a prefix, + // but it did not. + $all_strings[] = $role->getName(); + + if ($role->isMilestone()) { + $all_strings[] = $role->getParentRole()->getName(); + } + + foreach ($role->getSlugs() as $role_slug) { + $all_strings[] = $role_slug->getSlug(); + } + + $all_strings = implode("\n", $all_strings); + + $role_result = id(new PhabricatorTypeaheadResult()) + ->setName($all_strings) + ->setDisplayName($role->getDisplayName()) + ->setDisplayType($role->getDisplayIconName()) + ->setURI($role->getURI()) + ->setPHID($phid) + ->setIcon($role->getDisplayIconIcon()) + ->setColor($role->getColor()) + ->setPriorityType('role') + ->setClosed($closed); + + if ($slug !== null && $slug !== '') { + $role_result->setAutocomplete('#'.$slug); + } + + $role_result->setImageURI($role->getProfileImageURI()); + + if ($is_browse) { + $role_result->addAttribute($role->getDisplayIconName()); + + $description = idx($descriptions, $phid); + if ($description !== null && $description !== '') { + $summary = PhabricatorMarkupEngine::summarizeSentence($description); + $role_result->addAttribute($summary); + } + } + + $results[] = $role_result; + } + + return $results; + } + +} diff --git a/src/applications/roles/typeahead/PhabricatorRoleLogicalAncestorDatasource.php b/src/applications/roles/typeahead/PhabricatorRoleLogicalAncestorDatasource.php new file mode 100644 index 0000000000..f60e386770 --- /dev/null +++ b/src/applications/roles/typeahead/PhabricatorRoleLogicalAncestorDatasource.php @@ -0,0 +1,95 @@ +getViewer(); + + $all_roles = id(new PhabricatorRoleQuery()) + ->setViewer($viewer) + ->withAncestorRolePHIDs($phids) + ->execute(); + + foreach ($phids as $phid) { + $map[$phid][] = $phid; + } + + foreach ($all_roles as $role) { + $role_phid = $role->getPHID(); + $map[$role_phid][] = $role_phid; + foreach ($role->getAncestorRoles() as $ancestor) { + $ancestor_phid = $ancestor->getPHID(); + + if (isset($phids[$role_phid]) && isset($phids[$ancestor_phid])) { + // This is a descendant of some other role in the query, so + // we don't need to query for that role. This happens if a user + // runs a query for both "Engineering" and "Engineering > Warp + // Drive". We can only ever match the "Warp Drive" results, so + // we do not need to add the weaker "Engineering" constraint. + $skip[$ancestor_phid] = true; + } + + $map[$ancestor_phid][] = $role_phid; + } + } + } + + foreach ($results as $key => $result) { + if (!is_string($result)) { + continue; + } + + if (empty($map[$result])) { + continue; + } + + // This constraint is implied by another, stronger constraint. + if (isset($skip[$result])) { + unset($results[$key]); + continue; + } + + // If we have duplicates, don't apply the second constraint. + $skip[$result] = true; + + $results[$key] = new PhabricatorQueryConstraint( + PhabricatorQueryConstraint::OPERATOR_ANCESTOR, + $map[$result]); + } + + return $results; + } + +} diff --git a/src/applications/roles/typeahead/PhabricatorRoleLogicalDatasource.php b/src/applications/roles/typeahead/PhabricatorRoleLogicalDatasource.php new file mode 100644 index 0000000000..0059801f4e --- /dev/null +++ b/src/applications/roles/typeahead/PhabricatorRoleLogicalDatasource.php @@ -0,0 +1,29 @@ + array( + 'name' => pht('Only Match Other Constraints'), + 'summary' => pht( + 'Find results with only the specified tags.'), + 'description' => pht( + "This function is used with other tags, and causes the query to ". + "match only results with exactly those tags. For example, to find ". + "tasks tagged only iOS:". + "\n\n". + "> ios, only()". + "\n\n". + "This will omit results with any other role tag."), + ), + ); + } + + public function loadResults() { + $results = array( + $this->renderOnlyFunctionToken(), + ); + return $this->filterResultsAgainstTokens($results); + } + + protected function evaluateFunction($function, array $argv_list) { + $results = array(); + + $results[] = new PhabricatorQueryConstraint( + PhabricatorQueryConstraint::OPERATOR_ONLY, + null); + + return $results; + } + + public function renderFunctionTokens( + $function, + array $argv_list) { + + $tokens = array(); + foreach ($argv_list as $argv) { + $tokens[] = PhabricatorTypeaheadTokenView::newFromTypeaheadResult( + $this->renderOnlyFunctionToken()); + } + + return $tokens; + } + + private function renderOnlyFunctionToken() { + return $this->newFunctionResult() + ->setName(pht('Only')) + ->setPHID('only()') + ->setIcon('fa-asterisk') + ->setUnique(true) + ->addAttribute( + pht('Select only results with exactly the other specified tags.')); + } + +} diff --git a/src/applications/roles/typeahead/PhabricatorRoleLogicalOrNotDatasource.php b/src/applications/roles/typeahead/PhabricatorRoleLogicalOrNotDatasource.php new file mode 100644 index 0000000000..bcf64ad7c0 --- /dev/null +++ b/src/applications/roles/typeahead/PhabricatorRoleLogicalOrNotDatasource.php @@ -0,0 +1,169 @@ +) or not()...'); + } + + public function getDatasourceApplicationClass() { + return 'PhabricatorRoleApplication'; + } + + public function getComponentDatasources() { + return array( + new PhabricatorRoleDatasource(), + ); + } + + public function getDatasourceFunctions() { + return array( + 'any' => array( + 'name' => pht('In Any: ...'), + 'arguments' => pht('role'), + 'summary' => pht('Find results in any of several roles.'), + 'description' => pht( + 'This function allows you to find results in one of several '. + 'roles. Another way to think of this function is that it '. + 'allows you to perform an "or" query.'. + "\n\n". + 'By default, if you enter several roles, results are returned '. + 'only if they belong to all of the roles you enter. That is, '. + 'this query will only return results in //both// roles:'. + "\n\n". + '> ios, android'. + "\n\n". + 'If you want to find results in any of several roles, you can '. + 'use the `any()` function. For example, you can use this query to '. + 'find results which are in //either// role:'. + "\n\n". + '> any(ios), any(android)'. + "\n\n". + 'You can combine the `any()` function with normal role tokens '. + 'to refine results. For example, use this query to find bugs in '. + '//either// iOS or Android:'. + "\n\n". + '> bug, any(ios), any(android)'), + ), + 'not' => array( + 'name' => pht('Not In: ...'), + 'arguments' => pht('role'), + 'summary' => pht('Find results not in specific roles.'), + 'description' => pht( + 'This function allows you to find results which are not in '. + 'one or more roles. For example, use this query to find '. + 'results which are not associated with a specific role:'. + "\n\n". + '> not(vanilla)'. + "\n\n". + 'You can exclude multiple roles. This will cause the query '. + 'to return only results which are not in any of the excluded '. + 'roles:'. + "\n\n". + '> not(vanilla), not(chocolate)'. + "\n\n". + 'You can combine this function with other functions to refine '. + 'results. For example, use this query to find iOS results which '. + 'are not bugs:'. + "\n\n". + '> ios, not(bug)'), + ), + ); + } + + protected function didLoadResults(array $results) { + $function = $this->getCurrentFunction(); + $return_any = ($function !== 'not'); + $return_not = ($function !== 'any'); + + $return = array(); + foreach ($results as $result) { + $result + ->setTokenType(PhabricatorTypeaheadTokenView::TYPE_FUNCTION) + ->setIcon('fa-asterisk') + ->setColor(null) + ->resetAttributes() + ->addAttribute(pht('Function')); + + if ($return_any) { + $return[] = id(clone $result) + ->setPHID('any('.$result->getPHID().')') + ->setDisplayName(pht('In Any: %s', $result->getDisplayName())) + ->setName('any '.$result->getName()) + ->addAttribute(pht('Include results tagged with this role.')); + } + + if ($return_not) { + $return[] = id(clone $result) + ->setPHID('not('.$result->getPHID().')') + ->setDisplayName(pht('Not In: %s', $result->getDisplayName())) + ->setName('not '.$result->getName()) + ->addAttribute(pht('Exclude results tagged with this role.')); + } + } + + return $return; + } + + protected function evaluateFunction($function, array $argv_list) { + $phids = array(); + foreach ($argv_list as $argv) { + $phids[] = head($argv); + } + + $operator = array( + 'any' => PhabricatorQueryConstraint::OPERATOR_OR, + 'not' => PhabricatorQueryConstraint::OPERATOR_NOT, + ); + + $results = array(); + foreach ($phids as $phid) { + $results[] = new PhabricatorQueryConstraint( + $operator[$function], + $phid); + } + + return $results; + } + + public function renderFunctionTokens($function, array $argv_list) { + $phids = array(); + foreach ($argv_list as $argv) { + $phids[] = head($argv); + } + + $tokens = $this->renderTokens($phids); + foreach ($tokens as $token) { + $token->setColor(null); + if ($token->isInvalid()) { + if ($function == 'any') { + $token->setValue(pht('In Any: Invalid Role')); + } else { + $token->setValue(pht('Not In: Invalid Role')); + } + } else { + $token + ->setIcon('fa-asterisk') + ->setTokenType(PhabricatorTypeaheadTokenView::TYPE_FUNCTION); + + if ($function == 'any') { + $token + ->setKey('any('.$token->getKey().')') + ->setValue(pht('In Any: %s', $token->getValue())); + } else { + $token + ->setKey('not('.$token->getKey().')') + ->setValue(pht('Not In: %s', $token->getValue())); + } + } + } + + return $tokens; + } + +} diff --git a/src/applications/roles/typeahead/PhabricatorRoleLogicalUserDatasource.php b/src/applications/roles/typeahead/PhabricatorRoleLogicalUserDatasource.php new file mode 100644 index 0000000000..beede77552 --- /dev/null +++ b/src/applications/roles/typeahead/PhabricatorRoleLogicalUserDatasource.php @@ -0,0 +1,138 @@ +)...'); + } + + public function getDatasourceApplicationClass() { + return 'PhabricatorRoleApplication'; + } + + public function getComponentDatasources() { + return array( + new PhabricatorPeopleDatasource(), + ); + } + + public function getDatasourceFunctions() { + return array( + 'roles' => array( + 'name' => pht('Roles: ...'), + 'arguments' => pht('username'), + 'summary' => pht("Find results in any of a user's roles."), + 'description' => pht( + "This function allows you to find results associated with any ". + "of the roles a specified user is a member of. For example, ". + "this will find results associated with all of the roles ". + "`%s` is a member of:\n\n%s\n\n", + 'alincoln', + '> roles(alincoln)'), + ), + ); + } + + protected function didLoadResults(array $results) { + foreach ($results as $result) { + $result + ->setColor(null) + ->setTokenType(PhabricatorTypeaheadTokenView::TYPE_FUNCTION) + ->setIcon('fa-asterisk') + ->setPHID('roles('.$result->getPHID().')') + ->setDisplayName(pht("User's Roles: %s", $result->getDisplayName())) + ->setName('roles '.$result->getName()); + } + + return $results; + } + + protected function evaluateFunction($function, array $argv_list) { + $phids = array(); + foreach ($argv_list as $argv) { + $phids[] = head($argv); + } + + $phids = $this->resolvePHIDs($phids); + + $roles = id(new PhabricatorRoleQuery()) + ->setViewer($this->getViewer()) + ->withMemberPHIDs($phids) + ->execute(); + + $results = array(); + foreach ($roles as $role) { + $results[] = new PhabricatorQueryConstraint( + PhabricatorQueryConstraint::OPERATOR_OR, + $role->getPHID()); + } + + return $results; + } + + public function renderFunctionTokens($function, array $argv_list) { + $phids = array(); + foreach ($argv_list as $argv) { + $phids[] = head($argv); + } + + $phids = $this->resolvePHIDs($phids); + + $tokens = $this->renderTokens($phids); + foreach ($tokens as $token) { + $token->setColor(null); + if ($token->isInvalid()) { + $token + ->setValue(pht("User's Roles: Invalid User")); + } else { + $token + ->setIcon('fa-asterisk') + ->setTokenType(PhabricatorTypeaheadTokenView::TYPE_FUNCTION) + ->setKey('roles('.$token->getKey().')') + ->setValue(pht("User's Roles: %s", $token->getValue())); + } + } + + return $tokens; + } + + private function resolvePHIDs(array $phids) { + // If we have a function like `roles(alincoln)`, try to resolve the + // username first. This won't happen normally, but can be passed in from + // the query string. + + // The user might also give us an invalid username. In this case, we + // preserve it and return it in-place so we get an "invalid" token rendered + // in the UI. This shows the user where the issue is and best represents + // the user's input. + + $usernames = array(); + foreach ($phids as $key => $phid) { + if (phid_get_type($phid) != PhabricatorPeopleUserPHIDType::TYPECONST) { + $usernames[$key] = $phid; + } + } + + if ($usernames) { + $users = id(new PhabricatorPeopleQuery()) + ->setViewer($this->getViewer()) + ->withUsernames($usernames) + ->execute(); + $users = mpull($users, null, 'getUsername'); + foreach ($usernames as $key => $username) { + $user = idx($users, $username); + if ($user) { + $phids[$key] = $user->getPHID(); + } + } + } + + return $phids; + } + +} diff --git a/src/applications/roles/typeahead/PhabricatorRoleLogicalViewerDatasource.php b/src/applications/roles/typeahead/PhabricatorRoleLogicalViewerDatasource.php new file mode 100644 index 0000000000..ff050f7e15 --- /dev/null +++ b/src/applications/roles/typeahead/PhabricatorRoleLogicalViewerDatasource.php @@ -0,0 +1,103 @@ + array( + 'name' => pht("Current Viewer's Roles"), + 'summary' => pht( + "Find results in any of the current viewer's roles."), + 'description' => pht( + "This function matches results in any of the current viewing ". + "user's roles:". + "\n\n". + "> viewerroles()". + "\n\n". + "This normally means //your// roles, but if you save a query ". + "using this function and send it to someone else, it will mean ". + "//their// roles when they run it (they become the current ". + "viewer). This can be useful for building dashboard panels."), + ), + ); + } + + public function loadResults() { + if ($this->getViewer()->getPHID()) { + $results = array($this->renderViewerRolesFunctionToken()); + } else { + $results = array(); + } + + return $this->filterResultsAgainstTokens($results); + } + + protected function canEvaluateFunction($function) { + if (!$this->getViewer()->getPHID()) { + return false; + } + + return parent::canEvaluateFunction($function); + } + + protected function evaluateFunction($function, array $argv_list) { + $viewer = $this->getViewer(); + + $roles = id(new PhabricatorRoleQuery()) + ->setViewer($viewer) + ->withMemberPHIDs(array($viewer->getPHID())) + ->execute(); + $phids = mpull($roles, 'getPHID'); + + $results = array(); + if ($phids) { + foreach ($phids as $phid) { + $results[] = new PhabricatorQueryConstraint( + PhabricatorQueryConstraint::OPERATOR_OR, + $phid); + } + } else { + $results[] = new PhabricatorQueryConstraint( + PhabricatorQueryConstraint::OPERATOR_EMPTY, + null); + } + + return $results; + } + + public function renderFunctionTokens( + $function, + array $argv_list) { + + $tokens = array(); + foreach ($argv_list as $argv) { + $tokens[] = PhabricatorTypeaheadTokenView::newFromTypeaheadResult( + $this->renderViewerRolesFunctionToken()); + } + + return $tokens; + } + + private function renderViewerRolesFunctionToken() { + return $this->newFunctionResult() + ->setName(pht('Current Viewer\'s Roles')) + ->setPHID('viewerroles()') + ->setIcon('fa-asterisk') + ->setUnique(true) + ->addAttribute(pht('Select roles current viewer is a member of.')); + } + +} diff --git a/src/applications/roles/typeahead/PhabricatorRoleMembersDatasource.php b/src/applications/roles/typeahead/PhabricatorRoleMembersDatasource.php new file mode 100644 index 0000000000..73b11e52a3 --- /dev/null +++ b/src/applications/roles/typeahead/PhabricatorRoleMembersDatasource.php @@ -0,0 +1,104 @@ +)...'); + } + + public function getDatasourceApplicationClass() { + return 'PhabricatorRoleApplication'; + } + + public function getComponentDatasources() { + return array( + new PhabricatorRoleDatasource(), + ); + } + + public function getDatasourceFunctions() { + return array( + 'members' => array( + 'name' => pht('Members: ...'), + 'arguments' => pht('role'), + 'summary' => pht('Find results for members of a role.'), + 'description' => pht( + 'This function allows you to find results for any of the members '. + 'of a role:'. + "\n\n". + '> members(frontend)'), + ), + ); + } + + protected function didLoadResults(array $results) { + foreach ($results as $result) { + $result + ->setTokenType(PhabricatorTypeaheadTokenView::TYPE_FUNCTION) + ->setIcon('fa-users') + ->setColor(null) + ->setPHID('members('.$result->getPHID().')') + ->setDisplayName(pht('Members: %s', $result->getDisplayName())) + ->setName($result->getName().' members') + ->resetAttributes() + ->addAttribute(pht('Function')) + ->addAttribute(pht('Select role members.')); + } + + return $results; + } + + protected function evaluateFunction($function, array $argv_list) { + $phids = array(); + foreach ($argv_list as $argv) { + $phids[] = head($argv); + } + + $roles = id(new PhabricatorRoleQuery()) + ->setViewer($this->getViewer()) + ->needMembers(true) + ->withPHIDs($phids) + ->execute(); + + $results = array(); + foreach ($roles as $role) { + foreach ($role->getMemberPHIDs() as $phid) { + $results[$phid] = $phid; + } + } + + return array_values($results); + } + + public function renderFunctionTokens($function, array $argv_list) { + $phids = array(); + foreach ($argv_list as $argv) { + $phids[] = head($argv); + } + + $tokens = $this->renderTokens($phids); + foreach ($tokens as $token) { + // Remove any role color on this token. + $token->setColor(null); + + if ($token->isInvalid()) { + $token + ->setValue(pht('Members: Invalid Role')); + } else { + $token + ->setIcon('fa-users') + ->setTokenType(PhabricatorTypeaheadTokenView::TYPE_FUNCTION) + ->setKey('members('.$token->getKey().')') + ->setValue(pht('Members: %s', $token->getValue())); + } + } + + return $tokens; + } + +} diff --git a/src/applications/roles/typeahead/PhabricatorRoleNoRolesDatasource.php b/src/applications/roles/typeahead/PhabricatorRoleNoRolesDatasource.php new file mode 100644 index 0000000000..7f27efca3f --- /dev/null +++ b/src/applications/roles/typeahead/PhabricatorRoleNoRolesDatasource.php @@ -0,0 +1,75 @@ + array( + 'name' => pht('Not Tagged With Any Roles'), + 'summary' => pht( + 'Find results which are not tagged with any roles.'), + 'description' => pht( + "This function matches results which are not tagged with any ". + "roles. It is usually most often used to find objects which ". + "might have slipped through the cracks and not been organized ". + "properly.\n\n%s", + '> null()'), + ), + ); + } + + public function loadResults() { + $results = array( + $this->buildNullResult(), + ); + + return $this->filterResultsAgainstTokens($results); + } + + protected function evaluateFunction($function, array $argv_list) { + $results = array(); + + foreach ($argv_list as $argv) { + $results[] = new PhabricatorQueryConstraint( + PhabricatorQueryConstraint::OPERATOR_NULL, + 'empty'); + } + + return $results; + } + + public function renderFunctionTokens($function, array $argv_list) { + $results = array(); + foreach ($argv_list as $argv) { + $results[] = PhabricatorTypeaheadTokenView::newFromTypeaheadResult( + $this->buildNullResult()); + } + return $results; + } + + private function buildNullResult() { + $name = pht('Not Tagged With Any Roles'); + + return $this->newFunctionResult() + ->setUnique(true) + ->setPHID('null()') + ->setIcon('fa-ban') + ->setName('null '.$name) + ->setDisplayName($name) + ->addAttribute(pht('Select results with no tags.')); + } + +} diff --git a/src/applications/roles/typeahead/PhabricatorRoleOrUserDatasource.php b/src/applications/roles/typeahead/PhabricatorRoleOrUserDatasource.php new file mode 100644 index 0000000000..687b3d30c1 --- /dev/null +++ b/src/applications/roles/typeahead/PhabricatorRoleOrUserDatasource.php @@ -0,0 +1,21 @@ +buildResults(); + return $this->filterResultsAgainstTokens($results); + } + + protected function renderSpecialTokens(array $values) { + return $this->renderTokensFromResults($this->buildResults(), $values); + } + + private function buildResults() { + $results = array(); + + $subtype_map = id(new PhabricatorRole())->newEditEngineSubtypeMap(); + foreach ($subtype_map->getSubtypes() as $key => $subtype) { + + $result = id(new PhabricatorTypeaheadResult()) + ->setIcon($subtype->getIcon()) + ->setColor($subtype->getColor()) + ->setPHID($key) + ->setName($subtype->getName()); + + $results[$key] = $result; + } + + return $results; + } + +} diff --git a/src/applications/roles/typeahead/PhabricatorRoleUserFunctionDatasource.php b/src/applications/roles/typeahead/PhabricatorRoleUserFunctionDatasource.php new file mode 100644 index 0000000000..a613daa5d5 --- /dev/null +++ b/src/applications/roles/typeahead/PhabricatorRoleUserFunctionDatasource.php @@ -0,0 +1,36 @@ +)...'); + } + + public function getDatasourceApplicationClass() { + return 'PhabricatorRoleApplication'; + } + + public function getComponentDatasources() { + return array( + new PhabricatorRoleLogicalUserDatasource(), + ); + } + + protected function evaluateFunction($function, array $argv_list) { + $result = parent::evaluateFunction($function, $argv_list); + + foreach ($result as $k => $v) { + if ($v instanceof PhabricatorQueryConstraint) { + $result[$k] = $v->getValue(); + } + } + + return $result; + } + +} diff --git a/src/applications/roles/view/PhabricatorRoleCardView.php b/src/applications/roles/view/PhabricatorRoleCardView.php new file mode 100644 index 0000000000..e10041ca31 --- /dev/null +++ b/src/applications/roles/view/PhabricatorRoleCardView.php @@ -0,0 +1,84 @@ +role = $role; + return $this; + } + + public function setViewer(PhabricatorUser $viewer) { + $this->viewer = $viewer; + return $this; + } + + public function setTag($tag) { + $this->tag = $tag; + return $this; + } + + protected function getTagName() { + if ($this->tag) { + return $this->tag; + } + return 'div'; + } + + protected function getTagAttributes() { + $classes = array(); + $classes[] = 'project-card-view'; + + $color = $this->role->getColor(); + $classes[] = 'project-card-'.$color; + + return array( + 'class' => implode(' ', $classes), + ); + } + + protected function getTagContent() { + + $role = $this->role; + $viewer = $this->viewer; + require_celerity_resource('project-card-view-css'); + + $icon = $role->getDisplayIconIcon(); + $icon_name = $role->getDisplayIconName(); + $tag = id(new PHUITagView()) + ->setIcon($icon) + ->setName($icon_name) + ->addClass('project-view-header-tag') + ->setType(PHUITagView::TYPE_SHADE); + + $header = id(new PHUIHeaderView()) + ->setHeader(array($role->getDisplayName(), $tag)) + ->setUser($viewer) + ->setPolicyObject($role) + ->setImage($role->getProfileImageURI()); + + if ($role->getStatus() == PhabricatorRoleStatus::STATUS_ACTIVE) { + $header->setStatus('fa-check', 'bluegrey', pht('Active')); + } else { + $header->setStatus('fa-ban', 'red', pht('Archived')); + } + + $description = null; + + $card = phutil_tag( + 'div', + array( + 'class' => 'project-card-inner', + ), + array( + $header, + $description, + )); + + return $card; + } + +} diff --git a/src/applications/roles/view/PhabricatorRoleListView.php b/src/applications/roles/view/PhabricatorRoleListView.php new file mode 100644 index 0000000000..8e20cbfbb8 --- /dev/null +++ b/src/applications/roles/view/PhabricatorRoleListView.php @@ -0,0 +1,107 @@ +roles = $roles; + return $this; + } + + public function getRoles() { + return $this->roles; + } + + public function setShowWatching($watching) { + $this->showWatching = $watching; + return $this; + } + + public function setShowMember($member) { + $this->showMember = $member; + return $this; + } + + public function setNoDataString($text) { + $this->noDataString = $text; + return $this; + } + + public function renderList() { + $viewer = $this->getUser(); + $viewer_phid = $viewer->getPHID(); + $roles = $this->getRoles(); + + $handles = $viewer->loadHandles(mpull($roles, 'getPHID')); + + $no_data = pht('No roles found.'); + if ($this->noDataString) { + $no_data = $this->noDataString; + } + + $list = id(new PHUIObjectItemListView()) + ->setUser($viewer) + ->setNoDataString($no_data); + + foreach ($roles as $key => $role) { + $id = $role->getID(); + + $icon = $role->getDisplayIconIcon(); + $icon_icon = id(new PHUIIconView()) + ->setIcon($icon); + + $icon_name = $role->getDisplayIconName(); + + $item = id(new PHUIObjectItemView()) + ->setObject($role) + ->setHeader($role->getName()) + ->setHref("/role/view/{$id}/") + ->setImageURI($role->getProfileImageURI()) + ->addAttribute( + array( + $icon_icon, + ' ', + $icon_name, + )); + + if ($role->getStatus() == PhabricatorRoleStatus::STATUS_ARCHIVED) { + $item->addIcon('fa-ban', pht('Archived')); + $item->setDisabled(true); + } + + if ($this->showMember) { + $is_member = $role->isUserMember($viewer_phid); + if ($is_member) { + $item->addIcon('fa-user', pht('Member')); + } + } + + if ($this->showWatching) { + $is_watcher = $role->isUserWatcher($viewer_phid); + if ($is_watcher) { + $item->addIcon('fa-eye', pht('Watching')); + } + } + + $subtype = $role->newSubtypeObject(); + if ($subtype && $subtype->hasTagView()) { + $subtype_tag = $subtype->newTagView() + ->setSlimShady(true); + $item->addAttribute($subtype_tag); + } + + $list->addItem($item); + } + + return $list; + } + + public function render() { + return $this->renderList(); + } + +} diff --git a/src/applications/roles/view/PhabricatorRoleMemberListView.php b/src/applications/roles/view/PhabricatorRoleMemberListView.php new file mode 100644 index 0000000000..f5fc2ee3a4 --- /dev/null +++ b/src/applications/roles/view/PhabricatorRoleMemberListView.php @@ -0,0 +1,65 @@ +getViewer(); + $role = $this->getRole(); + + if (!$role->supportsEditMembers()) { + return false; + } + + return PhabricatorPolicyFilter::hasCapability( + $viewer, + $role, + PhabricatorPolicyCapability::CAN_EDIT); + } + + protected function getNoDataString() { + return pht('This role does not have any members.'); + } + + protected function getRemoveURI($phid) { + $role = $this->getRole(); + $id = $role->getID(); + return "/role/members/{$id}/remove/?phid={$phid}"; + } + + protected function getHeaderText() { + return pht('Members'); + } + + protected function getMembershipNote() { + $viewer = $this->getViewer(); + $viewer_phid = $viewer->getPHID(); + $role = $this->getRole(); + + if (!$viewer_phid) { + return null; + } + + $note = null; + if ($role->isUserMember($viewer_phid)) { + $edge_type = PhabricatorRoleSilencedEdgeType::EDGECONST; + $silenced = PhabricatorEdgeQuery::loadDestinationPHIDs( + $role->getPHID(), + $edge_type); + $silenced = array_fuse($silenced); + $is_silenced = isset($silenced[$viewer_phid]); + if ($is_silenced) { + $note = pht( + 'You have disabled mail. When mail is sent to role members, '. + 'you will not receive a copy.'); + } else { + $note = pht( + 'You are a member and you will receive mail that is sent to all '. + 'role members.'); + } + } + + return $note; + } + +} diff --git a/src/applications/roles/view/PhabricatorRoleUserListView.php b/src/applications/roles/view/PhabricatorRoleUserListView.php new file mode 100644 index 0000000000..907dc2f64e --- /dev/null +++ b/src/applications/roles/view/PhabricatorRoleUserListView.php @@ -0,0 +1,183 @@ +role = $role; + return $this; + } + + public function getRole() { + return $this->role; + } + + public function setUserPHIDs(array $user_phids) { + $this->userPHIDs = $user_phids; + return $this; + } + + public function getUserPHIDs() { + return $this->userPHIDs; + } + + public function setLimit($limit) { + $this->limit = $limit; + return $this; + } + + public function getLimit() { + return $this->limit; + } + + public function setBackground($color) { + $this->background = $color; + return $this; + } + + public function setShowNote($show) { + $this->showNote = $show; + return $this; + } + + abstract protected function canEditList(); + abstract protected function getNoDataString(); + abstract protected function getRemoveURI($phid); + abstract protected function getHeaderText(); + abstract protected function getMembershipNote(); + + public function render() { + $viewer = $this->getViewer(); + $role = $this->getRole(); + $user_phids = $this->getUserPHIDs(); + + $can_edit = $this->canEditList(); + $supports_edit = $role->supportsEditMembers(); + $no_data = $this->getNoDataString(); + + $list = id(new PHUIObjectItemListView()) + ->setNoDataString($no_data); + + $limit = $this->getLimit(); + $is_panel = (bool)$limit; + + $handles = $viewer->loadHandles($user_phids); + + // Reorder users in display order. We're going to put the viewer first + // if they're a member, then enabled users, then disabled/invalid users. + + $phid_map = array(); + foreach ($user_phids as $user_phid) { + $handle = $handles[$user_phid]; + + $is_viewer = ($user_phid === $viewer->getPHID()); + $is_enabled = ($handle->isComplete() && !$handle->isDisabled()); + + // If we're showing the main member list, show oldest to newest. If we're + // showing only a slice in a panel, show newest to oldest. + if ($limit) { + $order_scalar = 1; + } else { + $order_scalar = -1; + } + + $phid_map[$user_phid] = id(new PhutilSortVector()) + ->addInt($is_viewer ? 0 : 1) + ->addInt($is_enabled ? 0 : 1) + ->addInt($order_scalar * count($phid_map)); + } + $phid_map = msortv($phid_map, 'getSelf'); + + $handles = iterator_to_array($handles); + $handles = array_select_keys($handles, array_keys($phid_map)); + + if ($limit) { + $handles = array_slice($handles, 0, $limit); + } + + foreach ($handles as $user_phid => $handle) { + $item = id(new PHUIObjectItemView()) + ->setHeader($handle->getFullName()) + ->setHref($handle->getURI()) + ->setImageURI($handle->getImageURI()); + + if ($handle->isDisabled()) { + if ($is_panel) { + // Don't show disabled users in the panel view at all. + continue; + } + + $item + ->setDisabled(true) + ->addAttribute(pht('Disabled')); + } else { + $icon = id(new PHUIIconView()) + ->setIcon($handle->getIcon()); + + $subtitle = $handle->getSubtitle(); + + $item->addAttribute(array($icon, ' ', $subtitle)); + } + + if ($supports_edit && !$is_panel) { + $remove_uri = $this->getRemoveURI($user_phid); + + $item->addAction( + id(new PHUIListItemView()) + ->setIcon('fa-times') + ->setName(pht('Remove')) + ->setHref($remove_uri) + ->setDisabled(!$can_edit) + ->setWorkflow(true)); + } + + $list->addItem($item); + } + + if ($user_phids) { + $header_text = pht( + '%s (%s)', + $this->getHeaderText(), + phutil_count($user_phids)); + } else { + $header_text = $this->getHeaderText(); + } + + $id = $role->getID(); + + $header = id(new PHUIHeaderView()) + ->setHeader($header_text); + + if ($limit) { + $list->newTailButton() + ->setText(pht('View All')) + ->setHref("/role/members/{$id}/"); + } + + $box = id(new PHUIObjectBoxView()) + ->setHeader($header) + ->setObjectList($list); + + if ($this->showNote) { + if ($this->getMembershipNote()) { + $info = id(new PHUIInfoView()) + ->setSeverity(PHUIInfoView::SEVERITY_PLAIN) + ->appendChild($this->getMembershipNote()); + $box->setInfoView($info); + } + } + + if ($this->background) { + $box->setBackground($this->background); + } + + return $box; + } + +} diff --git a/src/applications/roles/view/PhabricatorRoleWatcherListView.php b/src/applications/roles/view/PhabricatorRoleWatcherListView.php new file mode 100644 index 0000000000..a812c3fde3 --- /dev/null +++ b/src/applications/roles/view/PhabricatorRoleWatcherListView.php @@ -0,0 +1,43 @@ +getViewer(); + $role = $this->getRole(); + + return PhabricatorPolicyFilter::hasCapability( + $viewer, + $role, + PhabricatorPolicyCapability::CAN_EDIT); + } + + protected function getNoDataString() { + return pht('This role does not have any watchers.'); + } + + protected function getRemoveURI($phid) { + $role = $this->getRole(); + $id = $role->getID(); + return "/role/watchers/{$id}/remove/?phid={$phid}"; + } + + protected function getHeaderText() { + return pht('Watchers'); + } + + protected function getMembershipNote() { + $viewer = $this->getViewer(); + $viewer_phid = $viewer->getPHID(); + $role = $this->getRole(); + + $note = null; + if ($role->isUserWatcher($viewer_phid)) { + $note = pht('You are watching this role and will receive mail about '. + 'changes made to any related object.'); + } + return $note; + } + +} diff --git a/src/applications/roles/view/ProjectBoardTaskCard.php b/src/applications/roles/view/ProjectBoardTaskCard.php new file mode 100644 index 0000000000..4f66594edd --- /dev/null +++ b/src/applications/roles/view/ProjectBoardTaskCard.php @@ -0,0 +1,186 @@ +viewer = $viewer; + return $this; + } + public function getViewer() { + return $this->viewer; + } + + public function setRoleHandles(array $handles) { + $this->roleHandles = $handles; + return $this; + } + + public function getRoleHandles() { + return $this->roleHandles; + } + + public function setCoverImageFile(PhabricatorFile $cover_image_file) { + $this->coverImageFile = $cover_image_file; + return $this; + } + + public function getCoverImageFile() { + return $this->coverImageFile; + } + + public function setHideArchivedRoles($hide_archived_roles) { + $this->hideArchivedRoles = $hide_archived_roles; + return $this; + } + + public function getHideArchivedRoles() { + return $this->hideArchivedRoles; + } + + public function setTask(ManiphestTask $task) { + $this->task = $task; + return $this; + } + public function getTask() { + return $this->task; + } + + public function setOwner(PhabricatorObjectHandle $owner = null) { + $this->owner = $owner; + return $this; + } + public function getOwner() { + return $this->owner; + } + + public function setCanEdit($can_edit) { + $this->canEdit = $can_edit; + return $this; + } + + public function getCanEdit() { + return $this->canEdit; + } + + public function setShowEditControls($show_edit_controls) { + $this->showEditControls = $show_edit_controls; + return $this; + } + + public function getShowEditControls() { + return $this->showEditControls; + } + + public function getItem() { + $task = $this->getTask(); + $owner = $this->getOwner(); + $can_edit = $this->getCanEdit(); + $viewer = $this->getViewer(); + + $color_map = ManiphestTaskPriority::getColorMap(); + $bar_color = idx($color_map, $task->getPriority(), 'grey'); + + $card = id(new PHUIObjectItemView()) + ->setObject($task) + ->setUser($viewer) + ->setObjectName($task->getMonogram()) + ->setHeader($task->getTitle()) + ->setHref($task->getURI()) + ->addSigil('role-card') + ->setDisabled($task->isClosed()) + ->setBarColor($bar_color); + + if ($this->getShowEditControls()) { + if ($can_edit) { + $card + ->addSigil('draggable-card') + ->addClass('draggable-card'); + $edit_icon = 'fa-pencil'; + } else { + $card + ->addClass('not-editable') + ->addClass('undraggable-card'); + $edit_icon = 'fa-lock red'; + } + + $card->addAction( + id(new PHUIListItemView()) + ->setName(pht('Edit')) + ->setIcon($edit_icon) + ->addSigil('edit-project-card') + ->setHref('/maniphest/task/edit/'.$task->getID().'/')); + } + + if ($owner) { + $card->addHandleIcon($owner, $owner->getName()); + } + + $cover_file = $this->getCoverImageFile(); + if ($cover_file) { + $card->setCoverImage($cover_file->getBestURI()); + } + + if (ManiphestTaskPoints::getIsEnabled()) { + $points = $task->getPoints(); + if ($points !== null) { + $points_tag = id(new PHUITagView()) + ->setType(PHUITagView::TYPE_SHADE) + ->setColor(PHUITagView::COLOR_GREY) + ->setSlimShady(true) + ->setName($points) + ->addClass('phui-workcard-points'); + $card->addAttribute($points_tag); + } + } + + $subtype = $task->newSubtypeObject(); + if ($subtype && $subtype->hasTagView()) { + $subtype_tag = $subtype->newTagView() + ->setSlimShady(true); + $card->addAttribute($subtype_tag); + } + + if ($task->isClosed()) { + $icon = ManiphestTaskStatus::getStatusIcon($task->getStatus()); + $icon = id(new PHUIIconView()) + ->setIcon($icon.' grey'); + $card->addAttribute($icon); + $card->setBarColor('grey'); + } + + $role_handles = $this->getRoleHandles(); + + // Remove any archived roles from the list. + if ($this->hideArchivedRoles) { + if ($role_handles) { + foreach ($role_handles as $key => $handle) { + if ($handle->getStatus() == PhabricatorObjectHandle::STATUS_CLOSED) { + unset($role_handles[$key]); + } + } + } + } + + if ($role_handles) { + $role_handles = array_reverse($role_handles); + $tag_list = id(new PHUIHandleTagListView()) + ->setSlim(true) + ->setHandles($role_handles); + $card->addAttribute($tag_list); + } + + $card->addClass('phui-workcard'); + + return $card; + } + +} diff --git a/src/applications/roles/xaction/PhabricatorRoleColorTransaction.php b/src/applications/roles/xaction/PhabricatorRoleColorTransaction.php new file mode 100644 index 0000000000..f8e6a318a0 --- /dev/null +++ b/src/applications/roles/xaction/PhabricatorRoleColorTransaction.php @@ -0,0 +1,33 @@ +getColor(); + } + + public function applyInternalEffects($object, $value) { + $object->setColor($value); + } + + public function getTitle() { + $new = $this->getNewValue(); + return pht( + "%s set this role's color to %s.", + $this->renderAuthor(), + $this->renderValue(PHUITagView::getShadeName($new))); + } + + public function getTitleForFeed() { + $new = $this->getNewValue(); + return pht( + '%s set the color for %s to %s.', + $this->renderAuthor(), + $this->renderObject(), + $this->renderValue(PHUITagView::getShadeName($new))); + } + +} diff --git a/src/applications/roles/xaction/PhabricatorRoleFilterTransaction.php b/src/applications/roles/xaction/PhabricatorRoleFilterTransaction.php new file mode 100644 index 0000000000..a5ebc78da5 --- /dev/null +++ b/src/applications/roles/xaction/PhabricatorRoleFilterTransaction.php @@ -0,0 +1,26 @@ +getDefaultWorkboardFilter(); + } + + public function applyInternalEffects($object, $value) { + $object->setDefaultWorkboardFilter($value); + } + + public function getTitle() { + return pht( + '%s changed the default filter for the role workboard.', + $this->renderAuthor()); + } + + public function shouldHide() { + return true; + } + +} diff --git a/src/applications/roles/xaction/PhabricatorRoleIconTransaction.php b/src/applications/roles/xaction/PhabricatorRoleIconTransaction.php new file mode 100644 index 0000000000..9f39a68f77 --- /dev/null +++ b/src/applications/roles/xaction/PhabricatorRoleIconTransaction.php @@ -0,0 +1,42 @@ +getIcon(); + } + + public function applyInternalEffects($object, $value) { + $object->setIcon($value); + } + + public function getTitle() { + $set = new PhabricatorRoleIconSet(); + $new = $this->getNewValue(); + + return pht( + "%s set this role's icon to %s.", + $this->renderAuthor(), + $this->renderValue($set->getIconLabel($new))); + } + + public function getTitleForFeed() { + $set = new PhabricatorRoleIconSet(); + $new = $this->getNewValue(); + + return pht( + '%s set the icon for %s to %s.', + $this->renderAuthor(), + $this->renderObject(), + $this->renderValue($set->getIconLabel($new))); + } + + public function getIcon() { + $new = $this->getNewValue(); + return PhabricatorRoleIconSet::getIconIcon($new); + } + +} diff --git a/src/applications/roles/xaction/PhabricatorRoleImageTransaction.php b/src/applications/roles/xaction/PhabricatorRoleImageTransaction.php new file mode 100644 index 0000000000..b449ba5083 --- /dev/null +++ b/src/applications/roles/xaction/PhabricatorRoleImageTransaction.php @@ -0,0 +1,136 @@ +getProfileImagePHID(); + } + + public function applyInternalEffects($object, $value) { + $object->setProfileImagePHID($value); + } + + public function applyExternalEffects($object, $value) { + $old = $this->getOldValue(); + $new = $value; + $all = array(); + if ($old) { + $all[] = $old; + } + if ($new) { + $all[] = $new; + } + + $files = id(new PhabricatorFileQuery()) + ->setViewer($this->getActor()) + ->withPHIDs($all) + ->execute(); + $files = mpull($files, null, 'getPHID'); + + $old_file = idx($files, $old); + if ($old_file) { + $old_file->detachFromObject($object->getPHID()); + } + + $new_file = idx($files, $new); + if ($new_file) { + $new_file->attachToObject($object->getPHID()); + } + } + + public function getTitle() { + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + // TODO: Some day, it would be nice to show the images. + if (!$old) { + return pht( + "%s set this role's image to %s.", + $this->renderAuthor(), + $this->renderNewHandle()); + } else if (!$new) { + return pht( + "%s removed this role's image.", + $this->renderAuthor()); + } else { + return pht( + "%s updated this role's image from %s to %s.", + $this->renderAuthor(), + $this->renderOldHandle(), + $this->renderNewHandle()); + } + } + + public function getTitleForFeed() { + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + // TODO: Some day, it would be nice to show the images. + if (!$old) { + return pht( + '%s set the image for %s to %s.', + $this->renderAuthor(), + $this->renderObject(), + $this->renderNewHandle()); + } else if (!$new) { + return pht( + '%s removed the image for %s.', + $this->renderAuthor(), + $this->renderObject()); + } else { + return pht( + '%s updated the image for %s from %s to %s.', + $this->renderAuthor(), + $this->renderObject(), + $this->renderOldHandle(), + $this->renderNewHandle()); + } + } + + public function getIcon() { + return 'fa-photo'; + } + + public function extractFilePHIDs($object, $value) { + if ($value) { + return array($value); + } + return array(); + } + + public function validateTransactions($object, array $xactions) { + $errors = array(); + $viewer = $this->getActor(); + + foreach ($xactions as $xaction) { + $file_phid = $xaction->getNewValue(); + + // Only validate if file was uploaded + if ($file_phid) { + $file = id(new PhabricatorFileQuery()) + ->setViewer($viewer) + ->withPHIDs(array($file_phid)) + ->executeOne(); + + if (!$file) { + $errors[] = $this->newInvalidError( + pht('"%s" is not a valid file PHID.', + $file_phid)); + } else { + if (!$file->isViewableImage()) { + $mime_type = $file->getMimeType(); + $errors[] = $this->newInvalidError( + pht('File mime type of "%s" is not a valid viewable image.', + $mime_type)); + } + } + } + } + + return $errors; + } + +} diff --git a/src/applications/roles/xaction/PhabricatorRoleLockTransaction.php b/src/applications/roles/xaction/PhabricatorRoleLockTransaction.php new file mode 100644 index 0000000000..eab4e90887 --- /dev/null +++ b/src/applications/roles/xaction/PhabricatorRoleLockTransaction.php @@ -0,0 +1,64 @@ +getIsMembershipLocked(); + } + + public function applyInternalEffects($object, $value) { + $object->setIsMembershipLocked($value); + } + + public function getTitle() { + $new = $this->getNewValue(); + + if ($new) { + return pht( + "%s locked this role's membership.", + $this->renderAuthor()); + } else { + return pht( + "%s unlocked this role's membership.", + $this->renderAuthor()); + } + } + + public function getTitleForFeed() { + $new = $this->getNewValue(); + + if ($new) { + return pht( + '%s locked %s membership.', + $this->renderAuthor(), + $this->renderObject()); + } else { + return pht( + '%s unlocked %s membership.', + $this->renderAuthor(), + $this->renderObject()); + } + } + + public function getIcon() { + $new = $this->getNewValue(); + + if ($new) { + return 'fa-lock'; + } else { + return 'fa-unlock'; + } + } + + public function validateTransactions($object, array $xactions) { + if ($xactions) { + $this->requireApplicationCapability( + RoleCanLockRolesCapability::CAPABILITY); + } + return array(); + } + +} diff --git a/src/applications/roles/xaction/PhabricatorRoleMilestoneTransaction.php b/src/applications/roles/xaction/PhabricatorRoleMilestoneTransaction.php new file mode 100644 index 0000000000..dd8fd0e1b0 --- /dev/null +++ b/src/applications/roles/xaction/PhabricatorRoleMilestoneTransaction.php @@ -0,0 +1,31 @@ +setViewer($this->getActor()) + ->withPHIDs(array($parent_phid)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + + $object->attachParentRole($role); + + $number = $object->getParentRole()->loadNextMilestoneNumber(); + $object->setMilestoneNumber($number); + $object->setParentRolePHID($value); + } + +} diff --git a/src/applications/roles/xaction/PhabricatorRoleNameTransaction.php b/src/applications/roles/xaction/PhabricatorRoleNameTransaction.php new file mode 100644 index 0000000000..140f535797 --- /dev/null +++ b/src/applications/roles/xaction/PhabricatorRoleNameTransaction.php @@ -0,0 +1,112 @@ +getName(); + } + + public function applyInternalEffects($object, $value) { + $object->setName($value); + if (!$this->getEditor()->getIsMilestone()) { + $object->setPrimarySlug(PhabricatorSlug::normalizeProjectSlug($value)); + } + } + + public function applyExternalEffects($object, $value) { + $old = $this->getOldValue(); + + // First, add the old name as a secondary slug; this is helpful + // for renames and generally a good thing to do. + if (!$this->getEditor()->getIsMilestone()) { + if ($old !== null) { + $this->getEditor()->addSlug($object, $old, false); + } + $this->getEditor()->addSlug($object, $value, false); + } + return; + } + + public function getTitle() { + $old = $this->getOldValue(); + if ($old === null) { + return pht( + '%s created this role.', + $this->renderAuthor()); + } else { + return pht( + '%s renamed this role from %s to %s.', + $this->renderAuthor(), + $this->renderOldValue(), + $this->renderNewValue()); + } + } + + public function getTitleForFeed() { + $old = $this->getOldValue(); + if ($old === null) { + return pht( + '%s created %s.', + $this->renderAuthor(), + $this->renderObject()); + } else { + return pht( + '%s renamed %s from %s to %s.', + $this->renderAuthor(), + $this->renderObject(), + $this->renderOldValue(), + $this->renderNewValue()); + } + } + + public function validateTransactions($object, array $xactions) { + $errors = array(); + + if ($this->isEmptyTextTransaction($object->getName(), $xactions)) { + $errors[] = $this->newRequiredError(pht('Roles must have a name.')); + } + + $max_length = $object->getColumnMaximumByteLength('name'); + foreach ($xactions as $xaction) { + $new_value = $xaction->getNewValue(); + $new_length = strlen($new_value); + if ($new_length > $max_length) { + $errors[] = $this->newInvalidError( + pht( + 'Role names must not be longer than %s character(s).', + new PhutilNumber($max_length))); + } + } + + if ($this->getEditor()->getIsMilestone() || !$xactions) { + return $errors; + } + + $name = last($xactions)->getNewValue(); + + if (!PhabricatorSlug::isValidProjectSlug($name)) { + $errors[] = $this->newInvalidError( + pht('Role names must contain at least one letter or number.')); + } + + $slug = PhabricatorSlug::normalizeProjectSlug($name); + + $slug_used_already = id(new PhabricatorRoleSlug()) + ->loadOneWhere('slug = %s', $slug); + if ($slug_used_already && + $slug_used_already->getRolePHID() != $object->getPHID()) { + + $errors[] = $this->newInvalidError( + pht( + 'Role name generates the same hashtag ("%s") as another '. + 'existing role. Choose a unique name.', + '#'.$slug)); + } + + return $errors; + } + +} diff --git a/src/applications/roles/xaction/PhabricatorRoleParentTransaction.php b/src/applications/roles/xaction/PhabricatorRoleParentTransaction.php new file mode 100644 index 0000000000..f0dcbdd009 --- /dev/null +++ b/src/applications/roles/xaction/PhabricatorRoleParentTransaction.php @@ -0,0 +1,29 @@ +setViewer($this->getActor()) + ->withPHIDs(array($parent_phid)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + + $object->attachParentRole($role); + + $object->setParentRolePHID($value); + } + +} diff --git a/src/applications/roles/xaction/PhabricatorRoleSlugsTransaction.php b/src/applications/roles/xaction/PhabricatorRoleSlugsTransaction.php new file mode 100644 index 0000000000..7531d49d9a --- /dev/null +++ b/src/applications/roles/xaction/PhabricatorRoleSlugsTransaction.php @@ -0,0 +1,174 @@ +getSlugs(); + $slugs = mpull($slugs, 'getSlug', 'getSlug'); + unset($slugs[$object->getPrimarySlug()]); + return array_keys($slugs); + } + + public function generateNewValue($object, $value) { + return $this->getEditor()->normalizeSlugs($value); + } + + public function applyInternalEffects($object, $value) { + return; + } + + public function applyExternalEffects($object, $value) { + $old = $this->getOldValue(); + $new = $value; + $add = array_diff($new, $old); + $rem = array_diff($old, $new); + + foreach ($add as $slug) { + $this->getEditor()->addSlug($object, $slug, true); + } + + $this->getEditor()->removeSlugs($object, $rem); + } + + public function getTitle() { + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + $add = array_diff($new, $old); + $rem = array_diff($old, $new); + + $add = $this->renderHashtags($add); + $rem = $this->renderHashtags($rem); + + if ($add && $rem) { + return pht( + '%s changed role hashtag(s), added %d: %s; removed %d: %s.', + $this->renderAuthor(), + count($add), + $this->renderValueList($add), + count($rem), + $this->renderValueList($rem)); + } else if ($add) { + return pht( + '%s added %d role hashtag(s): %s.', + $this->renderAuthor(), + count($add), + $this->renderValueList($add)); + } else if ($rem) { + return pht( + '%s removed %d role hashtag(s): %s.', + $this->renderAuthor(), + count($rem), + $this->renderValueList($rem)); + } + } + + public function getTitleForFeed() { + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + $add = array_diff($new, $old); + $rem = array_diff($old, $new); + + $add = $this->renderHashtags($add); + $rem = $this->renderHashtags($rem); + + if ($add && $rem) { + return pht( + '%s changed %s hashtag(s), added %d: %s; removed %d: %s.', + $this->renderAuthor(), + $this->renderObject(), + count($add), + $this->renderValueList($add), + count($rem), + $this->renderValueList($rem)); + } else if ($add) { + return pht( + '%s added %d %s hashtag(s): %s.', + $this->renderAuthor(), + count($add), + $this->renderObject(), + $this->renderValueList($add)); + } else if ($rem) { + return pht( + '%s removed %d %s hashtag(s): %s.', + $this->renderAuthor(), + count($rem), + $this->renderObject(), + $this->renderValueList($rem)); + } + } + + public function getIcon() { + return 'fa-tag'; + } + + public function validateTransactions($object, array $xactions) { + $errors = array(); + + if (!$xactions) { + return $errors; + } + + $slug_xaction = last($xactions); + + $new = $slug_xaction->getNewValue(); + + $invalid = array(); + foreach ($new as $slug) { + if (!PhabricatorSlug::isValidProjectSlug($slug)) { + $invalid[] = $slug; + } + } + + if ($invalid) { + $errors[] = $this->newInvalidError( + pht( + 'Hashtags must contain at least one letter or number. %s '. + 'role hashtag(s) are invalid: %s.', + phutil_count($invalid), + implode(', ', $invalid))); + + return $errors; + } + + $new = $this->getEditor()->normalizeSlugs($new); + + if ($new) { + $slugs_used_already = id(new PhabricatorRoleSlug()) + ->loadAllWhere('slug IN (%Ls)', $new); + } else { + // The role doesn't have any extra slugs. + $slugs_used_already = array(); + } + + $slugs_used_already = mgroup($slugs_used_already, 'getRolePHID'); + foreach ($slugs_used_already as $role_phid => $used_slugs) { + if ($role_phid == $object->getPHID()) { + continue; + } + + $used_slug_strs = mpull($used_slugs, 'getSlug'); + + $errors[] = $this->newInvalidError( + pht( + '%s role hashtag(s) are already used by other roles: %s.', + phutil_count($used_slug_strs), + implode(', ', $used_slug_strs))); + } + + return $errors; + } + + private function renderHashtags(array $tags) { + $result = array(); + foreach ($tags as $tag) { + $result[] = '#'.$tag; + } + return $result; + } + +} diff --git a/src/applications/roles/xaction/PhabricatorRoleSortTransaction.php b/src/applications/roles/xaction/PhabricatorRoleSortTransaction.php new file mode 100644 index 0000000000..46117278a7 --- /dev/null +++ b/src/applications/roles/xaction/PhabricatorRoleSortTransaction.php @@ -0,0 +1,26 @@ +getDefaultWorkboardSort(); + } + + public function applyInternalEffects($object, $value) { + $object->setDefaultWorkboardSort($value); + } + + public function getTitle() { + return pht( + '%s changed the default sort order for the role workboard.', + $this->renderAuthor()); + } + + public function shouldHide() { + return true; + } + +} diff --git a/src/applications/roles/xaction/PhabricatorRoleStatusTransaction.php b/src/applications/roles/xaction/PhabricatorRoleStatusTransaction.php new file mode 100644 index 0000000000..4c4598a8e4 --- /dev/null +++ b/src/applications/roles/xaction/PhabricatorRoleStatusTransaction.php @@ -0,0 +1,66 @@ +getStatus(); + } + + public function applyInternalEffects($object, $value) { + $object->setStatus($value); + } + + public function getTitle() { + $old = $this->getOldValue(); + + if ($old == 0) { + return pht( + '%s archived this role.', + $this->renderAuthor()); + } else { + return pht( + '%s activated this role.', + $this->renderAuthor()); + } + } + + public function getTitleForFeed() { + $old = $this->getOldValue(); + + if ($old == 0) { + return pht( + '%s archived %s.', + $this->renderAuthor(), + $this->renderObject()); + } else { + return pht( + '%s activated %s.', + $this->renderAuthor(), + $this->renderObject()); + } + } + + public function getColor() { + $old = $this->getOldValue(); + + if ($old == 0) { + return 'red'; + } else { + return 'green'; + } + } + + public function getIcon() { + $old = $this->getOldValue(); + + if ($old == 0) { + return 'fa-ban'; + } else { + return 'fa-check'; + } + } + +} diff --git a/src/applications/roles/xaction/PhabricatorRoleTransactionType.php b/src/applications/roles/xaction/PhabricatorRoleTransactionType.php new file mode 100644 index 0000000000..79d0b7adb7 --- /dev/null +++ b/src/applications/roles/xaction/PhabricatorRoleTransactionType.php @@ -0,0 +1,4 @@ +getNewValue(); + if (!$parent_phid) { + return $errors; + } + + if (!$this->getEditor()->getIsNewObject()) { + $errors[] = $this->newInvalidError( + pht( + 'You can only set a parent or milestone role when creating a '. + 'role for the first time.')); + return $errors; + } + + $roles = id(new PhabricatorRoleQuery()) + ->setViewer($this->getActor()) + ->withPHIDs(array($parent_phid)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->execute(); + if (!$roles) { + $errors[] = $this->newInvalidError( + pht( + 'Parent or milestone role PHID ("%s") must be the PHID of a '. + 'valid, visible role which you have permission to edit.', + $parent_phid)); + return $errors; + } + + $role = head($roles); + + if ($role->isMilestone()) { + $errors[] = $this->newInvalidError( + pht( + 'Parent or milestone role PHID ("%s") must not be a '. + 'milestone. Milestones may not have subroles or milestones.', + $parent_phid)); + return $errors; + } + + $limit = PhabricatorRole::getRoleDepthLimit(); + if ($role->getRoleDepth() >= ($limit - 1)) { + $errors[] = $this->newInvalidError( + pht( + 'You can not create a subrole or milestone under this parent '. + 'because it would nest roles too deeply. The maximum '. + 'nesting depth of roles is %s.', + new PhutilNumber($limit))); + return $errors; + } + + return $errors; + } + +} diff --git a/src/applications/roles/xaction/PhabricatorRoleWorkboardBackgroundTransaction.php b/src/applications/roles/xaction/PhabricatorRoleWorkboardBackgroundTransaction.php new file mode 100644 index 0000000000..de3f69a649 --- /dev/null +++ b/src/applications/roles/xaction/PhabricatorRoleWorkboardBackgroundTransaction.php @@ -0,0 +1,26 @@ +getWorkboardBackgroundColor(); + } + + public function applyInternalEffects($object, $value) { + $object->setWorkboardBackgroundColor($value); + } + + public function getTitle() { + return pht( + '%s changed the background color of the role workboard.', + $this->renderAuthor()); + } + + public function shouldHide() { + return true; + } + +} diff --git a/src/applications/roles/xaction/PhabricatorRoleWorkboardTransaction.php b/src/applications/roles/xaction/PhabricatorRoleWorkboardTransaction.php new file mode 100644 index 0000000000..29d055d84c --- /dev/null +++ b/src/applications/roles/xaction/PhabricatorRoleWorkboardTransaction.php @@ -0,0 +1,38 @@ +getHasWorkboard(); + } + + public function generateNewValue($object, $value) { + return (int)$value; + } + + public function applyInternalEffects($object, $value) { + $object->setHasWorkboard($value); + } + + public function getTitle() { + $new = $this->getNewValue(); + + if ($new) { + return pht( + '%s enabled the workboard for this role.', + $this->renderAuthor()); + } else { + return pht( + '%s disabled the workboard for this role.', + $this->renderAuthor()); + } + } + + public function shouldHide() { + return true; + } + +} diff --git a/src/applications/roles/xaction/column/PhabricatorRoleColumnLimitTransaction.php b/src/applications/roles/xaction/column/PhabricatorRoleColumnLimitTransaction.php new file mode 100644 index 0000000000..217745a32d --- /dev/null +++ b/src/applications/roles/xaction/column/PhabricatorRoleColumnLimitTransaction.php @@ -0,0 +1,63 @@ +getPointLimit(); + } + + public function generateNewValue($object, $value) { + if ($value !== null && $value !== '') { + return (int)$value; + } else { + return null; + } + } + + public function applyInternalEffects($object, $value) { + $object->setPointLimit($value); + } + + public function getTitle() { + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + if (!$old) { + return pht( + '%s set the point limit for this column to %s.', + $this->renderAuthor(), + $this->renderNewValue()); + } else if (!$new) { + return pht( + '%s removed the point limit for this column.', + $this->renderAuthor()); + } else { + return pht( + '%s changed the point limit for this column from %s to %s.', + $this->renderAuthor(), + $this->renderOldValue(), + $this->renderNewValue()); + } + } + + public function validateTransactions($object, array $xactions) { + $errors = array(); + + foreach ($xactions as $xaction) { + $value = $xaction->getNewValue(); + if (strlen($value) && !preg_match('/^\d+\z/', $value)) { + $errors[] = $this->newInvalidError( + pht( + 'Column point limit must either be empty or a nonnegative '. + 'integer.'), + $xaction); + } + } + + return $errors; + } + +} diff --git a/src/applications/roles/xaction/column/PhabricatorRoleColumnNameTransaction.php b/src/applications/roles/xaction/column/PhabricatorRoleColumnNameTransaction.php new file mode 100644 index 0000000000..add74cb42a --- /dev/null +++ b/src/applications/roles/xaction/column/PhabricatorRoleColumnNameTransaction.php @@ -0,0 +1,69 @@ +getName(); + } + + public function applyInternalEffects($object, $value) { + $object->setName($value); + } + + public function getTitle() { + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + if ($old === null || $old === '') { + return pht( + '%s named this column %s.', + $this->renderAuthor(), + $this->renderNewValue()); + } else if ($new !== null && $new !== '') { + return pht( + '%s renamed this column from %s to %s.', + $this->renderAuthor(), + $this->renderOldValue(), + $this->renderNewValue()); + } else { + return pht( + '%s removed the custom name of this column.', + $this->renderAuthor()); + } + } + + public function validateTransactions($object, array $xactions) { + $errors = array(); + + if ($this->isEmptyTextTransaction($object->getName(), $xactions)) { + // The default "Backlog" column is allowed to be unnamed, which + // means we use the default name. + + // Proxy columns can't have a name, so don't raise an error here. + + if (!$object->isDefaultColumn() && !$object->getProxy()) { + $errors[] = $this->newRequiredError( + pht('Columns must have a name.')); + } + } + + $max_length = $object->getColumnMaximumByteLength('name'); + foreach ($xactions as $xaction) { + $new_value = $xaction->getNewValue(); + $new_length = strlen($new_value); + if ($new_length > $max_length) { + $errors[] = $this->newInvalidError( + pht( + 'Column names must not be longer than %s characters.', + new PhutilNumber($max_length)), + $xaction); + } + } + + return $errors; + } + +} diff --git a/src/applications/roles/xaction/column/PhabricatorRoleColumnStatusTransaction.php b/src/applications/roles/xaction/column/PhabricatorRoleColumnStatusTransaction.php new file mode 100644 index 0000000000..5d41b5a88d --- /dev/null +++ b/src/applications/roles/xaction/column/PhabricatorRoleColumnStatusTransaction.php @@ -0,0 +1,64 @@ +getStatus(); + } + + public function applyInternalEffects($object, $value) { + $object->setStatus($value); + } + + public function applyExternalEffects($object, $value) { + // Update the trigger usage index, which cares about whether columns are + // active or not. + $trigger_phid = $object->getTriggerPHID(); + if ($trigger_phid) { + PhabricatorSearchWorker::queueDocumentForIndexing($trigger_phid); + } + } + + public function getTitle() { + $new = $this->getNewValue(); + + switch ($new) { + case PhabricatorRoleColumn::STATUS_ACTIVE: + return pht( + '%s unhid this column.', + $this->renderAuthor()); + case PhabricatorRoleColumn::STATUS_HIDDEN: + return pht( + '%s hid this column.', + $this->renderAuthor()); + } + } + + public function validateTransactions($object, array $xactions) { + $errors = array(); + + $map = array( + PhabricatorRoleColumn::STATUS_ACTIVE, + PhabricatorRoleColumn::STATUS_HIDDEN, + ); + $map = array_fuse($map); + + foreach ($xactions as $xaction) { + $value = $xaction->getNewValue(); + if (!isset($map[$value])) { + $errors[] = $this->newInvalidError( + pht( + 'Column status "%s" is unrecognized, valid statuses are: %s.', + $value, + implode(', ', array_keys($map))), + $xaction); + } + } + + return $errors; + } + +} diff --git a/src/applications/roles/xaction/column/PhabricatorRoleColumnTransactionType.php b/src/applications/roles/xaction/column/PhabricatorRoleColumnTransactionType.php new file mode 100644 index 0000000000..e0b5bb3203 --- /dev/null +++ b/src/applications/roles/xaction/column/PhabricatorRoleColumnTransactionType.php @@ -0,0 +1,4 @@ +getTriggerPHID(); + } + + public function applyInternalEffects($object, $value) { + $object->setTriggerPHID($value); + } + + public function applyExternalEffects($object, $value) { + // After we change the trigger attached to a column, update the search + // indexes for the old and new triggers so we update the usage index. + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + $column_phids = array(); + if ($old) { + $column_phids[] = $old; + } + if ($new) { + $column_phids[] = $new; + } + + foreach ($column_phids as $phid) { + PhabricatorSearchWorker::queueDocumentForIndexing($phid); + } + } + + public function getTitle() { + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + if (!$old) { + return pht( + '%s set the column trigger to %s.', + $this->renderAuthor(), + $this->renderNewHandle()); + } else if (!$new) { + return pht( + '%s removed the trigger for this column (was %s).', + $this->renderAuthor(), + $this->renderOldHandle()); + } else { + return pht( + '%s changed the trigger for this column from %s to %s.', + $this->renderAuthor(), + $this->renderOldHandle(), + $this->renderNewHandle()); + } + } + + public function validateTransactions($object, array $xactions) { + $actor = $this->getActor(); + $errors = array(); + + foreach ($xactions as $xaction) { + $trigger_phid = $xaction->getNewValue(); + + // You can always remove a trigger. + if (!$trigger_phid) { + continue; + } + + // You can't put a trigger on a column that can't have triggers, like + // a backlog column or a proxy column. + if (!$object->canHaveTrigger()) { + $errors[] = $this->newInvalidError( + pht('This column can not have a trigger.'), + $xaction); + continue; + } + + $trigger = id(new PhabricatorRoleTriggerQuery()) + ->setViewer($actor) + ->withPHIDs(array($trigger_phid)) + ->execute(); + if (!$trigger) { + $errors[] = $this->newInvalidError( + pht( + 'Trigger "%s" is not a valid trigger, or you do not have '. + 'permission to view it.', + $trigger_phid), + $xaction); + continue; + } + } + + return $errors; + } + +} diff --git a/src/applications/roles/xaction/trigger/PhabricatorRoleTriggerNameTransaction.php b/src/applications/roles/xaction/trigger/PhabricatorRoleTriggerNameTransaction.php new file mode 100644 index 0000000000..10139caae0 --- /dev/null +++ b/src/applications/roles/xaction/trigger/PhabricatorRoleTriggerNameTransaction.php @@ -0,0 +1,58 @@ +getName(); + } + + public function applyInternalEffects($object, $value) { + $object->setName($value); + } + + public function getTitle() { + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + if (strlen($old) && strlen($new)) { + return pht( + '%s renamed this trigger from %s to %s.', + $this->renderAuthor(), + $this->renderOldValue(), + $this->renderNewValue()); + } else if (strlen($new)) { + return pht( + '%s named this trigger %s.', + $this->renderAuthor(), + $this->renderNewValue()); + } else { + return pht( + '%s stripped the name %s from this trigger.', + $this->renderAuthor(), + $this->renderOldValue()); + } + } + + public function validateTransactions($object, array $xactions) { + $errors = array(); + + $max_length = $object->getColumnMaximumByteLength('name'); + foreach ($xactions as $xaction) { + $new_value = $xaction->getNewValue(); + $new_length = strlen($new_value); + if ($new_length > $max_length) { + $errors[] = $this->newInvalidError( + pht( + 'Trigger names must not be longer than %s characters.', + new PhutilNumber($max_length)), + $xaction); + } + } + + return $errors; + } + +} diff --git a/src/applications/roles/xaction/trigger/PhabricatorRoleTriggerRulesetTransaction.php b/src/applications/roles/xaction/trigger/PhabricatorRoleTriggerRulesetTransaction.php new file mode 100644 index 0000000000..dbf39c7a8a --- /dev/null +++ b/src/applications/roles/xaction/trigger/PhabricatorRoleTriggerRulesetTransaction.php @@ -0,0 +1,81 @@ +getRuleset(); + } + + public function applyInternalEffects($object, $value) { + $object->setRuleset($value); + } + + public function getTitle() { + return pht( + '%s updated the ruleset for this trigger.', + $this->renderAuthor()); + } + + public function validateTransactions($object, array $xactions) { + $actor = $this->getActor(); + $errors = array(); + + foreach ($xactions as $xaction) { + $ruleset = $xaction->getNewValue(); + + try { + $rules = + PhabricatorRoleTrigger::newTriggerRulesFromRuleSpecifications( + $ruleset, + $allow_invalid = false, + $actor); + } catch (PhabricatorRoleTriggerCorruptionException $ex) { + $errors[] = $this->newInvalidError( + pht( + 'Ruleset specification is not valid. %s', + $ex->getMessage()), + $xaction); + continue; + } + + foreach ($rules as $rule) { + $exception = $rule->getRuleRecordValueValidationException(); + if ($exception) { + $errors[] = $this->newInvalidError( + pht( + 'Value for "%s" rule is invalid: %s', + $rule->getSelectControlName(), + $exception->getMessage()), + $xaction); + continue; + } + } + } + + return $errors; + } + + public function hasChangeDetailView() { + return true; + } + + public function newChangeDetailView() { + $viewer = $this->getViewer(); + + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + $json = new PhutilJSON(); + $old_json = $json->encodeAsList($old); + $new_json = $json->encodeAsList($new); + + return id(new PhabricatorApplicationTransactionTextDiffDetailView()) + ->setViewer($viewer) + ->setOldText($old_json) + ->setNewText($new_json); + } + +} diff --git a/src/applications/roles/xaction/trigger/PhabricatorRoleTriggerTransactionType.php b/src/applications/roles/xaction/trigger/PhabricatorRoleTriggerTransactionType.php new file mode 100644 index 0000000000..2d74ac842d --- /dev/null +++ b/src/applications/roles/xaction/trigger/PhabricatorRoleTriggerTransactionType.php @@ -0,0 +1,4 @@ + array( + '' => 'TimeTrackerRenderController', + 'view/(?P[^/]+)/' => 'TimeTrackerRenderController', + ), + ); + } + +} diff --git a/src/applications/timetracker/application/TimeTrackerStorageManager.php b/src/applications/timetracker/application/TimeTrackerStorageManager.php new file mode 100644 index 0000000000..fe0cec4bc2 --- /dev/null +++ b/src/applications/timetracker/application/TimeTrackerStorageManager.php @@ -0,0 +1,87 @@ +getID(); + $numMinutesToTrack = $this->getNumMinutesToTrack($numHours, $numMinutes); + + $timestampWhenTrackedFor = strtotime($year . '-' . $month . '-' . $day); + + $dao = new TimeTrackerTrackedTime(); + $connection = id($dao)->establishConnection('w'); + $guard = AphrontWriteGuard::beginScopedUnguardedWrites(); + $dao->openTransaction(); + + queryfx($connection, 'INSERT INTO timetracker_trackedtime SET + numMinutes = %d, realDateWhenTracked = %d, userID = %d, dateWhenTrackedFor = %d, projectPHID = %s', + $numMinutesToTrack, time(), $userID, $timestampWhenTrackedFor, $projectPHID); + + $dao->saveTransaction(); + unset($guard); + } + + public static function getNumMinutesTrackedToday($user) { + $todayTimestamp = TimeTrackerTimeUtils::getTodayTimestamp(); + $dao = new TimeTrackerTrackedTime(); + $connection = id($dao)->establishConnection('w'); + + $rows = queryfx_all( + $connection, + 'SELECT numMinutes FROM timetracker_trackedtime WHERE dateWhenTrackedFor = %d AND userID = %d', + $todayTimestamp, + $user->getID()); + + $totalMinutes = 0; + foreach ($rows as $row) { + $totalMinutes += $row['numMinutes']; + } + return $totalMinutes; + } + + public static function getNumMinutesTrackedFromDate($user, $date, $projectPHID) { + $dao = new TimeTrackerTrackedTime(); + $connection = id($dao)->establishConnection('w'); + + $rows = queryfx_all( + $connection, + 'SELECT numMinutes FROM timetracker_trackedtime WHERE dateWhenTrackedFor = %d AND userID = %d AND projectPHID = %s', + $date, + $user->getID(), + $projectPHID); + + $totalMinutes = 0; + foreach ($rows as $row) { + $totalMinutes += $row['numMinutes']; + } + return $totalMinutes; +} + + + private function getNumMinutesToTrack($numHours, $numMinutes){ + return $numHours * 60 + $numMinutes; + } + + public static function getNameSelectedProject($projectPHID){ + $dao = new PhabricatorProject(); + $connection = id($dao)->establishConnection('w'); + + $projectName = queryfx_one( + $connection, + 'SELECT name FROM project WHERE phid = %s', $projectPHID); + + return $projectName['name']; + } + + public static function getLastProjectTracked($user){ + + $dao = new TimeTrackerTrackedTime(); + $connection = id($dao)->establishConnection('w'); + + $projectPHID = queryfx_one( + $connection, + 'SELECT projectPHID FROM timetracker_trackedtime WHERE userID = %d ORDER BY dateWhenTrackedFor DESC LIMIT 1', $user->getID()); + + return $projectPHID['projectPHID']; + } +} diff --git a/src/applications/timetracker/application/TimeTrackerTimeUtils.php b/src/applications/timetracker/application/TimeTrackerTimeUtils.php new file mode 100644 index 0000000000..a2ac2c385d --- /dev/null +++ b/src/applications/timetracker/application/TimeTrackerTimeUtils.php @@ -0,0 +1,60 @@ +translateDate('Y', $date); + } + + public static function getCurrentMonth() { + $date = new DateTime('@' . time()); + return PhutilTranslator::getInstance()->translateDate('m', $date); + } + + public static function getCurrentDay() { + $date = new DateTime('@' . time()); + return PhutilTranslator::getInstance()->translateDate('d', $date); + } + + public static function getTodayTimestamp() { + $currentDay = TimeTrackerTimeUtils::getCurrentDay(); + $currentMonth = TimeTrackerTimeUtils::getCurrentMonth(); + $currentYear = TimeTrackerTimeUtils::getCurrentYear(); + + return TimeTrackerTimeUtils::getTimestamp($currentDay, $currentMonth, $currentYear); + } + + public static function getTimestamp($day, $month, $year) { + return strtotime($year . '-' . $month . '-' . $day); + } + + public static function numMinutesToString($numMinutes) { + $isNegative = $numMinutes < 0; + + $numMinutes = abs($numMinutes); + if ($numMinutes < 60) { + $str = ''; + if ($isNegative) { + $str .= '-'; + } + $str .= $numMinutes . ' minutes'; + return $str; + } + + $numHours = floor($numMinutes / 60); + $remainingMinutes = $numMinutes % 60; + + $str = ''; + if ($isNegative) { + $str .= '-'; + } + $str .= $numHours . ' hours'; + if ($remainingMinutes > 0) { + $str .= ' ' . $remainingMinutes . ' minutes'; + } + return $str; + } +} \ No newline at end of file diff --git a/src/applications/timetracker/components/TimeTracker.php b/src/applications/timetracker/components/TimeTracker.php new file mode 100644 index 0000000000..9b006fdf0e --- /dev/null +++ b/src/applications/timetracker/components/TimeTracker.php @@ -0,0 +1,33 @@ +request = $request; + return $this; + } + + public function getRequest() { + return $this->request; + } + + public function setRequestHandler($handler) { + $this->requestHandler = $handler; + } + + public function getRequestHandler() { + return $this->requestHandler; + } + + abstract public function getName(); + abstract public function getDescription(); + abstract public function renderPage($user); + abstract protected function getPanelType(); + + public function getCategory() { + return pht('General'); + } +} diff --git a/src/applications/timetracker/components/TimeTrackerDayHistoryDetailsBox.php b/src/applications/timetracker/components/TimeTrackerDayHistoryDetailsBox.php new file mode 100644 index 0000000000..7f794e08c3 --- /dev/null +++ b/src/applications/timetracker/components/TimeTrackerDayHistoryDetailsBox.php @@ -0,0 +1,60 @@ +userId = $userId; + $this->timestamp = $timestamp; + } + + public function getDetailsBox() { + $dao = new TimeTrackerTrackedTime(); + $connection = id($dao)->establishConnection('w'); + + $result = queryfx_all( + $connection, + 'SELECT numMinutes, realDateWhenTracked, projectPHID FROM timetracker_trackedtime WHERE userID = %d + AND dateWhenTrackedFor = %d ORDER BY realDateWhenTracked ASC', $this->userId, $this->timestamp); + + $totalTrackedTime = 0; + $data = array(); + foreach ($result as $row) { + $totalTrackedTime += $row['numMinutes']; + $row['realDateWhenTracked'] = date("Y-m-d H:i:s", $row['realDateWhenTracked']); + $row['numMinutes'] = $this->numMinutesToString($row['numMinutes']); + $data[] = $row; + } + + $totalTrackedTime = $this->numMinutesToString($totalTrackedTime); + + $list = new PHUIStatusListView(); + + foreach ($data as $row) { + $iconColor = ($row['numMinutes'] > 0) ? 'green' : 'red'; + + $projectInfo = ''; + if($row['projectPHID'] != ''){ + $projectInfo .= " on ". TimeTrackerStorageManager::getNameSelectedProject($row['projectPHID']); + } + + $list->addItem(id(new PHUIStatusItemView()) + ->setIcon(PHUIStatusItemView::ICON_CLOCK, $iconColor, pht('')) + ->setTarget(pht($row['numMinutes'].$projectInfo)) + ->setNote(pht('tracked ' . $row['realDateWhenTracked']))); + } + + $date = date('d-m-Y', $this->timestamp); + + $box = id(new PHUIObjectBoxView()) + ->setHeaderText('Tracked time history for ' . $date . ' (' . $totalTrackedTime . ' total)') + ->appendChild($list); + + return $box; + } + + private function numMinutesToString($numMinutes) { + return TimeTrackerTimeUtils::numMinutesToString($numMinutes); + } +} \ No newline at end of file diff --git a/src/applications/timetracker/components/TimeTrackerMainPanel.php b/src/applications/timetracker/components/TimeTrackerMainPanel.php new file mode 100644 index 0000000000..e11834bff9 --- /dev/null +++ b/src/applications/timetracker/components/TimeTrackerMainPanel.php @@ -0,0 +1,159 @@ + + To find the required project, enter its name or click on the loupe and select from the list.
+ You can track time for other days than today. Change the date for when you want to track your time, then save.
+ You can also deduct time, in case you made a mistake or tracked your time in wrong date or project.
+ To check how much time you already tracked, use summary page.'); + } + + public function renderPage($user) { + + $timeTrackingFormBox = $this->getTimeTrackingFormBox($user); + + $view = new PHUIInfoView(); + $view->setSeverity(PHUIInfoView::SEVERITY_NOTICE); + $view->setTitle('Examples how to track your time'); + $view->setErrors( + array( + pht('8h'), + pht('4h40m'), + pht('4h 40m'), + pht('35m'), + phutil_safe_html('1-2 (range, from 1-2 there will be 1hour tracked)'), + phutil_safe_html('22-2 (range, from 22-2 there will be 4hour tracked)'), + phutil_safe_html('-8h (deduct 8hours, use when you made a mistake)'), + )); + $arr = array($view, $timeTrackingFormBox); + + $responseBox = $this->getResponseBox(); + if ($responseBox != null) { + $arr[] = $responseBox; + + $date = $this->getRequest()->getStr('date'); + $pieces = explode('/', $date); + + $day = $pieces[1]; + $month = $pieces[0]; + $year = $pieces[2]; + + $timestamp = TimeTrackerTimeUtils::getTimestamp($day, $month, $year); + + $dayHistoryDetails = new TimeTrackerDayHistoryDetailsBox($user->getID(), $timestamp); + $box = $dayHistoryDetails->getDetailsBox(); + $arr[] = $box; + } + $arr[] = $this->getSummaryHoursBox($user); + $arr[] = $this->getSummaryHoursProjectsBox($user); + return $arr; + } + + private function getTimeTrackingFormBox($user) { + require_celerity_resource('jquery-js'); + require_celerity_resource('jquery-ui-js'); + require_celerity_resource('timetracker-js'); + require_celerity_resource('jquery-ui-css'); + + $arrProjects = $this->tryGetLastProjectName($user); + + $submit = id(new AphrontFormSubmitControl()); + $submit->setValue(pht('SAVE')) + ->setControlStyle('width: 13%; margin-left: 3%;'); + + $dateFormComponent = (id(new AphrontFormTextControl()) + ->setLabel(pht('Date:')) + ->setDisableAutocomplete(true) + ->setName('date') + ->setValue($this->getPrefilledDate()) + ->setControlStyle('width: 13%; margin-left: 3%;') + ->setID('datepicker')); + + + $form = id(new AphrontFormView()) + ->setUser($this->getRequest()->getUser()) + ->appendChild(id(new AphrontFormTextControl()) + + ->setDisableAutocomplete(true) + ->setControlStyle('width: 13%; margin-left: 3%;') + ->setLabel(pht('Time:')) + ->setName('timeTracked') + ->setValue('')) + ->addHiddenInput('isSent', '1') + ->addHiddenInput('panelType', $this->getPanelType()) + ->appendControl( + id(new AphrontFormTokenizerControl()) + ->setName('add_project') + ->setUser($user) + ->setLabel('Project:') + ->setLimit(1) + ->setControlStyle('width: 30%; margin-left: -0.4%;') + ->setValue($arrProjects) + ->setDatasource(new PhabricatorProjectDatasource())) + ->appendChild($dateFormComponent) + ->appendChild($submit); + + $box = id(new PHUIObjectBoxView()) + ->setForm($form) + ->setHeaderText('Track your working time') + ->appendChild(id(new PHUIBoxView())); + + return $box; + } + + private function getSummaryHoursBox($user) { + + $summaryBox = new TimeTrackerMonthSummaryBox($user); + return $summaryBox->getBox(); + } + + private function getSummaryHoursProjectsBox($user) { + + $summaryBox = new TimeTrackerProjectMonthSummaryBox($user); + return $summaryBox->getBox(); +} + + private function getCurrentDate() { + $currentDay = TimeTrackerTimeUtils::getCurrentDay(); + $currentMonth = TimeTrackerTimeUtils::getCurrentMonth(); + $currentYear = TimeTrackerTimeUtils::getCurrentYear(); + return $currentMonth . '/' . $currentDay . '/' . $currentYear; + } + + private function getResponseBox() { + $handler = $this->getRequestHandler(); + if ($handler != null) { + return $handler->getResponsePanel(); + } + return null; + } + private function tryGetLastProjectName($user){ + + $lastProjectName = TimeTrackerStorageManager::getLastProjectTracked($user); + + if($lastProjectName == ""){ + return null; + }else{ + $arrProjects = array($lastProjectName); + } + return $arrProjects; + } + + private function getPrefilledDate() { + $handler = $this->getRequestHandler(); + if ($handler != null) { + return $handler->getRequest()->getStr('date'); + } + return $this->getCurrentDate(); + } +} diff --git a/src/applications/timetracker/components/TimeTrackerMonthSummaryBox.php b/src/applications/timetracker/components/TimeTrackerMonthSummaryBox.php new file mode 100644 index 0000000000..7f2df70774 --- /dev/null +++ b/src/applications/timetracker/components/TimeTrackerMonthSummaryBox.php @@ -0,0 +1,48 @@ +user = $user; + } + + /* Add first day and the last day of current week + * CAREFUL! Not so easy as it seems to be + * Results from built-in PHP functions may return incorrect values, depending on PHP version + */ + public function getBox() { + $firstDayOfMonth = strtotime(date('Y-m-01')); + $lastDayOfMonth = strtotime(date('Y-m-t')); + $today = TimeTrackerTimeUtils::getTodayTimestamp(); + + $dao = new TimeTrackerTrackedTime(); + $connection = id($dao)->establishConnection('w'); + + $result = queryfx_all( + $connection, + 'SELECT SUM(numMinutes) numMinutes, dateWhenTrackedFor FROM timetracker_trackedtime WHERE userID = %d + AND dateWhenTrackedFor >= %d AND dateWhenTrackedFor <= %d + GROUP BY dateWhenTrackedFor ORDER BY dateWhenTrackedFor ASC', $this->user->getID(), $firstDayOfMonth, $lastDayOfMonth); + + $totalMinutesThisMonth = 0; + foreach ($result as $row) { + $totalMinutesThisMonth += $row['numMinutes']; + } + + $totalMinutesToday = TimeTrackerStorageManager::getNumMinutesTrackedToday($this->user); + + $timeTrackedThisMonth = TimeTrackerTimeUtils::numMinutesToString($totalMinutesThisMonth); + $timeTrackedToday = TimeTrackerTimeUtils::numMinutesToString($totalMinutesToday); + + $view = new PHUIInfoView(); + $view->setSeverity(PHUIInfoView::SEVERITY_NOTICE); + $view->setTitle('Total tracked time'); + $view->setErrors( + array( + phutil_safe_html('This month: ' . $timeTrackedThisMonth . ''), + phutil_safe_html('Today: ' . $timeTrackedToday . ''), + )); + return $view; + } +} \ No newline at end of file diff --git a/src/applications/timetracker/components/TimeTrackerPanelType.php b/src/applications/timetracker/components/TimeTrackerPanelType.php new file mode 100644 index 0000000000..67cb0795e6 --- /dev/null +++ b/src/applications/timetracker/components/TimeTrackerPanelType.php @@ -0,0 +1,7 @@ +user = $user; + $this->firstDayTracked = $firstDayTracked; + $this->lastDayTracked = $lastDayTracked; + } + + /* Add first day and the last day of current week + * CAREFUL! Not so easy as it seems to be + * Results from built-in PHP functions may return incorrect values, depending on PHP version + */ + public function getBox() { + $firstDayOfMonth = $this->firstDayTracked; + $lastDayOfMonth = $this->lastDayTracked; + + $dao = new TimeTrackerTrackedTime(); + $connection = id($dao)->establishConnection('w'); + $projects = queryfx_all( + $connection, + 'SELECT projectPHID FROM timetracker_trackedtime WHERE userID = %d + AND dateWhenTrackedFor >= %d AND dateWhenTrackedFor <= %d AND projectPHID != %s + GROUP BY projectPHID', $this->user->getID(), $firstDayOfMonth, $lastDayOfMonth, ''); + + $list = new PHUIStatusListView(); + + foreach ($projects as $row) { + $projectPHID = $row['projectPHID']; + + $projectName = TimeTrackerStorageManager::getNameSelectedProject($projectPHID); + + $result = queryfx_all( + $connection, + 'SELECT SUM(numMinutes) numMinutes, dateWhenTrackedFor FROM timetracker_trackedtime WHERE userID = %d + AND dateWhenTrackedFor >= %d AND dateWhenTrackedFor <= %d AND projectPHID = %s + GROUP BY dateWhenTrackedFor ORDER BY dateWhenTrackedFor ASC', $this->user->getID(), $firstDayOfMonth, $lastDayOfMonth, $projectPHID); + + $totalMinutesThisProjectOnMonth = 0; + foreach ($result as $row) { + $totalMinutesThisProjectOnMonth += $row['numMinutes']; + } + + if($totalMinutesThisProjectOnMonth > 0){ + + $timeTrackedThisMonth = TimeTrackerTimeUtils::numMinutesToString($totalMinutesThisProjectOnMonth); + + $list->addItem(id(new PHUIStatusItemView()) + ->setIcon(PHUIStatusItemView::ICON_CLOCK, 'black', pht('')) + ->setTarget(pht($projectName)) + ->setNote(pht($timeTrackedThisMonth.' tracked'))); + } + + + } + + $box = id(new PHUIObjectBoxView()) + ->setHeaderText('All tracked time on projects from '. date("m-d-Y", $this->firstDayTracked) . ' to '.date("m-d-Y", $this->lastDayTracked)) + ->appendChild($list); + return $box; + } +} \ No newline at end of file diff --git a/src/applications/timetracker/components/TimeTrackerProjectMonthSummaryBox.php b/src/applications/timetracker/components/TimeTrackerProjectMonthSummaryBox.php new file mode 100644 index 0000000000..e90a648491 --- /dev/null +++ b/src/applications/timetracker/components/TimeTrackerProjectMonthSummaryBox.php @@ -0,0 +1,61 @@ +user = $user; + } + + /* Add first day and the last day of current week + * CAREFUL! Not so easy as it seems to be + * Results from built-in PHP functions may return incorrect values, depending on PHP version + */ + public function getBox() { + $firstDayOfMonth = strtotime(date('Y-m-01')); + $lastDayOfMonth = strtotime(date('Y-m-t')); + + $dao = new TimeTrackerTrackedTime(); + $connection = id($dao)->establishConnection('w'); + $projects = queryfx_all( + $connection, + 'SELECT projectPHID FROM timetracker_trackedtime WHERE userID = %d + AND dateWhenTrackedFor >= %d AND dateWhenTrackedFor <= %d AND projectPHID != %s + GROUP BY projectPHID', $this->user->getID(), $firstDayOfMonth, $lastDayOfMonth, ''); + + $list = new PHUIStatusListView(); + + foreach ($projects as $row) { + $projectPHID = $row['projectPHID']; + + $projectName = TimeTrackerStorageManager::getNameSelectedProject($projectPHID); + + $result = queryfx_all( + $connection, + 'SELECT SUM(numMinutes) numMinutes, dateWhenTrackedFor FROM timetracker_trackedtime WHERE userID = %d + AND dateWhenTrackedFor >= %d AND dateWhenTrackedFor <= %d AND projectPHID = %s + GROUP BY dateWhenTrackedFor ORDER BY dateWhenTrackedFor ASC', $this->user->getID(), $firstDayOfMonth, $lastDayOfMonth, $projectPHID); + + $totalMinutesThisProjectOnMonth = 0; + foreach ($result as $row) { + $totalMinutesThisProjectOnMonth += $row['numMinutes']; + } + + if($totalMinutesThisProjectOnMonth > 0){ + + $timeTrackedThisMonth = TimeTrackerTimeUtils::numMinutesToString($totalMinutesThisProjectOnMonth); + + $list->addItem(id(new PHUIStatusItemView()) + ->setIcon(PHUIStatusItemView::ICON_CLOCK, 'black', pht('')) + ->setTarget(pht($projectName)) + ->setNote(pht($timeTrackedThisMonth.' tracked'))); + } + } + + $box = id(new PHUIObjectBoxView()) + ->setHeaderText('Tracked time history in this month on your projects') + ->appendChild($list); + return $box; + + } +} \ No newline at end of file diff --git a/src/applications/timetracker/components/TimeTrackerSummaryPanel.php b/src/applications/timetracker/components/TimeTrackerSummaryPanel.php new file mode 100644 index 0000000000..119e77f32b --- /dev/null +++ b/src/applications/timetracker/components/TimeTrackerSummaryPanel.php @@ -0,0 +1,133 @@ + + Click on concrete day to view tracked time history.'); + } + + protected function getPanelType() { + return TimeTrackerPanelType::SUMMARY; + } + + public function renderPage($user) { + + $dateRangeFormBox = $this->getDateRangeFormBox($user); + $chartBox = $this->getChartBox(); + $dayDetailsBox = $this->getDayDetailsBox(); + $dateDetailsBox = $this->getDateDatailsBox($user); + + $elements = array(); + $elements[] = $dateRangeFormBox; + if ($chartBox != null) { + $elements[] = $chartBox; + } + if ($dayDetailsBox != null) { + $elements[] = $dayDetailsBox; + } + if($dateDetailsBox != null){ + $elements[] = $dateDetailsBox; + } + return $elements; + } + + private function getDateRangeFormBox($user) { + require_celerity_resource('jquery-js'); + require_celerity_resource('jquery-ui-js'); + require_celerity_resource('timetracker-js'); + require_celerity_resource('jquery-ui-css'); + + $submit = id(new AphrontFormSubmitControl()); + $submit->setValue(pht('GO')) + ->setControlStyle('width: 13%; margin-left: 3%;'); + + $fromDateInput = (id(new AphrontFormTextControl()) + ->setLabel(pht('From: ')) + ->setDisableAutocomplete(true) + ->setName('from') + ->setValue('') + ->setID('datepicker') + ->setControlStyle('width: 13%; margin-left: 3%;')); + + $toDateInput = (id(new AphrontFormTextControl()) + ->setLabel(pht('To: ')) + ->setDisableAutocomplete(true) + ->setName('to') + ->setValue('') + ->setID('datepicker2') + ->setControlStyle('width: 13%; margin-left: 3%;')); + + $form = id(new AphrontFormView()) + ->setUser($this->getRequest()->getUser()) + ->appendChild($fromDateInput) + ->appendChild($toDateInput) + ->addHiddenInput('isSent', '1') + ->addHiddenInput('panelType', $this->getPanelType()) + ->appendChild($submit); + + $box = id(new PHUIObjectBoxView()) + ->setForm($form) + ->setHeaderText('Pick days range') + ->appendChild(id(new PHUIBoxView())); + + return $box; + } + + private function getChartBox() { + $requestHandler = $this->getRequestHandler(); + if ($requestHandler == null || !($requestHandler instanceof TimeTrackerSummaryPanelRequestHandler)) { + return null; + } + + $chartJsonData = $requestHandler->getChartJsonData(); + + $map = CelerityResourceMap::getNamedInstance('phabricator'); + $chart = CelerityAPI::getStaticResourceResponse()->getURI($map, 'rsrc/js/application/timetracker/chart/Chart.min.js'); + $jquery = CelerityAPI::getStaticResourceResponse()->getURI($map, 'rsrc/js/application/timetracker/chart/jquery.min.js'); + $showGraph = CelerityAPI::getStaticResourceResponse()->getURI($map, 'rsrc/js/application/timetracker/chart/show-graph.js'); + + $widthPercentage = '65%'; + $content = phutil_safe_html(pht('
+
+ + + + ', $widthPercentage, $jquery, $chart, $showGraph, $chartJsonData)); + + $box = id(new PHUIObjectBoxView()) + ->setHeader('Title') + ->appendChild($content) + ->appendChild(id(new PHUIBoxView())); + + return $box; + } + + private function getDateDatailsBox($user){ + $requestHandler = $this->getRequestHandler(); + if ($requestHandler == null || !($requestHandler instanceof TimeTrackerSummaryPanelRequestHandler)) { + return null; + } + + $form = $requestHandler->getDateFrom(); + $to = $requestHandler->getDateTo(); + + $dateBox = new TimeTrackerProjectDateSummaryBox($user, $form, $to); + return $dateBox->getBox(); + + } + + private function getDayDetailsBox() { + $requestHandler = $this->getRequestHandler(); + if ($requestHandler == null || !($requestHandler instanceof TimeTrackerDayDetailsRequestHandler)) { + return null; + } + + $box = $requestHandler->getDayDetailsBox(); + return $box; + } +} diff --git a/src/applications/timetracker/controller/TimeTrackerRenderController.php b/src/applications/timetracker/controller/TimeTrackerRenderController.php new file mode 100644 index 0000000000..405927e231 --- /dev/null +++ b/src/applications/timetracker/controller/TimeTrackerRenderController.php @@ -0,0 +1,83 @@ +getRequestHandler($request); + if ($requestHandler != null) { + $requestHandler->handleRequest($request); + } + + $classes = id(new PhutilClassMapQuery()) + ->setAncestorClass('TimeTracker') + ->setSortMethod('getName') + ->execute(); + + $nav = new AphrontSideNavFilterView(); + $nav->setBaseURI(new PhutilURI($this->getApplicationURI('view/'))); + + $groups = mgroup($classes, 'getCategory'); + ksort($groups); + foreach ($groups as $group => $group_classes) { + $nav->addLabel($group); + foreach ($group_classes as $class => $obj) { + $name = $obj->getName(); + $nav->addFilter($class, $name); + } + } + + $id = $request->getURIData('class'); + $selected = $nav->selectFilter($id, head_key($classes)); + + $page = $classes[$selected]; + $page->setRequest($this->getRequest()); + $page->setRequestHandler($requestHandler); + + $result = $page->renderPage($request->getUser()); + if ($result instanceof AphrontResponse) { + // This allows examples to generate dialogs, etc., for demonstration. + return $result; + } + + require_celerity_resource('phabricator-ui-example-css'); + + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb($page->getName()); + + $note = id(new PHUIInfoView()) + ->setTitle(pht('%s', $page->getName())) + ->appendChild($page->getDescription()) + ->setSeverity(PHUIInfoView::SEVERITY_NODATA); + + $nav->appendChild( + array( + $crumbs, + $note, + $result, + )); + + return $this->newPage() + ->setTitle($page->getName()) + ->appendChild($nav); + } + + private function getRequestHandler($request) { + $panelType = $request->getStr('panelType'); + + if (strcmp($panelType, TimeTrackerPanelType::MAIN) == 0) { + return new TimeTrackerMainPanelRequestHandler(); + } + if (strcmp($panelType, TimeTrackerPanelType::SUMMARY) == 0) { + return new TimeTrackerSummaryPanelRequestHandler(); + } + if (strcmp($panelType, TimeTrackerPanelType::DAY_DETAILS) == 0) { + return new TimeTrackerDayDetailsRequestHandler(); + } + return null; + } +} diff --git a/src/applications/timetracker/requesthandlers/TimeTrackerDayDetailsRequestHandler.php b/src/applications/timetracker/requesthandlers/TimeTrackerDayDetailsRequestHandler.php new file mode 100644 index 0000000000..f600b94eb3 --- /dev/null +++ b/src/applications/timetracker/requesthandlers/TimeTrackerDayDetailsRequestHandler.php @@ -0,0 +1,30 @@ +getTimestampFromInput($request->getStr('day')); + $userID = $request->getUser()->getID(); + + $box = new TimeTrackerDayHistoryDetailsBox($userID, $dayTimestamp); + $this->dayDetailsBox = $box->getDetailsBox(); + } + + private function getTimestampFromInput($dateInput) { + $dateInput = trim($dateInput); + $pieces = explode('-', $dateInput); + + $day = $pieces[0]; + $month = $pieces[1]; + $year = $pieces[2]; + + return TimeTrackerTimeUtils::getTimestamp($day, $month, $year); + } + + public function getDayDetailsBox() { + return $this->dayDetailsBox; + } +} diff --git a/src/applications/timetracker/requesthandlers/TimeTrackerMainPanelRequestHandler.php b/src/applications/timetracker/requesthandlers/TimeTrackerMainPanelRequestHandler.php new file mode 100644 index 0000000000..92cf81c494 --- /dev/null +++ b/src/applications/timetracker/requesthandlers/TimeTrackerMainPanelRequestHandler.php @@ -0,0 +1,191 @@ +request = $request; + + $isSent = $request->getStr('isSent') == '1'; + + + $projectPHID = $this->tryGetProject($request); + + if ($projectPHID != null) { + $this->projectName = TimeTrackerStorageManager::getNameSelectedProject($projectPHID); + }else{ + $this->responsePanel = $this->createFeedbackMessage(); + } + + if ($isSent && $projectPHID != null) { + $correctRequest = $this->parseTrackTimeRequest($request, $projectPHID); + + if (!$correctRequest) { + $this->responsePanel = $this->createResponsePanel(false); + } else { + $date = $request->getStr('date'); + $date = trim($date); + $pieces = explode('/', $date); + + $day = $pieces[1]; + $month = $pieces[0]; + $year = $pieces[2]; + + $manager = new TimeTrackerStorageManager(); + $manager->trackTime($request->getUser(), $this->numHours, $this->numMinutes, $day, $month, $year, $projectPHID); + + $this->responsePanel = $this->createResponsePanel(true); + } + } + } + + public function parseTrackTimeRequest($request, $projectPHID) + { + $timeTracked = $request->getStr('timeTracked'); + + $timeTracked = trim($timeTracked); + $timeTracked = strtolower($timeTracked); + + if (!strpbrk($timeTracked, '0123456789')) { + return false; + } + + $hasMinutes = strpos($timeTracked, 'm') !== false; + $hasHours = strpos($timeTracked, 'h') !== false; + $isNegative = strcmp(substr($timeTracked, 0, 1), '-') == 0; + $isRange = (strpos($timeTracked, '-') !== false) && !$isNegative; + + if (!$hasMinutes && !$hasHours && !$isRange) { + return false; + } + + $date = $request->getStr('date'); + $date = trim($date); + $pieces = explode('/', $date); + + $day = $pieces[1]; + $month = $pieces[0]; + $year = $pieces[2]; + + $date = TimeTrackerTimeUtils::getTimestamp($day, $month, $year); + + $correctInput = true; + if ($isRange) { + $correctInput = $this->parseRange($timeTracked); + } else { + $correctInput = $this->parseSingleTimeInput($timeTracked, $hasMinutes, $hasHours, $isNegative); + } + + $numMinutesToTrack = $this->numMinutes + $this->numHours * 60; + $numMinutesToTrackFromDay = TimeTrackerStorageManager::getNumMinutesTrackedFromDate($request->getUser(), $date, $projectPHID); + + // $numMinutesAlreadyTrackedToday = TimeTrackerStorageManager::getNumMinutesTrackedToday($request->getUser()); + + if ($numMinutesToTrackFromDay + $numMinutesToTrack < 0 || $numMinutesToTrack == 0) { + $correctInput = false; + } + + return $correctInput; + } + + private function parseSingleTimeInput($timeTracked, $hasMinutes, $hasHours, $isNegative) + { + if ($hasMinutes && $hasHours) { + list($this->numHours, $this->numMinutes) = explode('h', $timeTracked); + $this->numMinutes = trim(str_replace('m', '', $this->numMinutes)); + } else if ($hasMinutes && !$hasHours) { + $pieces = explode('m', $timeTracked); + $this->numMinutes = $pieces[0]; + } else if (!$hasMinutes && $hasHours) { + $pieces = explode('h', $timeTracked); + $this->numHours = $pieces[0]; + } + + $this->numMinutes = str_replace('-', '', $this->numMinutes); + $this->numHours = str_replace('-', '', $this->numHours); + + if ($isNegative) { + $this->numMinutes *= -1; + $this->numHours *= -1; + } + return true; + } + + private function parseRange($timeTracked) + { + $pieces = explode('-', $timeTracked); + $from = trim($pieces[0]); + $till = trim($pieces[1]); + + if ($from > 24 || $from < 0 || $till > 24 || $till < 0 || $from == $till) { + return false; + } + + if ($from > $till) { + $this->numHours = 24 - $from + $till; + } else { + $this->numHours = $till - $from; + } + return true; + } + + private function createResponsePanel($success) + { + $severity = $success ? PHUIInfoView::SEVERITY_SUCCESS : PHUIInfoView::SEVERITY_ERROR; + $responseText = ''; + if ($success) { + $responseText = 'Successfully tracked'; + if ($this->numHours != 0) { + $responseText .= ' ' . $this->numHours . ' hours'; + } + if ($this->numMinutes != 0) { + $responseText .= ' ' . $this->numMinutes . ' minutes'; + } + $responseText .= ' on project: ' . $this->projectName; + } else { + $responseText = 'Incorrect input'; + } + + $view = new PHUIInfoView(); + $view->setSeverity($severity); + $view->setErrors(array(pht($responseText))); + return $view; + } + + private function createFeedbackMessage() + { + $severity = PHUIInfoView::SEVERITY_ERROR; + $responseText = 'Please select project'; + $view = new PHUIInfoView(); + $view->setSeverity($severity); + $view->setErrors(array(pht($responseText))); + return $view; + } + + private function tryGetProject($request){ + $projectPHID = null; + try { + $arr = $request->getRequestData()['add_project']; + $projectPHID = end($arr); + } catch (Exception $e) { + + } + return $projectPHID; + } + public function getResponsePanel() + { + return $this->responsePanel; + } + + public function getRequest() + { + return $this->request; + } +} diff --git a/src/applications/timetracker/requesthandlers/TimeTrackerRequestHandler.php b/src/applications/timetracker/requesthandlers/TimeTrackerRequestHandler.php new file mode 100644 index 0000000000..8a964ba67f --- /dev/null +++ b/src/applications/timetracker/requesthandlers/TimeTrackerRequestHandler.php @@ -0,0 +1,5 @@ +getStr('isSent') == '1'; + if ($isSent) { + $from = $request->getStr('from'); + $to = $request->getStr('to'); + + if ($to == '') { + $to = $from; + } + + $fromTimestamp = $this->getTimestampFromInput($from); + $toTimestamp = $this->getTimestampFromInput($to); + + $this->dateTo = $toTimestamp; + $this->dateFrom = $fromTimestamp; + + $userID = $request->getUser()->getID(); + + $dao = new TimeTrackerTrackedTime(); + $connection = id($dao)->establishConnection('w'); + + $result = queryfx_all( + $connection, + 'SELECT SUM(numMinutes) numMinutes, dateWhenTrackedFor FROM timetracker_trackedtime WHERE userID = %d + AND dateWhenTrackedFor >= %d AND dateWhenTrackedFor <= %d + GROUP BY dateWhenTrackedFor ORDER BY dateWhenTrackedFor ASC', $userID, $fromTimestamp, $toTimestamp); + + $data = array(); + foreach ($result as $row) { + $data[] = $row; + } + + $data = $this->fillEmptyDays($fromTimestamp, $toTimestamp, $data); + $data = $this->sortData($data); + $data = $this->timestampToReadableDate($data); + + $this->chartJsonData = json_encode($data); + } + } + + private function timestampToReadableDate($data) { + foreach ($data as &$row) { + $row['dateWhenTrackedFor'] = date('d-m-y', $row['dateWhenTrackedFor']); + } + return $data; + } + + private function sortData($data) { + usort($data, function($a, $b) { + return $a['dateWhenTrackedFor'] < $b['dateWhenTrackedFor'] ? -1 : 1; + }); + return $data; + } + + private function fillEmptyDays($fromTimestamp, $toTimestamp, $data) { + $rangeOfDays = ($toTimestamp - $fromTimestamp) / TimeTrackerTimeUtils::NUM_SECONDS_IN_DAY + 1; + $dateWhenTrackedForColumn = array_column($data, 'dateWhenTrackedFor'); + + for ($i = 0; $i < $rangeOfDays; $i++) { + $currentDayInRangeDate = $fromTimestamp + $i * TimeTrackerTimeUtils::NUM_SECONDS_IN_DAY; + if (array_search($currentDayInRangeDate, $dateWhenTrackedForColumn) === false) { + $data[] = ['numMinutes' => '0', 'dateWhenTrackedFor' => $currentDayInRangeDate]; + } + } + return $data; + } + + public function getChartJsonData() { + return $this->chartJsonData; + } + + public function getDateTo(){ + return $this->dateTo; + } + + public function getDateFrom(){ + return $this->dateFrom; + } + + private function getTimestampFromInput($dateInput) { + $dateInput = trim($dateInput); + $pieces = explode('/', $dateInput); + + $day = $pieces[1]; + $month = $pieces[0]; + $year = $pieces[2]; + + return TimeTrackerTimeUtils::getTimestamp($day, $month, $year); + } +} diff --git a/src/applications/timetracker/storage/TimeTrackerDAO.php b/src/applications/timetracker/storage/TimeTrackerDAO.php new file mode 100644 index 0000000000..e6cc4d1ee3 --- /dev/null +++ b/src/applications/timetracker/storage/TimeTrackerDAO.php @@ -0,0 +1,8 @@ + false, + self::CONFIG_COLUMN_SCHEMA => array( + 'id' => 'auto', + 'userID' => 'uint32', + 'numMinutes' => 'uint32', + 'dateWhenTrackedFor' => 'uint32', + 'realDateWhenTracked' => 'uint32', + ), + self::CONFIG_KEY_SCHEMA => array( + 'PRIMARY' => array( + 'columns' => array('id'), + 'unique' => true, + ), + ), self::CONFIG_NO_TABLE => true + + ); + } +} + // + parent::getConfiguration() \ No newline at end of file diff --git a/src/applications/vacation/application/VacationApplication.php b/src/applications/vacation/application/VacationApplication.php new file mode 100644 index 0000000000..bfe5e27c81 --- /dev/null +++ b/src/applications/vacation/application/VacationApplication.php @@ -0,0 +1,54 @@ + array( + '' => 'VacationRenderController', + 'view/(?P[^/]+)/' => 'VacationRenderController', + ), + ); + } + +} diff --git a/src/applications/vacation/application/VacationStorageManager.php b/src/applications/vacation/application/VacationStorageManager.php new file mode 100644 index 0000000000..123f46b9da --- /dev/null +++ b/src/applications/vacation/application/VacationStorageManager.php @@ -0,0 +1,62 @@ +getID(); + $numMinutesToTrack = $this->getNumMinutesToTrack($numHours, $numMinutes); + + $timestampWhenTrackedFor = strtotime($year . '-' . $month . '-' . $day); + + $dao = new VacationVacationDay(); + $connection = id($dao)->establishConnection('w'); + $guard = AphrontWriteGuard::beginScopedUnguardedWrites(); + $dao->openTransaction(); + + queryfx($connection, 'INSERT INTO vacation_day SET + numMinutes = %d, realDateWhenTracked = %d, userID = %d, dateWhenTrackedFor = %d', + $numMinutesToTrack, time(), $userID, $timestampWhenTrackedFor); + + $dao->saveTransaction(); + unset($guard); + } + + public static function getNumMinutesTrackedToday($user) { + $todayTimestamp = VacationTimeUtils::getTodayTimestamp(); + $dao = new VacationVacationDay(); + $connection = id($dao)->establishConnection('w'); + + $rows = queryfx_all( + $connection, + 'SELECT numMinutes FROM vacation_day WHERE dateWhenTrackedFor = %d AND userID = %d', + $todayTimestamp, + $user->getID()); + + $totalMinutes = 0; + foreach ($rows as $row) { + $totalMinutes += $row['numMinutes']; + } + return $totalMinutes; + } + + public static function getNumMinutesTrackedFromDate($user, $date) { + $dao = new VacationVacationDay(); + $connection = id($dao)->establishConnection('w'); + + $rows = queryfx_all( + $connection, + 'SELECT numMinutes FROM vacation_day WHERE dateWhenTrackedFor = %d AND userID = %d', + $date, + $user->getID()); + + $totalMinutes = 0; + foreach ($rows as $row) { + $totalMinutes += $row['numMinutes']; + } + return $totalMinutes; +} + + private function getNumMinutesToTrack($numHours, $numMinutes) { + return $numHours * 60 + $numMinutes; + } +} diff --git a/src/applications/vacation/application/VacationTimeUtils.php b/src/applications/vacation/application/VacationTimeUtils.php new file mode 100644 index 0000000000..3d49776b59 --- /dev/null +++ b/src/applications/vacation/application/VacationTimeUtils.php @@ -0,0 +1,176 @@ +translateDate('Y', $date); + } + + public static function getCurrentMonth() { + $date = new DateTime('@' . time()); + return PhutilTranslator::getInstance()->translateDate('m', $date); + } + + public static function getCurrentDay() { + $date = new DateTime('@' . time()); + return PhutilTranslator::getInstance()->translateDate('d', $date); + } + + public static function getTodayTimestamp() { + $currentDay = VacationTimeUtils::getCurrentDay(); + $currentMonth = VacationTimeUtils::getCurrentMonth(); + $currentYear = VacationTimeUtils::getCurrentYear(); + + return VacationTimeUtils::getTimestamp($currentDay, $currentMonth, $currentYear); + } + + public static function getTimestamp($day, $month, $year) { + return strtotime($year . '-' . $month . '-' . $day); + } + + public static function getDateRegistrationUser($user){ + return $user->getDateCreated(); + } + + public static function numMinutesToString($numMinutes) { + $isNegative = $numMinutes < 0; + + $numMinutes = abs($numMinutes); + if ($numMinutes < 60) { + $str = ''; + if ($isNegative) { + $str .= '-'; + } + $str .= $numMinutes . ' minutes'; + return $str; + } + + $numHours = floor($numMinutes / 60); + $remainingMinutes = $numMinutes % 60; + + $str = ''; + if ($isNegative) { + $str .= '-'; + } + $str .= $numHours . ' hours'; + if ($remainingMinutes > 0) { + $str .= ' ' . $remainingMinutes . ' minutes'; + } + return $str; + } + + public static function getNumTrackedMinutes($user){ + + $firstDayOfStartWork = strtotime(date('1998-m-01')); // Should be changed as soon as possible to an adaptive value + $lastDayOfMonth = strtotime(date('Y-m-t')); + $today = VacationTimeUtils::getTodayTimestamp(); + + $dao = new TimeTrackerTrackedTime(); + $connection = id($dao)->establishConnection('w'); + + $result = queryfx_all( + $connection, + 'SELECT SUM(numMinutes) numMinutes, dateWhenTrackedFor FROM timetracker_trackedtime WHERE userID = %d + AND dateWhenTrackedFor >= %d AND dateWhenTrackedFor <= %d + GROUP BY dateWhenTrackedFor ORDER BY dateWhenTrackedFor ASC', $user->getID(), $firstDayOfStartWork, $lastDayOfMonth); + + $totalMinutesForAllTime = 0; + foreach ($result as $row) { + $totalMinutesForAllTime += $row['numMinutes']; + } + + return $totalMinutesForAllTime; + } + + + public static function getNumTrackedMinutesVacation($user){ + + $firstDayOfStartWork = strtotime(date('1998-m-01')); // Should be changed as soon as possible to an adaptive value + $lastDayOfMonth = strtotime(date('Y-m-t')); + + $dao = new VacationVacationDay(); + $connection = id($dao)->establishConnection('w'); + + $result = queryfx_all( + $connection, + 'SELECT SUM(numMinutes) numMinutes, dateWhenTrackedFor FROM vacation_day WHERE userID = %d + AND dateWhenTrackedFor >= %d AND dateWhenTrackedFor <= %d + GROUP BY dateWhenTrackedFor ORDER BY dateWhenTrackedFor ASC', $user->getID(), $firstDayOfStartWork, $lastDayOfMonth); + + $totalMinutesVacationTracked = 0; + foreach ($result as $row) { + $totalMinutesVacationTracked += $row['numMinutes']; + } + + return $totalMinutesVacationTracked; + } + + public static function getNumMinutesTrackedForDateRange($user){ + + $d = '-'.VacationTimeUtils::NUM_DAYS_ABLE_UNDO.'days'; + $date = strtotime($d); + $firstDay = date('Y-m-d', $date); + + $date = $firstDay; + $date = trim($date); + $pieces = explode('-', $date); + + $day = $pieces[2]; + $month = $pieces[1]; + $year = $pieces[0]; + + $date = VacationTimeUtils::getTimestamp($day,$month,$year); + + $firstDay = $date; + $lastDayOfMonth = strtotime(date('Y-m-t')); + + $dao = new VacationVacationDay(); + $connection = id($dao)->establishConnection('w'); + + $result = queryfx_all( + $connection, + 'SELECT SUM(numMinutes) numMinutes, dateWhenTrackedFor FROM vacation_day WHERE userID = %d + AND dateWhenTrackedFor >= %d AND dateWhenTrackedFor <= %d + GROUP BY dateWhenTrackedFor ORDER BY dateWhenTrackedFor ASC', $user->getID(), $firstDay, $lastDayOfMonth); + + $totalMinutesVacationTracked = 0; + foreach ($result as $row) { + $totalMinutesVacationTracked += $row['numMinutes']; + } + + return $totalMinutesVacationTracked; + } + public static function getNumMinutesVacation($user){ + + $totalMinutesForAllTime = VacationTimeUtils::getNumTrackedMinutes($user); + + $totalMinutesVacationTracked = VacationTimeUtils::getNumTrackedMinutesVacation($user); + + $dataCreatedUser = VacationTimeUtils::getDateRegistrationUser($user); + + $date = date("Y-m-d H:i:s", $dataCreatedUser); + $date = new DateTime($date); + $dateDiff = date_diff(new DateTime(), $date)->y; + + if($dateDiff >= VacationTimeUtils::NUM_YEARS_FOR_BONUS){ + $totalMinutesForAllTime = $totalMinutesForAllTime / VacationTimeUtils::COEF_BONUS_VACATION; + } + else{ + $totalMinutesForAllTime = $totalMinutesForAllTime / VacationTimeUtils::COEF_BASE_VACATION; + } + + $totalMinutesForAllTime = $totalMinutesForAllTime - $totalMinutesVacationTracked; + + return $totalMinutesForAllTime; + + } + + +} \ No newline at end of file diff --git a/src/applications/vacation/components/Vacation.php b/src/applications/vacation/components/Vacation.php new file mode 100644 index 0000000000..bfa52e2c18 --- /dev/null +++ b/src/applications/vacation/components/Vacation.php @@ -0,0 +1,33 @@ +request = $request; + return $this; + } + + public function getRequest() { + return $this->request; + } + + public function setRequestHandler($handler) { + $this->requestHandler = $handler; + } + + public function getRequestHandler() { + return $this->requestHandler; + } + + abstract public function getName(); + abstract public function getDescription(); + abstract public function renderPage($user); + abstract protected function getPanelType(); + + public function getCategory() { + return pht('General'); + } +} diff --git a/src/applications/vacation/components/VacationDayHistoryDetailsBox.php b/src/applications/vacation/components/VacationDayHistoryDetailsBox.php new file mode 100644 index 0000000000..2f3451a1a2 --- /dev/null +++ b/src/applications/vacation/components/VacationDayHistoryDetailsBox.php @@ -0,0 +1,54 @@ +userId = $userId; + $this->timestamp = $timestamp; + } + + public function getDetailsBox() { + $dao = new VacationVacationDay(); + $connection = id($dao)->establishConnection('w'); + + $result = queryfx_all( + $connection, + 'SELECT numMinutes, realDateWhenTracked FROM vacation_day WHERE userID = %d + AND dateWhenTrackedFor = %d ORDER BY realDateWhenTracked ASC', $this->userId, $this->timestamp); + + $totalTrackedTime = 0; + $data = array(); + foreach ($result as $row) { + $totalTrackedTime += $row['numMinutes']; + $row['realDateWhenTracked'] = date("Y-m-d H:i:s", $row['realDateWhenTracked']); + $row['numMinutes'] = $this->numMinutesToString($row['numMinutes']); + $data[] = $row; + } + + $totalTrackedTime = $this->numMinutesToString($totalTrackedTime); + + $list = new PHUIStatusListView(); + + foreach ($data as $row) { + $iconColor = ($row['numMinutes'] > 0) ? 'green' : 'red'; + $list->addItem(id(new PHUIStatusItemView()) + ->setIcon(PHUIStatusItemView::ICON_CLOCK, $iconColor, pht('')) + ->setTarget(pht($row['numMinutes'])) + ->setNote(pht('tracked ' . $row['realDateWhenTracked']))); + } + + $date = date('d-m-Y', $this->timestamp); + + $box = id(new PHUIObjectBoxView()) + ->setHeaderText('Tracked time history for ' . $date . ' (' . $totalTrackedTime . ' total)') + ->appendChild($list); + + return $box; + } + + private function numMinutesToString($numMinutes) { + return VacationTimeUtils::numMinutesToString($numMinutes); + } +} \ No newline at end of file diff --git a/src/applications/vacation/components/VacationMainPanel.php b/src/applications/vacation/components/VacationMainPanel.php new file mode 100644 index 0000000000..52c90be047 --- /dev/null +++ b/src/applications/vacation/components/VacationMainPanel.php @@ -0,0 +1,123 @@ + + You can also subtract time if you make a mistake, you have one day for that.
+ To check how much time you already tracked, use summary page.'); + } + + public function renderPage($user) { + + $timeTrackingFormBox = $this->getTimeTrackingFormBox($user); + + $view = new PHUIInfoView(); + $view->setSeverity(PHUIInfoView::SEVERITY_NOTICE); + $view->setTitle('Examples how to track your vacation hours'); + $view->setErrors( + array( + pht('8h'), + phutil_safe_html('-8h (deduct 8hours, use when you made a mistake)'), + )); + $arr = array($view, $timeTrackingFormBox); + + $arr[] = $this->getSummaryHoursBox($user); + + $responseBox = $this->getResponseBox(); + if ($responseBox != null) { + $arr[] = $responseBox; + + $date = $this->getRequest()->getStr('isDate'); + $pieces = explode('/', $date); + + $day = $pieces[1]; + $month = $pieces[0]; + $year = $pieces[2]; + + $timestamp = VacationTimeUtils::getTimestamp($day, $month, $year); + $dayHistoryDetails = new VacationDayHistoryDetailsBox($user->getID(), $timestamp); + $box = $dayHistoryDetails->getDetailsBox(); + $arr[] = $box; + } + + return $arr; + } + + private function getTimeTrackingFormBox($user) { + require_celerity_resource('jquery-js'); + require_celerity_resource('jquery-ui-js'); + require_celerity_resource('timetracker-js'); + require_celerity_resource('jquery-ui-css'); + + $submit = id(new AphrontFormSubmitControl()); + $submit->setValue(pht('SAVE')) + ->setControlStyle('width: 13%; margin-left: 3%; box-sizing : content-box;'); + + $dateFormComponent = (id(new AphrontFormTextControl()) + ->setLabel(pht('Date:')) + ->setDisabled('disabled') + ->setName('date') + ->setValue($this->getPrefilledDate()) + ->setControlStyle('width: 13%; margin-left: 3%;') + ->setID('datepicker')); + + $form = id(new AphrontFormView()) + ->setUser($this->getRequest()->getUser()) + ->appendChild(id(new AphrontFormTextControl()) + ->setDisableAutocomplete(true) + ->setControlStyle('width: 13%; margin-left: 3%;') + ->setLabel(pht('Time:')) + ->setName('timeTracked') + ->setValue('')) + ->addHiddenInput('isSent', '1') + ->addHiddenInput('isDate', $this->getPrefilledDate()) + ->addHiddenInput('panelType', $this->getPanelType()) + ->appendChild($dateFormComponent) + ->appendChild($submit); + + $box = id(new PHUIObjectBoxView()) + ->setForm($form) + ->setHeaderText('Track your vacation hours') + ->appendChild(id(new PHUIBoxView())); + + return $box; + } + + private function getSummaryHoursBox($user) { + + $summaryBox = new VacationMonthSummaryBox($user); + return $summaryBox->getBox(); + } + + private function getCurrentDate() { + $currentDay = VacationTimeUtils::getCurrentDay(); + $currentMonth = VacationTimeUtils::getCurrentMonth(); + $currentYear = VacationTimeUtils::getCurrentYear(); + return $currentMonth . '/' . $currentDay . '/' . $currentYear; + } + + private function getResponseBox() { + $handler = $this->getRequestHandler(); + if ($handler != null) { + return $handler->getResponsePanel(); + } + return null; + } + + private function getPrefilledDate() { + $handler = $this->getRequestHandler(); + if ($handler != null) { + return $handler->getRequest()->getStr('isDate'); + } + return $this->getCurrentDate(); + } +} diff --git a/src/applications/vacation/components/VacationMonthSummaryBox.php b/src/applications/vacation/components/VacationMonthSummaryBox.php new file mode 100644 index 0000000000..c345909790 --- /dev/null +++ b/src/applications/vacation/components/VacationMonthSummaryBox.php @@ -0,0 +1,30 @@ +user = $user; + } + public function getBox() { + + $totalMinutesToday = VacationStorageManager::getNumMinutesTrackedToday($this->user); + + $totalMinutesForAllTime = VacationTimeUtils::getNumMinutesVacation($this->user); + + $timeTrackedThisMonth = VacationTimeUtils::numMinutesToString($totalMinutesForAllTime); + + $timeTrackedToday = VacationTimeUtils::numMinutesToString($totalMinutesToday); + + $view = new PHUIInfoView(); + $view->setSeverity(PHUIInfoView::SEVERITY_NOTICE); + $view->setTitle('Vacation hours'); + $view->setErrors( + array( + phutil_safe_html('All time: ' . $timeTrackedThisMonth . ''), + phutil_safe_html('Used today: ' . $timeTrackedToday . ''), + )); + return $view; + } +} \ No newline at end of file diff --git a/src/applications/vacation/components/VacationPanelType.php b/src/applications/vacation/components/VacationPanelType.php new file mode 100644 index 0000000000..a2513250a1 --- /dev/null +++ b/src/applications/vacation/components/VacationPanelType.php @@ -0,0 +1,7 @@ + + Click on concrete day to view tracked time history.'); + } + + protected function getPanelType() { + return VacationPanelType::SUMMARY; + } + + public function renderPage($user) { + + $dateRangeFormBox = $this->getDateRangeFormBox($user); + $chartBox = $this->getChartBox(); + $dayDetailsBox = $this->getDayDetailsBox(); + + $elements = array(); + $elements[] = $dateRangeFormBox; + if ($chartBox != null) { + $elements[] = $chartBox; + } + if ($dayDetailsBox != null) { + $elements[] = $dayDetailsBox; + } + return $elements; + } + + private function getDateRangeFormBox($user) { + require_celerity_resource('jquery-js'); + require_celerity_resource('jquery-ui-js'); + require_celerity_resource('timetracker-js'); + require_celerity_resource('jquery-ui-css'); + + $submit = id(new AphrontFormSubmitControl()); + $submit->setValue(pht('GO')) + ->setControlStyle('width: 13%; margin-left: 3%;'); + + $fromDateInput = (id(new AphrontFormTextControl()) + ->setLabel(pht('From: ')) + ->setDisableAutocomplete(true) + ->setName('from') + ->setValue('') + ->setID('datepicker') + ->setControlStyle('width: 13%; margin-left: 3%;')); + + $toDateInput = (id(new AphrontFormTextControl()) + ->setLabel(pht('To: ')) + ->setDisableAutocomplete(true) + ->setName('to') + ->setValue('') + ->setID('datepicker2') + ->setControlStyle('width: 13%; margin-left: 3%;')); + + $form = id(new AphrontFormView()) + ->setUser($this->getRequest()->getUser()) + ->appendChild($fromDateInput) + ->appendChild($toDateInput) + ->addHiddenInput('isSent', '1') + ->addHiddenInput('panelType', $this->getPanelType()) + ->appendChild($submit); + + $box = id(new PHUIObjectBoxView()) + ->setForm($form) + ->setHeaderText('Pick days range') + ->appendChild(id(new PHUIBoxView())); + + return $box; + } + + private function getChartBox() { + $requestHandler = $this->getRequestHandler(); + if ($requestHandler == null || !($requestHandler instanceof VacationSummaryPanelRequestHandler)) { + return null; + } + + $chartJsonData = $requestHandler->getChartJsonData(); + + $map = CelerityResourceMap::getNamedInstance('phabricator'); + $chart = CelerityAPI::getStaticResourceResponse()->getURI($map, 'rsrc/js/application/timetracker/chart/Chart.min.js'); + $jquery = CelerityAPI::getStaticResourceResponse()->getURI($map, 'rsrc/js/application/timetracker/chart/jquery.min.js'); + $showGraph = CelerityAPI::getStaticResourceResponse()->getURI($map, 'rsrc/js/application/timetracker/chart/show-graph.js'); + + $widthPercentage = '65%'; + $content = phutil_safe_html(pht('
+
+ + + + ', $widthPercentage, $jquery, $chart, $showGraph, $chartJsonData)); + + $box = id(new PHUIObjectBoxView()) + ->setHeader('Title') + ->appendChild($content) + ->appendChild(id(new PHUIBoxView())); + + return $box; + } + + private function getDayDetailsBox() { + $requestHandler = $this->getRequestHandler(); + if ($requestHandler == null || !($requestHandler instanceof VacationDayDetailsRequestHandler)) { + return null; + } + + $box = $requestHandler->getDayDetailsBox(); + return $box; + } +} diff --git a/src/applications/vacation/controller/VacationRenderController.php b/src/applications/vacation/controller/VacationRenderController.php new file mode 100644 index 0000000000..affa47ca0b --- /dev/null +++ b/src/applications/vacation/controller/VacationRenderController.php @@ -0,0 +1,83 @@ +getRequestHandler($request); + if ($requestHandler != null) { + $requestHandler->handleRequest($request); + } + + $classes = id(new PhutilClassMapQuery()) + ->setAncestorClass('Vacation') + ->setSortMethod('getName') + ->execute(); + + $nav = new AphrontSideNavFilterView(); + $nav->setBaseURI(new PhutilURI($this->getApplicationURI('view/'))); + + $groups = mgroup($classes, 'getCategory'); + ksort($groups); + foreach ($groups as $group => $group_classes) { + $nav->addLabel($group); + foreach ($group_classes as $class => $obj) { + $name = $obj->getName(); + $nav->addFilter($class, $name); + } + } + + $id = $request->getURIData('class'); + $selected = $nav->selectFilter($id, head_key($classes)); + + $page = $classes[$selected]; + $page->setRequest($this->getRequest()); + $page->setRequestHandler($requestHandler); + + $result = $page->renderPage($request->getUser()); + if ($result instanceof AphrontResponse) { + // This allows examples to generate dialogs, etc., for demonstration. + return $result; + } + + require_celerity_resource('phabricator-ui-example-css'); + + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb($page->getName()); + + $note = id(new PHUIInfoView()) + ->setTitle(pht('%s', $page->getName())) + ->appendChild($page->getDescription()) + ->setSeverity(PHUIInfoView::SEVERITY_NODATA); + + $nav->appendChild( + array( + $crumbs, + $note, + $result, + )); + + return $this->newPage() + ->setTitle($page->getName()) + ->appendChild($nav); + } + + private function getRequestHandler($request) { + $panelType = $request->getStr('panelType'); + + if (strcmp($panelType, VacationPanelType::MAIN) == 0) { + return new VacationMainPanelRequestHandler(); + } + if (strcmp($panelType, VacationPanelType::SUMMARY) == 0) { + return new VacationSummaryPanelRequestHandler(); + } + if (strcmp($panelType, VacationPanelType::DAY_DETAILS) == 0) { + return new VacationDayDetailsRequestHandler(); + } + return null; + } +} diff --git a/src/applications/vacation/requesthandlers/VacationDayDetailsRequestHandler.php b/src/applications/vacation/requesthandlers/VacationDayDetailsRequestHandler.php new file mode 100644 index 0000000000..58e3b40a25 --- /dev/null +++ b/src/applications/vacation/requesthandlers/VacationDayDetailsRequestHandler.php @@ -0,0 +1,30 @@ +getTimestampFromInput($request->getStr('day')); + $userID = $request->getUser()->getID(); + + $box = new VacationDayHistoryDetailsBox($userID, $dayTimestamp); + $this->dayDetailsBox = $box->getDetailsBox(); + } + + private function getTimestampFromInput($dateInput) { + $dateInput = trim($dateInput); + $pieces = explode('-', $dateInput); + + $day = $pieces[0]; + $month = $pieces[1]; + $year = $pieces[2]; + + return VacationTimeUtils::getTimestamp($day, $month, $year); + } + + public function getDayDetailsBox() { + return $this->dayDetailsBox; + } +} diff --git a/src/applications/vacation/requesthandlers/VacationMainPanelRequestHandler.php b/src/applications/vacation/requesthandlers/VacationMainPanelRequestHandler.php new file mode 100644 index 0000000000..14cf422126 --- /dev/null +++ b/src/applications/vacation/requesthandlers/VacationMainPanelRequestHandler.php @@ -0,0 +1,161 @@ +request = $request; + + $isSent = $request->getStr('isSent') == '1'; + if ($isSent) { + $correctRequest = $this->parseTrackTimeRequest($request); + + if (!$correctRequest) { + $this->responsePanel = $this->createResponsePanel(false); + } + else { + $date = $request->getStr('isDate'); + $date = trim($date); + $pieces = explode('/', $date); + + $day = $pieces[1]; + $month = $pieces[0]; + $year = $pieces[2]; + + $manager = new VacationStorageManager(); + $manager->trackTime($request->getUser(), $this->numHours, $this->numMinutes, $day, $month, $year); + + $this->responsePanel = $this->createResponsePanel(true); + } + } + } + + public function parseTrackTimeRequest($request) { + $timeTracked = $request->getStr('timeTracked'); + + $timeTracked = trim($timeTracked); + $timeTracked = strtolower($timeTracked); + + if (!strpbrk($timeTracked, '0123456789')) { + return false; + } + + $hasMinutes = strpos($timeTracked, 'm') !== false; + $hasHours = strpos($timeTracked, 'h') !== false; + $isNegative = strcmp(substr($timeTracked, 0, 1), '-') == 0; + $isRange = (strpos($timeTracked, '-') !== false) && !$isNegative; + + if (!$hasMinutes && !$hasHours && !$isRange) { + return false; + } + + + $date = $request->getStr('isDate'); + $date = trim($date); + $pieces = explode('/', $date); + + $day = $pieces[1]; + $month = $pieces[0]; + $year = $pieces[2]; + + $date = VacationTimeUtils::getTimestamp($day,$month,$year); + + $correctInput = true; + if ($isRange) { + $correctInput = $this->parseRange($timeTracked); + } + else { + $correctInput = $this->parseSingleTimeInput($timeTracked, $hasMinutes, $hasHours, $isNegative); + } + + $numMinutesToTrack = $this->numMinutes + $this->numHours * 60; + + $numMinutesToTrackFromDay = VacationTimeUtils::getNumMinutesVacation($request->getUser()); + $numMinuterTrackedVacationDay = VacationTimeUtils::getNumTrackedMinutesVacation($request->getUser()); + $numDateRange = VacationTimeUtils::getNumMinutesTrackedForDateRange($request->getUser()); + + // $numMinutesToTrackFromDay = VacationStorageManager::getNumMinutesTrackedFromDate($request->getUser(), $date); + + if ($numMinutesToTrackFromDay - $numMinutesToTrack < 0 || $numDateRange + $numMinutesToTrack < 0 || $numMinutesToTrack == 0) { + $correctInput = false; + } + + return $correctInput; + } + + private function parseSingleTimeInput($timeTracked, $hasMinutes, $hasHours, $isNegative) { + if ($hasMinutes && $hasHours) { + list($this->numHours, $this->numMinutes) = explode('h', $timeTracked); + $this->numMinutes = trim(str_replace('m', '', $this->numMinutes)); + } + else if ($hasMinutes && !$hasHours) { + $pieces = explode('m', $timeTracked); + $this->numMinutes = $pieces[0]; + } + else if (!$hasMinutes && $hasHours) { + $pieces = explode('h', $timeTracked); + $this->numHours = $pieces[0]; + } + + $this->numMinutes = str_replace('-', '', $this->numMinutes); + $this->numHours = str_replace('-', '', $this->numHours); + + if ($isNegative) { + $this->numMinutes *= -1; + $this->numHours *= -1; + } + return true; + } + + private function parseRange($timeTracked) { + $pieces = explode('-', $timeTracked); + $from = trim($pieces[0]); + $till = trim($pieces[1]); + + if ($from > 24 || $from < 0 || $till > 24 || $till < 0 || $from == $till) { + return false; + } + + if ($from > $till) { + $this->numHours = 24 - $from + $till; + } + else { + $this->numHours = $till - $from; + } + return true; + } + + private function createResponsePanel($success) { + $severity = $success ? PHUIInfoView::SEVERITY_SUCCESS : PHUIInfoView::SEVERITY_ERROR; + $responseText = ''; + if ($success) { + $responseText = 'Successfully tracked'; + if ($this->numHours != 0) { + $responseText .= ' ' . $this->numHours . ' hours'; + } + if ($this->numMinutes != 0) { + $responseText .= ' ' . $this->numMinutes . ' minutes'; + } + } + else { + $responseText = 'Incorrect input'; + } + + $view = new PHUIInfoView(); + $view->setSeverity($severity); + $view->setErrors(array(pht($responseText))); + return $view; + } + + public function getResponsePanel() { + return $this->responsePanel; + } + + public function getRequest() { + return $this->request; + } +} diff --git a/src/applications/vacation/requesthandlers/VacationRequestHandler.php b/src/applications/vacation/requesthandlers/VacationRequestHandler.php new file mode 100644 index 0000000000..b8d9f0c5d0 --- /dev/null +++ b/src/applications/vacation/requesthandlers/VacationRequestHandler.php @@ -0,0 +1,5 @@ +getStr('isSent') == '1'; + if ($isSent) { + $from = $request->getStr('from'); + $to = $request->getStr('to'); + + if ($to == '') { + $to = $from; + } + + $fromTimestamp = $this->getTimestampFromInput($from); + $toTimestamp = $this->getTimestampFromInput($to); + $userID = $request->getUser()->getID(); + + $dao = new VacationVacationDay(); + $connection = id($dao)->establishConnection('w'); + + $result = queryfx_all( + $connection, + 'SELECT SUM(numMinutes) numMinutes, dateWhenTrackedFor FROM vacation_day WHERE userID = %d + AND dateWhenTrackedFor >= %d AND dateWhenTrackedFor <= %d + GROUP BY dateWhenTrackedFor ORDER BY dateWhenTrackedFor ASC', $userID, $fromTimestamp, $toTimestamp); + + $data = array(); + foreach ($result as $row) { + $data[] = $row; + } + + $data = $this->fillEmptyDays($fromTimestamp, $toTimestamp, $data); + $data = $this->sortData($data); + $data = $this->timestampToReadableDate($data); + + $this->chartJsonData = json_encode($data); + } + } + + private function timestampToReadableDate($data) { + foreach ($data as &$row) { + $row['dateWhenTrackedFor'] = date('d-m-y', $row['dateWhenTrackedFor']); + } + return $data; + } + + private function sortData($data) { + usort($data, function($a, $b) { + return $a['dateWhenTrackedFor'] < $b['dateWhenTrackedFor'] ? -1 : 1; + }); + return $data; + } + + private function fillEmptyDays($fromTimestamp, $toTimestamp, $data) { + $rangeOfDays = ($toTimestamp - $fromTimestamp) / VacationTimeUtils::NUM_SECONDS_IN_DAY + 1; + $dateWhenTrackedForColumn = array_column($data, 'dateWhenTrackedFor'); + + for ($i = 0; $i < $rangeOfDays; $i++) { + $currentDayInRangeDate = $fromTimestamp + $i * VacationTimeUtils::NUM_SECONDS_IN_DAY; + if (array_search($currentDayInRangeDate, $dateWhenTrackedForColumn) === false) { + $data[] = ['numMinutes' => '0', 'dateWhenTrackedFor' => $currentDayInRangeDate]; + } + } + return $data; + } + + public function getChartJsonData() { + return $this->chartJsonData; + } + + private function getTimestampFromInput($dateInput) { + $dateInput = trim($dateInput); + $pieces = explode('/', $dateInput); + + $day = $pieces[1]; + $month = $pieces[0]; + $year = $pieces[2]; + + return VacationTimeUtils::getTimestamp($day, $month, $year); + } +} diff --git a/src/applications/vacation/storage/VacationDAO.php b/src/applications/vacation/storage/VacationDAO.php new file mode 100644 index 0000000000..7ff159203b --- /dev/null +++ b/src/applications/vacation/storage/VacationDAO.php @@ -0,0 +1,7 @@ + false, + self::CONFIG_COLUMN_SCHEMA => array( + 'id' => 'auto', + 'userID' => 'uint32', + 'numMinutes' => 'uint32', + 'dateWhenTrackedFor' => 'uint32', + 'realDateWhenTracked' => 'uint32', + ), + self::CONFIG_KEY_SCHEMA => array( + 'PRIMARY' => array( + 'columns' => array('id'), + 'unique' => true, + ), + ), self::CONFIG_NO_TABLE => true + + ); + } +} + // + parent::getConfiguration() \ No newline at end of file diff --git a/src/infrastructure/customfield/datasource/PhabricatorStandardSelectCustomFieldDatasource.php b/src/infrastructure/customfield/datasource/PhabricatorStandardSelectCustomFieldDatasource.php index fee2c34596..feff8095c1 100644 --- a/src/infrastructure/customfield/datasource/PhabricatorStandardSelectCustomFieldDatasource.php +++ b/src/infrastructure/customfield/datasource/PhabricatorStandardSelectCustomFieldDatasource.php @@ -37,7 +37,7 @@ public function loadResults() { } $role = $this->getParameter('role'); - if (!strlen($role)) { + if ($role === null || $role === '') { throw new Exception(pht('No custom field role specified.')); } @@ -45,7 +45,7 @@ public function loadResults() { $field_list = PhabricatorCustomField::getObjectFields($object, $role); $field_key = $this->getParameter('key'); - if (!strlen($field_key)) { + if ($field_key === null || $field_key === '') { throw new Exception(pht('No custom field key specified.')); } diff --git a/src/infrastructure/customfield/field/PhabricatorCustomField.php b/src/infrastructure/customfield/field/PhabricatorCustomField.php index 9e2bf6895f..5039822511 100644 --- a/src/infrastructure/customfield/field/PhabricatorCustomField.php +++ b/src/infrastructure/customfield/field/PhabricatorCustomField.php @@ -1669,7 +1669,7 @@ private static function adjustCustomFieldsForObjectSubtype( $map = array(); foreach ($fields as $field) { $modern_key = $field->getModernFieldKey(); - if (!strlen($modern_key)) { + if ($modern_key === null || $modern_key === '') { continue; } diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomField.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomField.php index 4c0bce861b..a897c343db 100644 --- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomField.php +++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomField.php @@ -268,7 +268,7 @@ public function shouldAppearInEditView() { public function readValueFromRequest(AphrontRequest $request) { $value = $request->getStr($this->getFieldKey()); - if (!strlen($value)) { + if ($value === '') { $value = null; } $this->setFieldValue($value); @@ -301,7 +301,7 @@ public function shouldAppearInPropertyView() { } public function renderPropertyViewValue(array $handles) { - if (!strlen($this->getFieldValue())) { + if ($this->getFieldValue() === '') { return null; } return $this->getFieldValue(); @@ -389,7 +389,7 @@ protected function isValueEmpty($value) { if (is_array($value)) { return empty($value); } - return !strlen($value); + return $value === null || $value === ''; } public function getApplicationTransactionTitle( @@ -477,7 +477,7 @@ public function updateAbstractDocument( } $field_value = $this->getFieldValue(); - if (strlen($field_value)) { + if ($field_value !== null && $field_value !== '') { $document->addField($field_key, $field_value); } } diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldBool.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldBool.php index f1d1371a7d..54316a242c 100644 --- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldBool.php +++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldBool.php @@ -11,7 +11,7 @@ public function buildFieldIndexes() { $indexes = array(); $value = $this->getFieldValue(); - if (strlen($value)) { + if ($value !== null && $value !== '') { $indexes[] = $this->newNumericIndex((int)$value); } @@ -36,7 +36,7 @@ public function getValueForStorage() { } public function setValueFromStorage($value) { - if (strlen($value)) { + if ($value !== null && $value !== '') { $value = (bool)$value; } else { $value = null; diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldCredential.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldCredential.php index c2f958f228..a2163383ec 100644 --- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldCredential.php +++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldCredential.php @@ -11,7 +11,7 @@ public function buildFieldIndexes() { $indexes = array(); $value = $this->getFieldValue(); - if (strlen($value)) { + if ($value !== null && $value !== '') { $indexes[] = $this->newStringIndex($value); } diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldDate.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldDate.php index 4aba7543e7..932272640a 100644 --- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldDate.php +++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldDate.php @@ -11,7 +11,7 @@ public function buildFieldIndexes() { $indexes = array(); $value = $this->getFieldValue(); - if (strlen($value)) { + if ($value !== null && $value !== '') { $indexes[] = $this->newNumericIndex((int)$value); } @@ -24,7 +24,7 @@ public function buildOrderIndex() { public function getValueForStorage() { $value = $this->getFieldValue(); - if (strlen($value)) { + if ($value !== null && $value !== '') { return (int)$value; } else { return null; @@ -32,7 +32,7 @@ public function getValueForStorage() { } public function setValueFromStorage($value) { - if (strlen($value)) { + if ($value !== null && $value !== '') { $value = (int)$value; } else { $value = null; @@ -74,7 +74,7 @@ private function newDateControl() { // specify as a string. Parse the string into an epoch. $value = $this->getFieldValue(); - if (!ctype_digit($value)) { + if (!ctype_digit(phutil_string_cast($value))) { $value = PhabricatorTime::parseLocalTime($value, $this->getViewer()); } @@ -109,14 +109,14 @@ public function applyApplicationSearchConstraintToQuery( } $min_str = idx($value, 'min', ''); - if (strlen($min_str)) { + if ($min_str !== null && $min_str !== '') { $min = PhabricatorTime::parseLocalTime($min_str, $viewer); } else { $min = null; } $max_str = idx($value, 'max', ''); - if (strlen($max_str)) { + if ($max_str !== null && $max_str !== '') { $max = PhabricatorTime::parseLocalTime($max_str, $viewer); } else { $max = null; diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldInt.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldInt.php index c485ace162..cc095c45e1 100644 --- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldInt.php +++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldInt.php @@ -11,7 +11,7 @@ public function buildFieldIndexes() { $indexes = array(); $value = $this->getFieldValue(); - if (strlen($value)) { + if ($value !== null && $value !== '') { $indexes[] = $this->newNumericIndex((int)$value); } @@ -32,7 +32,7 @@ public function getValueForStorage() { } public function setValueFromStorage($value) { - if (strlen($value)) { + if ($value !== null && $value !== '') { $value = (int)$value; } else { $value = null; @@ -52,7 +52,7 @@ public function applyApplicationSearchConstraintToQuery( PhabricatorCursorPagedPolicyAwareQuery $query, $value) { - if (strlen($value)) { + if ($value !== null && $value !== '') { $query->withApplicationSearchContainsConstraint( $this->newNumericIndex(null), $value); @@ -83,7 +83,7 @@ public function validateApplicationTransactions( foreach ($xactions as $xaction) { $value = $xaction->getNewValue(); - if (strlen($value)) { + if ($value !== null && $value !== '') { if (!preg_match('/^-?\d+/', $value)) { $errors[] = new PhabricatorApplicationTransactionValidationError( $type, @@ -101,14 +101,14 @@ public function validateApplicationTransactions( public function getApplicationTransactionHasEffect( PhabricatorApplicationTransaction $xaction) { - $old = $xaction->getOldValue(); - $new = $xaction->getNewValue(); + $old = phutil_string_cast($xaction->getOldValue()); + $new = phutil_string_cast($xaction->getNewValue()); if (!strlen($old) && strlen($new)) { return true; - } else if (strlen($old) && !strlen($new)) { + } elseif (strlen($old) && !strlen($new)) { return true; } else { - return ((int)$old !== (int)$new); + return (int)$xaction->getOldValue() !== $xaction->getNewValue(); } } diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldLink.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldLink.php index 146f34ab07..61d76b0e02 100644 --- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldLink.php +++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldLink.php @@ -11,7 +11,7 @@ public function buildFieldIndexes() { $indexes = array(); $value = $this->getFieldValue(); - if (strlen($value)) { + if ($value !== null && $value !== '') { $indexes[] = $this->newStringIndex($value); } @@ -21,7 +21,7 @@ public function buildFieldIndexes() { public function renderPropertyViewValue(array $handles) { $value = $this->getFieldValue(); - if (!strlen($value)) { + if ($value === null || $value === '') { return null; } diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldSelect.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldSelect.php index 5957afe56a..66731cd66d 100644 --- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldSelect.php +++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldSelect.php @@ -11,7 +11,7 @@ public function buildFieldIndexes() { $indexes = array(); $value = $this->getFieldValue(); - if (strlen($value)) { + if ($value !== null && $value !== '') { $indexes[] = $this->newStringIndex($value); } @@ -73,7 +73,7 @@ public function renderEditControl(array $handles) { } public function renderPropertyViewValue(array $handles) { - if (!strlen($this->getFieldValue())) { + if ($this->getFieldValue() === null || $this->getFieldValue() === '') { return null; } return idx($this->getOptions(), $this->getFieldValue()); diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldText.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldText.php index 56164bb7b5..cff4a5185a 100644 --- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldText.php +++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldText.php @@ -11,7 +11,7 @@ public function buildFieldIndexes() { $indexes = array(); $value = $this->getFieldValue(); - if (strlen($value)) { + if ($value !== null && $value !== '') { $indexes[] = $this->newStringIndex($value); } @@ -30,7 +30,7 @@ public function applyApplicationSearchConstraintToQuery( PhabricatorCursorPagedPolicyAwareQuery $query, $value) { - if (strlen($value)) { + if ($value !== null && $value !== '') { $query->withApplicationSearchContainsConstraint( $this->newStringIndex(null), $value); diff --git a/src/infrastructure/graph/ManiphestTaskGraph.php b/src/infrastructure/graph/ManiphestTaskGraph.php index c237fd2c35..b52464c270 100644 --- a/src/infrastructure/graph/ManiphestTaskGraph.php +++ b/src/infrastructure/graph/ManiphestTaskGraph.php @@ -5,8 +5,17 @@ final class ManiphestTaskGraph private $seedMaps = array(); private $isStandalone; - + private $subtaskType = ManiphestTaskDependsOnTaskEdgeType::EDGECONST; + private $parentMarkerTip = 'Direct Parent'; + private $childMarkerTip = 'Direct Subtask'; + protected function getEdgeTypes() { + if ($this->subtaskType == ManiphestTaskBlockerEdgeType::EDGECONST) { + return array( + ManiphestTaskBlockedEdgeType::EDGECONST, + ManiphestTaskBlockerEdgeType::EDGECONST, + ); + } return array( ManiphestTaskDependedOnByTaskEdgeType::EDGECONST, ManiphestTaskDependsOnTaskEdgeType::EDGECONST, @@ -14,7 +23,7 @@ protected function getEdgeTypes() { } protected function getParentEdgeType() { - return ManiphestTaskDependsOnTaskEdgeType::EDGECONST; + return $this->subtaskType; } protected function newQuery() { @@ -25,6 +34,15 @@ protected function isClosed($object) { return $object->isClosed(); } + public function setSubtaskType(int $subtask_type) { + $this->subtaskType = $subtask_type; + if ($subtask_type == ManiphestTaskBlockerEdgeType::EDGECONST) { + $this->parentMarkerTip = 'Direct Blocked'; + $this->childMarkerTip = 'Direct Blocker'; + } + return $this; + } + public function setIsStandalone($is_standalone) { $this->isStandalone = $is_standalone; return $this; @@ -103,10 +121,10 @@ protected function newTableRow($phid, $object, $trace) { if ($this->isParentTask($phid)) { $marker = 'fa-chevron-circle-up bluegrey'; - $marker_tip = pht('Direct Parent'); - } else if ($this->isChildTask($phid)) { + $marker_tip = pht($this->parentMarkerTip); + } elseif ($this->isChildTask($phid)) { $marker = 'fa-chevron-circle-down bluegrey'; - $marker_tip = pht('Direct Subtask'); + $marker_tip = pht($this->childMarkerTip); } else { $marker = null; } @@ -178,12 +196,16 @@ protected function newTable(AphrontTableView $table) { } private function isParentTask($task_phid) { - $map = $this->getSeedMap(ManiphestTaskDependedOnByTaskEdgeType::EDGECONST); + if ($this->subtaskType == ManiphestTaskBlockerEdgeType::EDGECONST) { + $map = $this->getSeedMap(ManiphestTaskBlockedEdgeType::EDGECONST); + } else { + $map = $this->getSeedMap(ManiphestTaskDependedOnByTaskEdgeType::EDGECONST); + } return isset($map[$task_phid]); } private function isChildTask($task_phid) { - $map = $this->getSeedMap(ManiphestTaskDependsOnTaskEdgeType::EDGECONST); + $map = $this->getSeedMap($this->subtaskType); return isset($map[$task_phid]); } diff --git a/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php b/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php index f68635860a..72c771ff42 100644 --- a/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php +++ b/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php @@ -73,6 +73,7 @@ public function getOldPatches() { 'db.phame' => array(), 'db.phriction' => array(), 'db.project' => array(), + 'db.role' => array(), 'db.repository' => array(), 'db.search' => array(), 'db.slowvote' => array(), diff --git a/src/infrastructure/time/PhabricatorTime.php b/src/infrastructure/time/PhabricatorTime.php index 67378ca8df..0af0b2beb7 100644 --- a/src/infrastructure/time/PhabricatorTime.php +++ b/src/infrastructure/time/PhabricatorTime.php @@ -52,7 +52,7 @@ public static function parseLocalTime($time, PhabricatorUser $user) { $old_zone = date_default_timezone_get(); date_default_timezone_set($user->getTimezoneIdentifier()); - $timestamp = (int)strtotime($time, self::getNow()); + $timestamp = (int)strtotime(phutil_string_cast($time), self::getNow()); if ($timestamp <= 0) { $timestamp = null; } diff --git a/webroot/rsrc/css/application/timetracker/images/ui-icons_444444_256x240.png b/webroot/rsrc/css/application/timetracker/images/ui-icons_444444_256x240.png new file mode 100644 index 0000000000..3d1b5f45d8 Binary files /dev/null and b/webroot/rsrc/css/application/timetracker/images/ui-icons_444444_256x240.png differ diff --git a/webroot/rsrc/css/application/timetracker/images/ui-icons_555555_256x240.png b/webroot/rsrc/css/application/timetracker/images/ui-icons_555555_256x240.png new file mode 100644 index 0000000000..6d7593c973 Binary files /dev/null and b/webroot/rsrc/css/application/timetracker/images/ui-icons_555555_256x240.png differ diff --git a/webroot/rsrc/css/application/timetracker/images/ui-icons_777620_256x240.png b/webroot/rsrc/css/application/timetracker/images/ui-icons_777620_256x240.png new file mode 100644 index 0000000000..95082d26dc Binary files /dev/null and b/webroot/rsrc/css/application/timetracker/images/ui-icons_777620_256x240.png differ diff --git a/webroot/rsrc/css/application/timetracker/images/ui-icons_777777_256x240.png b/webroot/rsrc/css/application/timetracker/images/ui-icons_777777_256x240.png new file mode 100644 index 0000000000..fede5688db Binary files /dev/null and b/webroot/rsrc/css/application/timetracker/images/ui-icons_777777_256x240.png differ diff --git a/webroot/rsrc/css/application/timetracker/images/ui-icons_cc0000_256x240.png b/webroot/rsrc/css/application/timetracker/images/ui-icons_cc0000_256x240.png new file mode 100644 index 0000000000..e23c3e3b4c Binary files /dev/null and b/webroot/rsrc/css/application/timetracker/images/ui-icons_cc0000_256x240.png differ diff --git a/webroot/rsrc/css/application/timetracker/images/ui-icons_ffffff_256x240.png b/webroot/rsrc/css/application/timetracker/images/ui-icons_ffffff_256x240.png new file mode 100644 index 0000000000..fb914919fe Binary files /dev/null and b/webroot/rsrc/css/application/timetracker/images/ui-icons_ffffff_256x240.png differ diff --git a/webroot/rsrc/css/application/timetracker/jquery-ui.css b/webroot/rsrc/css/application/timetracker/jquery-ui.css new file mode 100644 index 0000000000..fb181dc744 --- /dev/null +++ b/webroot/rsrc/css/application/timetracker/jquery-ui.css @@ -0,0 +1,695 @@ +/** + * @provides jquery-ui-css + */ + +/* Layout helpers +----------------------------------*/ +.ui-helper-hidden { + display: none; +} +.ui-helper-hidden-accessible { + border: 0; + clip: rect(0 0 0 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; +} +.ui-helper-reset { + margin: 0; + padding: 0; + border: 0; + outline: 0; + line-height: 1.3; + text-decoration: none; + font-size: 100%; + list-style: none; +} +.ui-helper-clearfix:before, +.ui-helper-clearfix:after { + content: ""; + display: table; + border-collapse: collapse; +} +.ui-helper-clearfix:after { + clear: both; +} +.ui-helper-zfix { + width: 100%; + height: 100%; + top: 0; + left: 0; + position: absolute; + opacity: 0; + filter:Alpha(Opacity=0); /* support: IE8 */ +} + +.ui-front { + z-index: 100; +} + + +/* Interaction Cues +----------------------------------*/ +.ui-state-disabled { + cursor: default !important; + pointer-events: none; +} + + +/* Icons +----------------------------------*/ +.ui-icon { + display: inline-block; + vertical-align: middle; + margin-top: -.25em; + position: relative; + text-indent: -99999px; + overflow: hidden; + background-repeat: no-repeat; +} + +.ui-widget-icon-block { + left: 50%; + margin-left: -8px; + display: block; +} + +/* Misc visuals +----------------------------------*/ + +/* Overlays */ +.ui-widget-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; +} +.ui-datepicker { + width: 17em; + padding: .2em .2em 0; + display: none; +} +.ui-datepicker .ui-datepicker-header { + position: relative; + padding: .2em 0; +} +.ui-datepicker .ui-datepicker-prev, +.ui-datepicker .ui-datepicker-next { + position: absolute; + top: 2px; + width: 1.8em; + height: 1.8em; +} +.ui-datepicker .ui-datepicker-prev-hover, +.ui-datepicker .ui-datepicker-next-hover { + top: 1px; +} +.ui-datepicker .ui-datepicker-prev { + left: 2px; +} +.ui-datepicker .ui-datepicker-next { + right: 2px; +} +.ui-datepicker .ui-datepicker-prev-hover { + left: 1px; +} +.ui-datepicker .ui-datepicker-next-hover { + right: 1px; +} +.ui-datepicker .ui-datepicker-prev span, +.ui-datepicker .ui-datepicker-next span { + display: block; + position: absolute; + left: 50%; + margin-left: -8px; + top: 50%; + margin-top: -8px; +} +.ui-datepicker .ui-datepicker-title { + margin: 0 2.3em; + line-height: 1.8em; + text-align: center; +} +.ui-datepicker .ui-datepicker-title select { + font-size: 1em; + margin: 1px 0; +} +.ui-datepicker select.ui-datepicker-month, +.ui-datepicker select.ui-datepicker-year { + width: 45%; +} +.ui-datepicker table { + width: 100%; + font-size: .9em; + border-collapse: collapse; + margin: 0 0 .4em; +} +.ui-datepicker th { + padding: .7em .3em; + text-align: center; + font-weight: bold; + border: 0; +} +.ui-datepicker td { + border: 0; + padding: 1px; +} +.ui-datepicker td span, +.ui-datepicker td a { + display: block; + padding: .2em; + text-align: right; + text-decoration: none; +} +.ui-datepicker .ui-datepicker-buttonpane { + background-image: none; + margin: .7em 0 0 0; + padding: 0 .2em; + border-left: 0; + border-right: 0; + border-bottom: 0; +} +.ui-datepicker .ui-datepicker-buttonpane button { + float: right; + margin: .5em .2em .4em; + cursor: pointer; + padding: .2em .6em .3em .6em; + width: auto; + overflow: visible; +} +.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current { + float: left; +} + +/* with multiple calendars */ +.ui-datepicker.ui-datepicker-multi { + width: auto; +} +.ui-datepicker-multi .ui-datepicker-group { + float: left; +} +.ui-datepicker-multi .ui-datepicker-group table { + width: 95%; + margin: 0 auto .4em; +} +.ui-datepicker-multi-2 .ui-datepicker-group { + width: 50%; +} +.ui-datepicker-multi-3 .ui-datepicker-group { + width: 33.3%; +} +.ui-datepicker-multi-4 .ui-datepicker-group { + width: 25%; +} +.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header, +.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header { + border-left-width: 0; +} +.ui-datepicker-multi .ui-datepicker-buttonpane { + clear: left; +} +.ui-datepicker-row-break { + clear: both; + width: 100%; + font-size: 0; +} + +/* RTL support */ +.ui-datepicker-rtl { + direction: rtl; +} +.ui-datepicker-rtl .ui-datepicker-prev { + right: 2px; + left: auto; +} +.ui-datepicker-rtl .ui-datepicker-next { + left: 2px; + right: auto; +} +.ui-datepicker-rtl .ui-datepicker-prev:hover { + right: 1px; + left: auto; +} +.ui-datepicker-rtl .ui-datepicker-next:hover { + left: 1px; + right: auto; +} +.ui-datepicker-rtl .ui-datepicker-buttonpane { + clear: right; +} +.ui-datepicker-rtl .ui-datepicker-buttonpane button { + float: left; +} +.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current, +.ui-datepicker-rtl .ui-datepicker-group { + float: right; +} +.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header, +.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header { + border-right-width: 0; + border-left-width: 1px; +} + +/* Icons */ +.ui-datepicker .ui-icon { + display: block; + text-indent: -99999px; + overflow: hidden; + background-repeat: no-repeat; + left: .5em; + top: .3em; +} + +/* Component containers +----------------------------------*/ +.ui-widget { + font-family: Arial,Helvetica,sans-serif; + font-size: 1em; +} +.ui-widget .ui-widget { + font-size: 1em; +} +.ui-widget input, +.ui-widget select, +.ui-widget textarea, +.ui-widget button { + font-family: Arial,Helvetica,sans-serif; + font-size: 1em; +} +.ui-widget.ui-widget-content { + border: 1px solid #c5c5c5; +} +.ui-widget-content { + border: 1px solid #dddddd; + background: #ffffff; + color: #333333; +} +.ui-widget-content a { + color: #333333; +} +.ui-widget-header { + border: 1px solid #dddddd; + background: #e9e9e9; + color: #333333; + font-weight: bold; +} +.ui-widget-header a { + color: #333333; +} + +/* Interaction states +----------------------------------*/ +.ui-state-default, +.ui-widget-content .ui-state-default, +.ui-widget-header .ui-state-default, +.ui-button, + +/* We use html here because we need a greater specificity to make sure disabled +works properly when clicked or hovered */ +html .ui-button.ui-state-disabled:hover, +html .ui-button.ui-state-disabled:active { + border: 1px solid #c5c5c5; + background: #f6f6f6; + font-weight: normal; + color: #454545; +} +.ui-state-default a, +.ui-state-default a:link, +.ui-state-default a:visited, +a.ui-button, +a:link.ui-button, +a:visited.ui-button, +.ui-button { + color: #454545; + text-decoration: none; +} +.ui-state-hover, +.ui-widget-content .ui-state-hover, +.ui-widget-header .ui-state-hover, +.ui-state-focus, +.ui-widget-content .ui-state-focus, +.ui-widget-header .ui-state-focus, +.ui-button:hover, +.ui-button:focus { + border: 1px solid #cccccc; + background: #ededed; + font-weight: normal; + color: #2b2b2b; +} +.ui-state-hover a, +.ui-state-hover a:hover, +.ui-state-hover a:link, +.ui-state-hover a:visited, +.ui-state-focus a, +.ui-state-focus a:hover, +.ui-state-focus a:link, +.ui-state-focus a:visited, +a.ui-button:hover, +a.ui-button:focus { + color: #2b2b2b; + text-decoration: none; +} + +.ui-visual-focus { + box-shadow: 0 0 3px 1px rgb(94, 158, 214); +} +.ui-state-active, +.ui-widget-content .ui-state-active, +.ui-widget-header .ui-state-active, +a.ui-button:active, +.ui-button:active, +.ui-button.ui-state-active:hover { + border: 1px solid #003eff; + background: #007fff; + font-weight: normal; + color: #ffffff; +} +.ui-icon-background, +.ui-state-active .ui-icon-background { + border: #003eff; + background-color: #ffffff; +} +.ui-state-active a, +.ui-state-active a:link, +.ui-state-active a:visited { + color: #ffffff; + text-decoration: none; +} + +/* Interaction Cues +----------------------------------*/ +.ui-state-highlight, +.ui-widget-content .ui-state-highlight, +.ui-widget-header .ui-state-highlight { + border: 1px solid #dad55e; + background: #fffa90; + color: #777620; +} +.ui-state-checked { + border: 1px solid #dad55e; + background: #fffa90; +} +.ui-state-highlight a, +.ui-widget-content .ui-state-highlight a, +.ui-widget-header .ui-state-highlight a { + color: #777620; +} +.ui-state-error, +.ui-widget-content .ui-state-error, +.ui-widget-header .ui-state-error { + border: 1px solid #f1a899; + background: #fddfdf; + color: #5f3f3f; +} +.ui-state-error a, +.ui-widget-content .ui-state-error a, +.ui-widget-header .ui-state-error a { + color: #5f3f3f; +} +.ui-state-error-text, +.ui-widget-content .ui-state-error-text, +.ui-widget-header .ui-state-error-text { + color: #5f3f3f; +} +.ui-priority-primary, +.ui-widget-content .ui-priority-primary, +.ui-widget-header .ui-priority-primary { + font-weight: bold; +} +.ui-priority-secondary, +.ui-widget-content .ui-priority-secondary, +.ui-widget-header .ui-priority-secondary { + opacity: .7; + filter:Alpha(Opacity=70); /* support: IE8 */ + font-weight: normal; +} +.ui-state-disabled, +.ui-widget-content .ui-state-disabled, +.ui-widget-header .ui-state-disabled { + opacity: .35; + filter:Alpha(Opacity=35); /* support: IE8 */ + background-image: none; +} +.ui-state-disabled .ui-icon { + filter:Alpha(Opacity=35); /* support: IE8 - See #6059 */ +} + +/* Icons +----------------------------------*/ + +/* states and images */ +.ui-icon { + width: 16px; + height: 16px; +} +.ui-icon, +.ui-widget-content .ui-icon { + background-image: url("images/ui-icons_444444_256x240.png"); +} +.ui-widget-header .ui-icon { + background-image: url("images/ui-icons_444444_256x240.png"); +} +.ui-state-hover .ui-icon, +.ui-state-focus .ui-icon, +.ui-button:hover .ui-icon, +.ui-button:focus .ui-icon { + background-image: url("images/ui-icons_555555_256x240.png"); +} +.ui-state-active .ui-icon, +.ui-button:active .ui-icon { + background-image: url("images/ui-icons_ffffff_256x240.png"); +} +.ui-state-highlight .ui-icon, +.ui-button .ui-state-highlight.ui-icon { + background-image: url("images/ui-icons_777620_256x240.png"); +} +.ui-state-error .ui-icon, +.ui-state-error-text .ui-icon { + background-image: url("images/ui-icons_cc0000_256x240.png"); +} +.ui-button .ui-icon { + background-image: url("images/ui-icons_777777_256x240.png"); +} + +/* positioning */ +.ui-icon-blank { background-position: 16px 16px; } +.ui-icon-caret-1-n { background-position: 0 0; } +.ui-icon-caret-1-ne { background-position: -16px 0; } +.ui-icon-caret-1-e { background-position: -32px 0; } +.ui-icon-caret-1-se { background-position: -48px 0; } +.ui-icon-caret-1-s { background-position: -65px 0; } +.ui-icon-caret-1-sw { background-position: -80px 0; } +.ui-icon-caret-1-w { background-position: -96px 0; } +.ui-icon-caret-1-nw { background-position: -112px 0; } +.ui-icon-caret-2-n-s { background-position: -128px 0; } +.ui-icon-caret-2-e-w { background-position: -144px 0; } +.ui-icon-triangle-1-n { background-position: 0 -16px; } +.ui-icon-triangle-1-ne { background-position: -16px -16px; } +.ui-icon-triangle-1-e { background-position: -32px -16px; } +.ui-icon-triangle-1-se { background-position: -48px -16px; } +.ui-icon-triangle-1-s { background-position: -65px -16px; } +.ui-icon-triangle-1-sw { background-position: -80px -16px; } +.ui-icon-triangle-1-w { background-position: -96px -16px; } +.ui-icon-triangle-1-nw { background-position: -112px -16px; } +.ui-icon-triangle-2-n-s { background-position: -128px -16px; } +.ui-icon-triangle-2-e-w { background-position: -144px -16px; } +.ui-icon-arrow-1-n { background-position: 0 -32px; } +.ui-icon-arrow-1-ne { background-position: -16px -32px; } +.ui-icon-arrow-1-e { background-position: -32px -32px; } +.ui-icon-arrow-1-se { background-position: -48px -32px; } +.ui-icon-arrow-1-s { background-position: -65px -32px; } +.ui-icon-arrow-1-sw { background-position: -80px -32px; } +.ui-icon-arrow-1-w { background-position: -96px -32px; } +.ui-icon-arrow-1-nw { background-position: -112px -32px; } +.ui-icon-arrow-2-n-s { background-position: -128px -32px; } +.ui-icon-arrow-2-ne-sw { background-position: -144px -32px; } +.ui-icon-arrow-2-e-w { background-position: -160px -32px; } +.ui-icon-arrow-2-se-nw { background-position: -176px -32px; } +.ui-icon-arrowstop-1-n { background-position: -192px -32px; } +.ui-icon-arrowstop-1-e { background-position: -208px -32px; } +.ui-icon-arrowstop-1-s { background-position: -224px -32px; } +.ui-icon-arrowstop-1-w { background-position: -240px -32px; } +.ui-icon-arrowthick-1-n { background-position: 1px -48px; } +.ui-icon-arrowthick-1-ne { background-position: -16px -48px; } +.ui-icon-arrowthick-1-e { background-position: -32px -48px; } +.ui-icon-arrowthick-1-se { background-position: -48px -48px; } +.ui-icon-arrowthick-1-s { background-position: -64px -48px; } +.ui-icon-arrowthick-1-sw { background-position: -80px -48px; } +.ui-icon-arrowthick-1-w { background-position: -96px -48px; } +.ui-icon-arrowthick-1-nw { background-position: -112px -48px; } +.ui-icon-arrowthick-2-n-s { background-position: -128px -48px; } +.ui-icon-arrowthick-2-ne-sw { background-position: -144px -48px; } +.ui-icon-arrowthick-2-e-w { background-position: -160px -48px; } +.ui-icon-arrowthick-2-se-nw { background-position: -176px -48px; } +.ui-icon-arrowthickstop-1-n { background-position: -192px -48px; } +.ui-icon-arrowthickstop-1-e { background-position: -208px -48px; } +.ui-icon-arrowthickstop-1-s { background-position: -224px -48px; } +.ui-icon-arrowthickstop-1-w { background-position: -240px -48px; } +.ui-icon-arrowreturnthick-1-w { background-position: 0 -64px; } +.ui-icon-arrowreturnthick-1-n { background-position: -16px -64px; } +.ui-icon-arrowreturnthick-1-e { background-position: -32px -64px; } +.ui-icon-arrowreturnthick-1-s { background-position: -48px -64px; } +.ui-icon-arrowreturn-1-w { background-position: -64px -64px; } +.ui-icon-arrowreturn-1-n { background-position: -80px -64px; } +.ui-icon-arrowreturn-1-e { background-position: -96px -64px; } +.ui-icon-arrowreturn-1-s { background-position: -112px -64px; } +.ui-icon-arrowrefresh-1-w { background-position: -128px -64px; } +.ui-icon-arrowrefresh-1-n { background-position: -144px -64px; } +.ui-icon-arrowrefresh-1-e { background-position: -160px -64px; } +.ui-icon-arrowrefresh-1-s { background-position: -176px -64px; } +.ui-icon-arrow-4 { background-position: 0 -80px; } +.ui-icon-arrow-4-diag { background-position: -16px -80px; } +.ui-icon-extlink { background-position: -32px -80px; } +.ui-icon-newwin { background-position: -48px -80px; } +.ui-icon-refresh { background-position: -64px -80px; } +.ui-icon-shuffle { background-position: -80px -80px; } +.ui-icon-transfer-e-w { background-position: -96px -80px; } +.ui-icon-transferthick-e-w { background-position: -112px -80px; } +.ui-icon-folder-collapsed { background-position: 0 -96px; } +.ui-icon-folder-open { background-position: -16px -96px; } +.ui-icon-document { background-position: -32px -96px; } +.ui-icon-document-b { background-position: -48px -96px; } +.ui-icon-note { background-position: -64px -96px; } +.ui-icon-mail-closed { background-position: -80px -96px; } +.ui-icon-mail-open { background-position: -96px -96px; } +.ui-icon-suitcase { background-position: -112px -96px; } +.ui-icon-comment { background-position: -128px -96px; } +.ui-icon-person { background-position: -144px -96px; } +.ui-icon-print { background-position: -160px -96px; } +.ui-icon-trash { background-position: -176px -96px; } +.ui-icon-locked { background-position: -192px -96px; } +.ui-icon-unlocked { background-position: -208px -96px; } +.ui-icon-bookmark { background-position: -224px -96px; } +.ui-icon-tag { background-position: -240px -96px; } +.ui-icon-home { background-position: 0 -112px; } +.ui-icon-flag { background-position: -16px -112px; } +.ui-icon-calendar { background-position: -32px -112px; } +.ui-icon-cart { background-position: -48px -112px; } +.ui-icon-pencil { background-position: -64px -112px; } +.ui-icon-clock { background-position: -80px -112px; } +.ui-icon-disk { background-position: -96px -112px; } +.ui-icon-calculator { background-position: -112px -112px; } +.ui-icon-zoomin { background-position: -128px -112px; } +.ui-icon-zoomout { background-position: -144px -112px; } +.ui-icon-search { background-position: -160px -112px; } +.ui-icon-wrench { background-position: -176px -112px; } +.ui-icon-gear { background-position: -192px -112px; } +.ui-icon-heart { background-position: -208px -112px; } +.ui-icon-star { background-position: -224px -112px; } +.ui-icon-link { background-position: -240px -112px; } +.ui-icon-cancel { background-position: 0 -128px; } +.ui-icon-plus { background-position: -16px -128px; } +.ui-icon-plusthick { background-position: -32px -128px; } +.ui-icon-minus { background-position: -48px -128px; } +.ui-icon-minusthick { background-position: -64px -128px; } +.ui-icon-close { background-position: -80px -128px; } +.ui-icon-closethick { background-position: -96px -128px; } +.ui-icon-key { background-position: -112px -128px; } +.ui-icon-lightbulb { background-position: -128px -128px; } +.ui-icon-scissors { background-position: -144px -128px; } +.ui-icon-clipboard { background-position: -160px -128px; } +.ui-icon-copy { background-position: -176px -128px; } +.ui-icon-contact { background-position: -192px -128px; } +.ui-icon-image { background-position: -208px -128px; } +.ui-icon-video { background-position: -224px -128px; } +.ui-icon-script { background-position: -240px -128px; } +.ui-icon-alert { background-position: 0 -144px; } +.ui-icon-info { background-position: -16px -144px; } +.ui-icon-notice { background-position: -32px -144px; } +.ui-icon-help { background-position: -48px -144px; } +.ui-icon-check { background-position: -64px -144px; } +.ui-icon-bullet { background-position: -80px -144px; } +.ui-icon-radio-on { background-position: -96px -144px; } +.ui-icon-radio-off { background-position: -112px -144px; } +.ui-icon-pin-w { background-position: -128px -144px; } +.ui-icon-pin-s { background-position: -144px -144px; } +.ui-icon-play { background-position: 0 -160px; } +.ui-icon-pause { background-position: -16px -160px; } +.ui-icon-seek-next { background-position: -32px -160px; } +.ui-icon-seek-prev { background-position: -48px -160px; } +.ui-icon-seek-end { background-position: -64px -160px; } +.ui-icon-seek-start { background-position: -80px -160px; } +/* ui-icon-seek-first is deprecated, use ui-icon-seek-start instead */ +.ui-icon-seek-first { background-position: -80px -160px; } +.ui-icon-stop { background-position: -96px -160px; } +.ui-icon-eject { background-position: -112px -160px; } +.ui-icon-volume-off { background-position: -128px -160px; } +.ui-icon-volume-on { background-position: -144px -160px; } +.ui-icon-power { background-position: 0 -176px; } +.ui-icon-signal-diag { background-position: -16px -176px; } +.ui-icon-signal { background-position: -32px -176px; } +.ui-icon-battery-0 { background-position: -48px -176px; } +.ui-icon-battery-1 { background-position: -64px -176px; } +.ui-icon-battery-2 { background-position: -80px -176px; } +.ui-icon-battery-3 { background-position: -96px -176px; } +.ui-icon-circle-plus { background-position: 0 -192px; } +.ui-icon-circle-minus { background-position: -16px -192px; } +.ui-icon-circle-close { background-position: -32px -192px; } +.ui-icon-circle-triangle-e { background-position: -48px -192px; } +.ui-icon-circle-triangle-s { background-position: -64px -192px; } +.ui-icon-circle-triangle-w { background-position: -80px -192px; } +.ui-icon-circle-triangle-n { background-position: -96px -192px; } +.ui-icon-circle-arrow-e { background-position: -112px -192px; } +.ui-icon-circle-arrow-s { background-position: -128px -192px; } +.ui-icon-circle-arrow-w { background-position: -144px -192px; } +.ui-icon-circle-arrow-n { background-position: -160px -192px; } +.ui-icon-circle-zoomin { background-position: -176px -192px; } +.ui-icon-circle-zoomout { background-position: -192px -192px; } +.ui-icon-circle-check { background-position: -208px -192px; } +.ui-icon-circlesmall-plus { background-position: 0 -208px; } +.ui-icon-circlesmall-minus { background-position: -16px -208px; } +.ui-icon-circlesmall-close { background-position: -32px -208px; } +.ui-icon-squaresmall-plus { background-position: -48px -208px; } +.ui-icon-squaresmall-minus { background-position: -64px -208px; } +.ui-icon-squaresmall-close { background-position: -80px -208px; } +.ui-icon-grip-dotted-vertical { background-position: 0 -224px; } +.ui-icon-grip-dotted-horizontal { background-position: -16px -224px; } +.ui-icon-grip-solid-vertical { background-position: -32px -224px; } +.ui-icon-grip-solid-horizontal { background-position: -48px -224px; } +.ui-icon-gripsmall-diagonal-se { background-position: -64px -224px; } +.ui-icon-grip-diagonal-se { background-position: -80px -224px; } + + +/* Misc visuals +----------------------------------*/ + +/* Corner radius */ +.ui-corner-all, +.ui-corner-top, +.ui-corner-left, +.ui-corner-tl { + border-top-left-radius: 3px; +} +.ui-corner-all, +.ui-corner-top, +.ui-corner-right, +.ui-corner-tr { + border-top-right-radius: 3px; +} +.ui-corner-all, +.ui-corner-bottom, +.ui-corner-left, +.ui-corner-bl { + border-bottom-left-radius: 3px; +} +.ui-corner-all, +.ui-corner-bottom, +.ui-corner-right, +.ui-corner-br { + border-bottom-right-radius: 3px; +} + +/* Overlays */ +.ui-widget-overlay { + background: #aaaaaa; + opacity: .3; + filter: Alpha(Opacity=30); /* support: IE8 */ +} +.ui-widget-shadow { + -webkit-box-shadow: 0px 0px 5px #666666; + box-shadow: 0px 0px 5px #666666; +} diff --git a/webroot/rsrc/js/application/fact/Chart.js b/webroot/rsrc/js/application/fact/Chart.js index 473feedcea..44c1faedaf 100644 --- a/webroot/rsrc/js/application/fact/Chart.js +++ b/webroot/rsrc/js/application/fact/Chart.js @@ -200,11 +200,8 @@ JX.install('Chart', { var y = parseInt(d.y1); - var label = d.n + ' Points'; - var view = - d_y + '-' + d_m + '-' + d_d + ': ' + y + '
' + - label; + d_y + '-' + d_m + '-' + d_d + '
' + y + ''; div .html(view) diff --git a/webroot/rsrc/js/application/timetracker/chart/Chart.min.js b/webroot/rsrc/js/application/timetracker/chart/Chart.min.js new file mode 100644 index 0000000000..75c1a0fbf2 --- /dev/null +++ b/webroot/rsrc/js/application/timetracker/chart/Chart.min.js @@ -0,0 +1,6515 @@ +/** + * @provides Chart.min + */ +! function(t) { + if ("object" == typeof exports && "undefined" != typeof module) module.exports = t(); + else if ("function" == typeof define && define.amd) define([], t); + else { + ("undefined" != typeof window ? window : "undefined" != typeof global ? global : "undefined" != typeof self ? self : this).Chart = t() + } +}(function() { + return function t(e, i, n) { + function a(r, s) { + if (!i[r]) { + if (!e[r]) { + var l = "function" == typeof require && require; + if (!s && l) return l(r, !0); + if (o) return o(r, !0); + var u = new Error("Cannot find module '" + r + "'"); + throw u.code = "MODULE_NOT_FOUND", u + } + var d = i[r] = { + exports: {} + }; + e[r][0].call(d.exports, function(t) { + var i = e[r][1][t]; + return a(i || t) + }, d, d.exports, t, e, i, n) + } + return i[r].exports + } + for (var o = "function" == typeof require && require, r = 0; r < n.length; r++) a(n[r]); + return a + }({ + 1: [function(t, e, i) {}, {}], + 2: [function(t, e, i) { + var n = t(6); + + function a(t) { + if (t) { + var e = [0, 0, 0], + i = 1, + a = t.match(/^#([a-fA-F0-9]{3})$/i); + if (a) { + a = a[1]; + for (var o = 0; o < e.length; o++) e[o] = parseInt(a[o] + a[o], 16) + } else if (a = t.match(/^#([a-fA-F0-9]{6})$/i)) { + a = a[1]; + for (o = 0; o < e.length; o++) e[o] = parseInt(a.slice(2 * o, 2 * o + 2), 16) + } else if (a = t.match(/^rgba?\(\s*([+-]?\d+)\s*,\s*([+-]?\d+)\s*,\s*([+-]?\d+)\s*(?:,\s*([+-]?[\d\.]+)\s*)?\)$/i)) { + for (o = 0; o < e.length; o++) e[o] = parseInt(a[o + 1]); + i = parseFloat(a[4]) + } else if (a = t.match(/^rgba?\(\s*([+-]?[\d\.]+)\%\s*,\s*([+-]?[\d\.]+)\%\s*,\s*([+-]?[\d\.]+)\%\s*(?:,\s*([+-]?[\d\.]+)\s*)?\)$/i)) { + for (o = 0; o < e.length; o++) e[o] = Math.round(2.55 * parseFloat(a[o + 1])); + i = parseFloat(a[4]) + } else if (a = t.match(/(\w+)/)) { + if ("transparent" == a[1]) return [0, 0, 0, 0]; + if (!(e = n[a[1]])) return + } + for (o = 0; o < e.length; o++) e[o] = d(e[o], 0, 255); + return i = i || 0 == i ? d(i, 0, 1) : 1, e[3] = i, e + } + } + + function o(t) { + if (t) { + var e = t.match(/^hsla?\(\s*([+-]?\d+)(?:deg)?\s*,\s*([+-]?[\d\.]+)%\s*,\s*([+-]?[\d\.]+)%\s*(?:,\s*([+-]?[\d\.]+)\s*)?\)/); + if (e) { + var i = parseFloat(e[4]); + return [d(parseInt(e[1]), 0, 360), d(parseFloat(e[2]), 0, 100), d(parseFloat(e[3]), 0, 100), d(isNaN(i) ? 1 : i, 0, 1)] + } + } + } + + function r(t) { + if (t) { + var e = t.match(/^hwb\(\s*([+-]?\d+)(?:deg)?\s*,\s*([+-]?[\d\.]+)%\s*,\s*([+-]?[\d\.]+)%\s*(?:,\s*([+-]?[\d\.]+)\s*)?\)/); + if (e) { + var i = parseFloat(e[4]); + return [d(parseInt(e[1]), 0, 360), d(parseFloat(e[2]), 0, 100), d(parseFloat(e[3]), 0, 100), d(isNaN(i) ? 1 : i, 0, 1)] + } + } + } + + function s(t, e) { + return void 0 === e && (e = void 0 !== t[3] ? t[3] : 1), "rgba(" + t[0] + ", " + t[1] + ", " + t[2] + ", " + e + ")" + } + + function l(t, e) { + return "rgba(" + Math.round(t[0] / 255 * 100) + "%, " + Math.round(t[1] / 255 * 100) + "%, " + Math.round(t[2] / 255 * 100) + "%, " + (e || t[3] || 1) + ")" + } + + function u(t, e) { + return void 0 === e && (e = void 0 !== t[3] ? t[3] : 1), "hsla(" + t[0] + ", " + t[1] + "%, " + t[2] + "%, " + e + ")" + } + + function d(t, e, i) { + return Math.min(Math.max(e, t), i) + } + + function c(t) { + var e = t.toString(16).toUpperCase(); + return e.length < 2 ? "0" + e : e + } + e.exports = { + getRgba: a, + getHsla: o, + getRgb: function(t) { + var e = a(t); + return e && e.slice(0, 3) + }, + getHsl: function(t) { + var e = o(t); + return e && e.slice(0, 3) + }, + getHwb: r, + getAlpha: function(t) { + var e = a(t); { + if (e) return e[3]; + if (e = o(t)) return e[3]; + if (e = r(t)) return e[3] + } + }, + hexString: function(t) { + return "#" + c(t[0]) + c(t[1]) + c(t[2]) + }, + rgbString: function(t, e) { + if (e < 1 || t[3] && t[3] < 1) return s(t, e); + return "rgb(" + t[0] + ", " + t[1] + ", " + t[2] + ")" + }, + rgbaString: s, + percentString: function(t, e) { + if (e < 1 || t[3] && t[3] < 1) return l(t, e); + var i = Math.round(t[0] / 255 * 100), + n = Math.round(t[1] / 255 * 100), + a = Math.round(t[2] / 255 * 100); + return "rgb(" + i + "%, " + n + "%, " + a + "%)" + }, + percentaString: l, + hslString: function(t, e) { + if (e < 1 || t[3] && t[3] < 1) return u(t, e); + return "hsl(" + t[0] + ", " + t[1] + "%, " + t[2] + "%)" + }, + hslaString: u, + hwbString: function(t, e) { + void 0 === e && (e = void 0 !== t[3] ? t[3] : 1); + return "hwb(" + t[0] + ", " + t[1] + "%, " + t[2] + "%" + (void 0 !== e && 1 !== e ? ", " + e : "") + ")" + }, + keyword: function(t) { + return h[t.slice(0, 3)] + } + }; + var h = {}; + for (var f in n) h[n[f]] = f + }, { + 6: 6 + }], + 3: [function(t, e, i) { + var n = t(5), + a = t(2), + o = function(t) { + return t instanceof o ? t : this instanceof o ? (this.valid = !1, this.values = { + rgb: [0, 0, 0], + hsl: [0, 0, 0], + hsv: [0, 0, 0], + hwb: [0, 0, 0], + cmyk: [0, 0, 0, 0], + alpha: 1 + }, void("string" == typeof t ? (e = a.getRgba(t)) ? this.setValues("rgb", e) : (e = a.getHsla(t)) ? this.setValues("hsl", e) : (e = a.getHwb(t)) && this.setValues("hwb", e) : "object" == typeof t && (void 0 !== (e = t).r || void 0 !== e.red ? this.setValues("rgb", e) : void 0 !== e.l || void 0 !== e.lightness ? this.setValues("hsl", e) : void 0 !== e.v || void 0 !== e.value ? this.setValues("hsv", e) : void 0 !== e.w || void 0 !== e.whiteness ? this.setValues("hwb", e) : void 0 === e.c && void 0 === e.cyan || this.setValues("cmyk", e)))) : new o(t); + var e + }; + o.prototype = { + isValid: function() { + return this.valid + }, + rgb: function() { + return this.setSpace("rgb", arguments) + }, + hsl: function() { + return this.setSpace("hsl", arguments) + }, + hsv: function() { + return this.setSpace("hsv", arguments) + }, + hwb: function() { + return this.setSpace("hwb", arguments) + }, + cmyk: function() { + return this.setSpace("cmyk", arguments) + }, + rgbArray: function() { + return this.values.rgb + }, + hslArray: function() { + return this.values.hsl + }, + hsvArray: function() { + return this.values.hsv + }, + hwbArray: function() { + var t = this.values; + return 1 !== t.alpha ? t.hwb.concat([t.alpha]) : t.hwb + }, + cmykArray: function() { + return this.values.cmyk + }, + rgbaArray: function() { + var t = this.values; + return t.rgb.concat([t.alpha]) + }, + hslaArray: function() { + var t = this.values; + return t.hsl.concat([t.alpha]) + }, + alpha: function(t) { + return void 0 === t ? this.values.alpha : (this.setValues("alpha", t), this) + }, + red: function(t) { + return this.setChannel("rgb", 0, t) + }, + green: function(t) { + return this.setChannel("rgb", 1, t) + }, + blue: function(t) { + return this.setChannel("rgb", 2, t) + }, + hue: function(t) { + return t && (t = (t %= 360) < 0 ? 360 + t : t), this.setChannel("hsl", 0, t) + }, + saturation: function(t) { + return this.setChannel("hsl", 1, t) + }, + lightness: function(t) { + return this.setChannel("hsl", 2, t) + }, + saturationv: function(t) { + return this.setChannel("hsv", 1, t) + }, + whiteness: function(t) { + return this.setChannel("hwb", 1, t) + }, + blackness: function(t) { + return this.setChannel("hwb", 2, t) + }, + value: function(t) { + return this.setChannel("hsv", 2, t) + }, + cyan: function(t) { + return this.setChannel("cmyk", 0, t) + }, + magenta: function(t) { + return this.setChannel("cmyk", 1, t) + }, + yellow: function(t) { + return this.setChannel("cmyk", 2, t) + }, + black: function(t) { + return this.setChannel("cmyk", 3, t) + }, + hexString: function() { + return a.hexString(this.values.rgb) + }, + rgbString: function() { + return a.rgbString(this.values.rgb, this.values.alpha) + }, + rgbaString: function() { + return a.rgbaString(this.values.rgb, this.values.alpha) + }, + percentString: function() { + return a.percentString(this.values.rgb, this.values.alpha) + }, + hslString: function() { + return a.hslString(this.values.hsl, this.values.alpha) + }, + hslaString: function() { + return a.hslaString(this.values.hsl, this.values.alpha) + }, + hwbString: function() { + return a.hwbString(this.values.hwb, this.values.alpha) + }, + keyword: function() { + return a.keyword(this.values.rgb, this.values.alpha) + }, + rgbNumber: function() { + var t = this.values.rgb; + return t[0] << 16 | t[1] << 8 | t[2] + }, + luminosity: function() { + for (var t = this.values.rgb, e = [], i = 0; i < t.length; i++) { + var n = t[i] / 255; + e[i] = n <= .03928 ? n / 12.92 : Math.pow((n + .055) / 1.055, 2.4) + } + return .2126 * e[0] + .7152 * e[1] + .0722 * e[2] + }, + contrast: function(t) { + var e = this.luminosity(), + i = t.luminosity(); + return e > i ? (e + .05) / (i + .05) : (i + .05) / (e + .05) + }, + level: function(t) { + var e = this.contrast(t); + return e >= 7.1 ? "AAA" : e >= 4.5 ? "AA" : "" + }, + dark: function() { + var t = this.values.rgb; + return (299 * t[0] + 587 * t[1] + 114 * t[2]) / 1e3 < 128 + }, + light: function() { + return !this.dark() + }, + negate: function() { + for (var t = [], e = 0; e < 3; e++) t[e] = 255 - this.values.rgb[e]; + return this.setValues("rgb", t), this + }, + lighten: function(t) { + var e = this.values.hsl; + return e[2] += e[2] * t, this.setValues("hsl", e), this + }, + darken: function(t) { + var e = this.values.hsl; + return e[2] -= e[2] * t, this.setValues("hsl", e), this + }, + saturate: function(t) { + var e = this.values.hsl; + return e[1] += e[1] * t, this.setValues("hsl", e), this + }, + desaturate: function(t) { + var e = this.values.hsl; + return e[1] -= e[1] * t, this.setValues("hsl", e), this + }, + whiten: function(t) { + var e = this.values.hwb; + return e[1] += e[1] * t, this.setValues("hwb", e), this + }, + blacken: function(t) { + var e = this.values.hwb; + return e[2] += e[2] * t, this.setValues("hwb", e), this + }, + greyscale: function() { + var t = this.values.rgb, + e = .3 * t[0] + .59 * t[1] + .11 * t[2]; + return this.setValues("rgb", [e, e, e]), this + }, + clearer: function(t) { + var e = this.values.alpha; + return this.setValues("alpha", e - e * t), this + }, + opaquer: function(t) { + var e = this.values.alpha; + return this.setValues("alpha", e + e * t), this + }, + rotate: function(t) { + var e = this.values.hsl, + i = (e[0] + t) % 360; + return e[0] = i < 0 ? 360 + i : i, this.setValues("hsl", e), this + }, + mix: function(t, e) { + var i = this, + n = t, + a = void 0 === e ? .5 : e, + o = 2 * a - 1, + r = i.alpha() - n.alpha(), + s = ((o * r == -1 ? o : (o + r) / (1 + o * r)) + 1) / 2, + l = 1 - s; + return this.rgb(s * i.red() + l * n.red(), s * i.green() + l * n.green(), s * i.blue() + l * n.blue()).alpha(i.alpha() * a + n.alpha() * (1 - a)) + }, + toJSON: function() { + return this.rgb() + }, + clone: function() { + var t, e, i = new o, + n = this.values, + a = i.values; + for (var r in n) n.hasOwnProperty(r) && (t = n[r], "[object Array]" === (e = {}.toString.call(t)) ? a[r] = t.slice(0) : "[object Number]" === e ? a[r] = t : console.error("unexpected color value:", t)); + return i + } + }, o.prototype.spaces = { + rgb: ["red", "green", "blue"], + hsl: ["hue", "saturation", "lightness"], + hsv: ["hue", "saturation", "value"], + hwb: ["hue", "whiteness", "blackness"], + cmyk: ["cyan", "magenta", "yellow", "black"] + }, o.prototype.maxes = { + rgb: [255, 255, 255], + hsl: [360, 100, 100], + hsv: [360, 100, 100], + hwb: [360, 100, 100], + cmyk: [100, 100, 100, 100] + }, o.prototype.getValues = function(t) { + for (var e = this.values, i = {}, n = 0; n < t.length; n++) i[t.charAt(n)] = e[t][n]; + return 1 !== e.alpha && (i.a = e.alpha), i + }, o.prototype.setValues = function(t, e) { + var i, a, o = this.values, + r = this.spaces, + s = this.maxes, + l = 1; + if (this.valid = !0, "alpha" === t) l = e; + else if (e.length) o[t] = e.slice(0, t.length), l = e[t.length]; + else if (void 0 !== e[t.charAt(0)]) { + for (i = 0; i < t.length; i++) o[t][i] = e[t.charAt(i)]; + l = e.a + } else if (void 0 !== e[r[t][0]]) { + var u = r[t]; + for (i = 0; i < t.length; i++) o[t][i] = e[u[i]]; + l = e.alpha + } + if (o.alpha = Math.max(0, Math.min(1, void 0 === l ? o.alpha : l)), "alpha" === t) return !1; + for (i = 0; i < t.length; i++) a = Math.max(0, Math.min(s[t][i], o[t][i])), o[t][i] = Math.round(a); + for (var d in r) d !== t && (o[d] = n[t][d](o[t])); + return !0 + }, o.prototype.setSpace = function(t, e) { + var i = e[0]; + return void 0 === i ? this.getValues(t) : ("number" == typeof i && (i = Array.prototype.slice.call(e)), this.setValues(t, i), this) + }, o.prototype.setChannel = function(t, e, i) { + var n = this.values[t]; + return void 0 === i ? n[e] : i === n[e] ? this : (n[e] = i, this.setValues(t, n), this) + }, "undefined" != typeof window && (window.Color = o), e.exports = o + }, { + 2: 2, + 5: 5 + }], + 4: [function(t, e, i) { + function n(t) { + var e, i, n = t[0] / 255, + a = t[1] / 255, + o = t[2] / 255, + r = Math.min(n, a, o), + s = Math.max(n, a, o), + l = s - r; + return s == r ? e = 0 : n == s ? e = (a - o) / l : a == s ? e = 2 + (o - n) / l : o == s && (e = 4 + (n - a) / l), (e = Math.min(60 * e, 360)) < 0 && (e += 360), i = (r + s) / 2, [e, 100 * (s == r ? 0 : i <= .5 ? l / (s + r) : l / (2 - s - r)), 100 * i] + } + + function a(t) { + var e, i, n = t[0], + a = t[1], + o = t[2], + r = Math.min(n, a, o), + s = Math.max(n, a, o), + l = s - r; + return i = 0 == s ? 0 : l / s * 1e3 / 10, s == r ? e = 0 : n == s ? e = (a - o) / l : a == s ? e = 2 + (o - n) / l : o == s && (e = 4 + (n - a) / l), (e = Math.min(60 * e, 360)) < 0 && (e += 360), [e, i, s / 255 * 1e3 / 10] + } + + function o(t) { + var e = t[0], + i = t[1], + a = t[2]; + return [n(t)[0], 100 * (1 / 255 * Math.min(e, Math.min(i, a))), 100 * (a = 1 - 1 / 255 * Math.max(e, Math.max(i, a)))] + } + + function s(t) { + var e, i = t[0] / 255, + n = t[1] / 255, + a = t[2] / 255; + return [100 * ((1 - i - (e = Math.min(1 - i, 1 - n, 1 - a))) / (1 - e) || 0), 100 * ((1 - n - e) / (1 - e) || 0), 100 * ((1 - a - e) / (1 - e) || 0), 100 * e] + } + + function l(t) { + return C[JSON.stringify(t)] + } + + function u(t) { + var e = t[0] / 255, + i = t[1] / 255, + n = t[2] / 255; + return [100 * (.4124 * (e = e > .04045 ? Math.pow((e + .055) / 1.055, 2.4) : e / 12.92) + .3576 * (i = i > .04045 ? Math.pow((i + .055) / 1.055, 2.4) : i / 12.92) + .1805 * (n = n > .04045 ? Math.pow((n + .055) / 1.055, 2.4) : n / 12.92)), 100 * (.2126 * e + .7152 * i + .0722 * n), 100 * (.0193 * e + .1192 * i + .9505 * n)] + } + + function d(t) { + var e = u(t), + i = e[0], + n = e[1], + a = e[2]; + return n /= 100, a /= 108.883, i = (i /= 95.047) > .008856 ? Math.pow(i, 1 / 3) : 7.787 * i + 16 / 116, [116 * (n = n > .008856 ? Math.pow(n, 1 / 3) : 7.787 * n + 16 / 116) - 16, 500 * (i - n), 200 * (n - (a = a > .008856 ? Math.pow(a, 1 / 3) : 7.787 * a + 16 / 116))] + } + + function c(t) { + var e, i, n, a, o, r = t[0] / 360, + s = t[1] / 100, + l = t[2] / 100; + if (0 == s) return [o = 255 * l, o, o]; + e = 2 * l - (i = l < .5 ? l * (1 + s) : l + s - l * s), a = [0, 0, 0]; + for (var u = 0; u < 3; u++)(n = r + 1 / 3 * -(u - 1)) < 0 && n++, n > 1 && n--, o = 6 * n < 1 ? e + 6 * (i - e) * n : 2 * n < 1 ? i : 3 * n < 2 ? e + (i - e) * (2 / 3 - n) * 6 : e, a[u] = 255 * o; + return a + } + + function h(t) { + var e = t[0] / 60, + i = t[1] / 100, + n = t[2] / 100, + a = Math.floor(e) % 6, + o = e - Math.floor(e), + r = 255 * n * (1 - i), + s = 255 * n * (1 - i * o), + l = 255 * n * (1 - i * (1 - o)); + n *= 255; + switch (a) { + case 0: + return [n, l, r]; + case 1: + return [s, n, r]; + case 2: + return [r, n, l]; + case 3: + return [r, s, n]; + case 4: + return [l, r, n]; + case 5: + return [n, r, s] + } + } + + function f(t) { + var e, i, n, a, o = t[0] / 360, + s = t[1] / 100, + l = t[2] / 100, + u = s + l; + switch (u > 1 && (s /= u, l /= u), n = 6 * o - (e = Math.floor(6 * o)), 0 != (1 & e) && (n = 1 - n), a = s + n * ((i = 1 - l) - s), e) { + default: + case 6: + case 0: + r = i, g = a, b = s; + break; + case 1: + r = a, g = i, b = s; + break; + case 2: + r = s, g = i, b = a; + break; + case 3: + r = s, g = a, b = i; + break; + case 4: + r = a, g = s, b = i; + break; + case 5: + r = i, g = s, b = a + } + return [255 * r, 255 * g, 255 * b] + } + + function p(t) { + var e = t[0] / 100, + i = t[1] / 100, + n = t[2] / 100, + a = t[3] / 100; + return [255 * (1 - Math.min(1, e * (1 - a) + a)), 255 * (1 - Math.min(1, i * (1 - a) + a)), 255 * (1 - Math.min(1, n * (1 - a) + a))] + } + + function m(t) { + var e, i, n, a = t[0] / 100, + o = t[1] / 100, + r = t[2] / 100; + return i = -.9689 * a + 1.8758 * o + .0415 * r, n = .0557 * a + -.204 * o + 1.057 * r, e = (e = 3.2406 * a + -1.5372 * o + -.4986 * r) > .0031308 ? 1.055 * Math.pow(e, 1 / 2.4) - .055 : e *= 12.92, i = i > .0031308 ? 1.055 * Math.pow(i, 1 / 2.4) - .055 : i *= 12.92, n = n > .0031308 ? 1.055 * Math.pow(n, 1 / 2.4) - .055 : n *= 12.92, [255 * (e = Math.min(Math.max(0, e), 1)), 255 * (i = Math.min(Math.max(0, i), 1)), 255 * (n = Math.min(Math.max(0, n), 1))] + } + + function v(t) { + var e = t[0], + i = t[1], + n = t[2]; + return i /= 100, n /= 108.883, e = (e /= 95.047) > .008856 ? Math.pow(e, 1 / 3) : 7.787 * e + 16 / 116, [116 * (i = i > .008856 ? Math.pow(i, 1 / 3) : 7.787 * i + 16 / 116) - 16, 500 * (e - i), 200 * (i - (n = n > .008856 ? Math.pow(n, 1 / 3) : 7.787 * n + 16 / 116))] + } + + function x(t) { + var e, i, n, a, o = t[0], + r = t[1], + s = t[2]; + return o <= 8 ? a = (i = 100 * o / 903.3) / 100 * 7.787 + 16 / 116 : (i = 100 * Math.pow((o + 16) / 116, 3), a = Math.pow(i / 100, 1 / 3)), [e = e / 95.047 <= .008856 ? e = 95.047 * (r / 500 + a - 16 / 116) / 7.787 : 95.047 * Math.pow(r / 500 + a, 3), i, n = n / 108.883 <= .008859 ? n = 108.883 * (a - s / 200 - 16 / 116) / 7.787 : 108.883 * Math.pow(a - s / 200, 3)] + } + + function y(t) { + var e, i = t[0], + n = t[1], + a = t[2]; + return (e = 360 * Math.atan2(a, n) / 2 / Math.PI) < 0 && (e += 360), [i, Math.sqrt(n * n + a * a), e] + } + + function k(t) { + return m(x(t)) + } + + function M(t) { + var e, i = t[0], + n = t[1]; + return e = t[2] / 360 * 2 * Math.PI, [i, n * Math.cos(e), n * Math.sin(e)] + } + + function w(t) { + return S[t] + } + e.exports = { + rgb2hsl: n, + rgb2hsv: a, + rgb2hwb: o, + rgb2cmyk: s, + rgb2keyword: l, + rgb2xyz: u, + rgb2lab: d, + rgb2lch: function(t) { + return y(d(t)) + }, + hsl2rgb: c, + hsl2hsv: function(t) { + var e = t[0], + i = t[1] / 100, + n = t[2] / 100; + if (0 === n) return [0, 0, 0]; + return [e, 100 * (2 * (i *= (n *= 2) <= 1 ? n : 2 - n) / (n + i)), 100 * ((n + i) / 2)] + }, + hsl2hwb: function(t) { + return o(c(t)) + }, + hsl2cmyk: function(t) { + return s(c(t)) + }, + hsl2keyword: function(t) { + return l(c(t)) + }, + hsv2rgb: h, + hsv2hsl: function(t) { + var e, i, n = t[0], + a = t[1] / 100, + o = t[2] / 100; + return e = a * o, [n, 100 * (e = (e /= (i = (2 - a) * o) <= 1 ? i : 2 - i) || 0), 100 * (i /= 2)] + }, + hsv2hwb: function(t) { + return o(h(t)) + }, + hsv2cmyk: function(t) { + return s(h(t)) + }, + hsv2keyword: function(t) { + return l(h(t)) + }, + hwb2rgb: f, + hwb2hsl: function(t) { + return n(f(t)) + }, + hwb2hsv: function(t) { + return a(f(t)) + }, + hwb2cmyk: function(t) { + return s(f(t)) + }, + hwb2keyword: function(t) { + return l(f(t)) + }, + cmyk2rgb: p, + cmyk2hsl: function(t) { + return n(p(t)) + }, + cmyk2hsv: function(t) { + return a(p(t)) + }, + cmyk2hwb: function(t) { + return o(p(t)) + }, + cmyk2keyword: function(t) { + return l(p(t)) + }, + keyword2rgb: w, + keyword2hsl: function(t) { + return n(w(t)) + }, + keyword2hsv: function(t) { + return a(w(t)) + }, + keyword2hwb: function(t) { + return o(w(t)) + }, + keyword2cmyk: function(t) { + return s(w(t)) + }, + keyword2lab: function(t) { + return d(w(t)) + }, + keyword2xyz: function(t) { + return u(w(t)) + }, + xyz2rgb: m, + xyz2lab: v, + xyz2lch: function(t) { + return y(v(t)) + }, + lab2xyz: x, + lab2rgb: k, + lab2lch: y, + lch2lab: M, + lch2xyz: function(t) { + return x(M(t)) + }, + lch2rgb: function(t) { + return k(M(t)) + } + }; + var S = { + aliceblue: [240, 248, 255], + antiquewhite: [250, 235, 215], + aqua: [0, 255, 255], + aquamarine: [127, 255, 212], + azure: [240, 255, 255], + beige: [245, 245, 220], + bisque: [255, 228, 196], + black: [0, 0, 0], + blanchedalmond: [255, 235, 205], + blue: [0, 0, 255], + blueviolet: [138, 43, 226], + brown: [165, 42, 42], + burlywood: [222, 184, 135], + cadetblue: [95, 158, 160], + chartreuse: [127, 255, 0], + chocolate: [210, 105, 30], + coral: [255, 127, 80], + cornflowerblue: [100, 149, 237], + cornsilk: [255, 248, 220], + crimson: [220, 20, 60], + cyan: [0, 255, 255], + darkblue: [0, 0, 139], + darkcyan: [0, 139, 139], + darkgoldenrod: [184, 134, 11], + darkgray: [169, 169, 169], + darkgreen: [0, 100, 0], + darkgrey: [169, 169, 169], + darkkhaki: [189, 183, 107], + darkmagenta: [139, 0, 139], + darkolivegreen: [85, 107, 47], + darkorange: [255, 140, 0], + darkorchid: [153, 50, 204], + darkred: [139, 0, 0], + darksalmon: [233, 150, 122], + darkseagreen: [143, 188, 143], + darkslateblue: [72, 61, 139], + darkslategray: [47, 79, 79], + darkslategrey: [47, 79, 79], + darkturquoise: [0, 206, 209], + darkviolet: [148, 0, 211], + deeppink: [255, 20, 147], + deepskyblue: [0, 191, 255], + dimgray: [105, 105, 105], + dimgrey: [105, 105, 105], + dodgerblue: [30, 144, 255], + firebrick: [178, 34, 34], + floralwhite: [255, 250, 240], + forestgreen: [34, 139, 34], + fuchsia: [255, 0, 255], + gainsboro: [220, 220, 220], + ghostwhite: [248, 248, 255], + gold: [255, 215, 0], + goldenrod: [218, 165, 32], + gray: [128, 128, 128], + green: [0, 128, 0], + greenyellow: [173, 255, 47], + grey: [128, 128, 128], + honeydew: [240, 255, 240], + hotpink: [255, 105, 180], + indianred: [205, 92, 92], + indigo: [75, 0, 130], + ivory: [255, 255, 240], + khaki: [240, 230, 140], + lavender: [230, 230, 250], + lavenderblush: [255, 240, 245], + lawngreen: [124, 252, 0], + lemonchiffon: [255, 250, 205], + lightblue: [173, 216, 230], + lightcoral: [240, 128, 128], + lightcyan: [224, 255, 255], + lightgoldenrodyellow: [250, 250, 210], + lightgray: [211, 211, 211], + lightgreen: [144, 238, 144], + lightgrey: [211, 211, 211], + lightpink: [255, 182, 193], + lightsalmon: [255, 160, 122], + lightseagreen: [32, 178, 170], + lightskyblue: [135, 206, 250], + lightslategray: [119, 136, 153], + lightslategrey: [119, 136, 153], + lightsteelblue: [176, 196, 222], + lightyellow: [255, 255, 224], + lime: [0, 255, 0], + limegreen: [50, 205, 50], + linen: [250, 240, 230], + magenta: [255, 0, 255], + maroon: [128, 0, 0], + mediumaquamarine: [102, 205, 170], + mediumblue: [0, 0, 205], + mediumorchid: [186, 85, 211], + mediumpurple: [147, 112, 219], + mediumseagreen: [60, 179, 113], + mediumslateblue: [123, 104, 238], + mediumspringgreen: [0, 250, 154], + mediumturquoise: [72, 209, 204], + mediumvioletred: [199, 21, 133], + midnightblue: [25, 25, 112], + mintcream: [245, 255, 250], + mistyrose: [255, 228, 225], + moccasin: [255, 228, 181], + navajowhite: [255, 222, 173], + navy: [0, 0, 128], + oldlace: [253, 245, 230], + olive: [128, 128, 0], + olivedrab: [107, 142, 35], + orange: [255, 165, 0], + orangered: [255, 69, 0], + orchid: [218, 112, 214], + palegoldenrod: [238, 232, 170], + palegreen: [152, 251, 152], + paleturquoise: [175, 238, 238], + palevioletred: [219, 112, 147], + papayawhip: [255, 239, 213], + peachpuff: [255, 218, 185], + peru: [205, 133, 63], + pink: [255, 192, 203], + plum: [221, 160, 221], + powderblue: [176, 224, 230], + purple: [128, 0, 128], + rebeccapurple: [102, 51, 153], + red: [255, 0, 0], + rosybrown: [188, 143, 143], + royalblue: [65, 105, 225], + saddlebrown: [139, 69, 19], + salmon: [250, 128, 114], + sandybrown: [244, 164, 96], + seagreen: [46, 139, 87], + seashell: [255, 245, 238], + sienna: [160, 82, 45], + silver: [192, 192, 192], + skyblue: [135, 206, 235], + slateblue: [106, 90, 205], + slategray: [112, 128, 144], + slategrey: [112, 128, 144], + snow: [255, 250, 250], + springgreen: [0, 255, 127], + steelblue: [70, 130, 180], + tan: [210, 180, 140], + teal: [0, 128, 128], + thistle: [216, 191, 216], + tomato: [255, 99, 71], + turquoise: [64, 224, 208], + violet: [238, 130, 238], + wheat: [245, 222, 179], + white: [255, 255, 255], + whitesmoke: [245, 245, 245], + yellow: [255, 255, 0], + yellowgreen: [154, 205, 50] + }, + C = {}; + for (var _ in S) C[JSON.stringify(S[_])] = _ + }, {}], + 5: [function(t, e, i) { + var n = t(4), + a = function() { + return new u + }; + for (var o in n) { + a[o + "Raw"] = function(t) { + return function(e) { + return "number" == typeof e && (e = Array.prototype.slice.call(arguments)), n[t](e) + } + }(o); + var r = /(\w+)2(\w+)/.exec(o), + s = r[1], + l = r[2]; + (a[s] = a[s] || {})[l] = a[o] = function(t) { + return function(e) { + "number" == typeof e && (e = Array.prototype.slice.call(arguments)); + var i = n[t](e); + if ("string" == typeof i || void 0 === i) return i; + for (var a = 0; a < i.length; a++) i[a] = Math.round(i[a]); + return i + } + }(o) + } + var u = function() { + this.convs = {} + }; + u.prototype.routeSpace = function(t, e) { + var i = e[0]; + return void 0 === i ? this.getValues(t) : ("number" == typeof i && (i = Array.prototype.slice.call(e)), this.setValues(t, i)) + }, u.prototype.setValues = function(t, e) { + return this.space = t, this.convs = {}, this.convs[t] = e, this + }, u.prototype.getValues = function(t) { + var e = this.convs[t]; + if (!e) { + var i = this.space, + n = this.convs[i]; + e = a[i][t](n), this.convs[t] = e + } + return e + }, ["rgb", "hsl", "hsv", "cmyk", "keyword"].forEach(function(t) { + u.prototype[t] = function(e) { + return this.routeSpace(t, arguments) + } + }), e.exports = a + }, { + 4: 4 + }], + 6: [function(t, e, i) { + "use strict"; + e.exports = { + aliceblue: [240, 248, 255], + antiquewhite: [250, 235, 215], + aqua: [0, 255, 255], + aquamarine: [127, 255, 212], + azure: [240, 255, 255], + beige: [245, 245, 220], + bisque: [255, 228, 196], + black: [0, 0, 0], + blanchedalmond: [255, 235, 205], + blue: [0, 0, 255], + blueviolet: [138, 43, 226], + brown: [165, 42, 42], + burlywood: [222, 184, 135], + cadetblue: [95, 158, 160], + chartreuse: [127, 255, 0], + chocolate: [210, 105, 30], + coral: [255, 127, 80], + cornflowerblue: [100, 149, 237], + cornsilk: [255, 248, 220], + crimson: [220, 20, 60], + cyan: [0, 255, 255], + darkblue: [0, 0, 139], + darkcyan: [0, 139, 139], + darkgoldenrod: [184, 134, 11], + darkgray: [169, 169, 169], + darkgreen: [0, 100, 0], + darkgrey: [169, 169, 169], + darkkhaki: [189, 183, 107], + darkmagenta: [139, 0, 139], + darkolivegreen: [85, 107, 47], + darkorange: [255, 140, 0], + darkorchid: [153, 50, 204], + darkred: [139, 0, 0], + darksalmon: [233, 150, 122], + darkseagreen: [143, 188, 143], + darkslateblue: [72, 61, 139], + darkslategray: [47, 79, 79], + darkslategrey: [47, 79, 79], + darkturquoise: [0, 206, 209], + darkviolet: [148, 0, 211], + deeppink: [255, 20, 147], + deepskyblue: [0, 191, 255], + dimgray: [105, 105, 105], + dimgrey: [105, 105, 105], + dodgerblue: [30, 144, 255], + firebrick: [178, 34, 34], + floralwhite: [255, 250, 240], + forestgreen: [34, 139, 34], + fuchsia: [255, 0, 255], + gainsboro: [220, 220, 220], + ghostwhite: [248, 248, 255], + gold: [255, 215, 0], + goldenrod: [218, 165, 32], + gray: [128, 128, 128], + green: [0, 128, 0], + greenyellow: [173, 255, 47], + grey: [128, 128, 128], + honeydew: [240, 255, 240], + hotpink: [255, 105, 180], + indianred: [205, 92, 92], + indigo: [75, 0, 130], + ivory: [255, 255, 240], + khaki: [240, 230, 140], + lavender: [230, 230, 250], + lavenderblush: [255, 240, 245], + lawngreen: [124, 252, 0], + lemonchiffon: [255, 250, 205], + lightblue: [173, 216, 230], + lightcoral: [240, 128, 128], + lightcyan: [224, 255, 255], + lightgoldenrodyellow: [250, 250, 210], + lightgray: [211, 211, 211], + lightgreen: [144, 238, 144], + lightgrey: [211, 211, 211], + lightpink: [255, 182, 193], + lightsalmon: [255, 160, 122], + lightseagreen: [32, 178, 170], + lightskyblue: [135, 206, 250], + lightslategray: [119, 136, 153], + lightslategrey: [119, 136, 153], + lightsteelblue: [176, 196, 222], + lightyellow: [255, 255, 224], + lime: [0, 255, 0], + limegreen: [50, 205, 50], + linen: [250, 240, 230], + magenta: [255, 0, 255], + maroon: [128, 0, 0], + mediumaquamarine: [102, 205, 170], + mediumblue: [0, 0, 205], + mediumorchid: [186, 85, 211], + mediumpurple: [147, 112, 219], + mediumseagreen: [60, 179, 113], + mediumslateblue: [123, 104, 238], + mediumspringgreen: [0, 250, 154], + mediumturquoise: [72, 209, 204], + mediumvioletred: [199, 21, 133], + midnightblue: [25, 25, 112], + mintcream: [245, 255, 250], + mistyrose: [255, 228, 225], + moccasin: [255, 228, 181], + navajowhite: [255, 222, 173], + navy: [0, 0, 128], + oldlace: [253, 245, 230], + olive: [128, 128, 0], + olivedrab: [107, 142, 35], + orange: [255, 165, 0], + orangered: [255, 69, 0], + orchid: [218, 112, 214], + palegoldenrod: [238, 232, 170], + palegreen: [152, 251, 152], + paleturquoise: [175, 238, 238], + palevioletred: [219, 112, 147], + papayawhip: [255, 239, 213], + peachpuff: [255, 218, 185], + peru: [205, 133, 63], + pink: [255, 192, 203], + plum: [221, 160, 221], + powderblue: [176, 224, 230], + purple: [128, 0, 128], + rebeccapurple: [102, 51, 153], + red: [255, 0, 0], + rosybrown: [188, 143, 143], + royalblue: [65, 105, 225], + saddlebrown: [139, 69, 19], + salmon: [250, 128, 114], + sandybrown: [244, 164, 96], + seagreen: [46, 139, 87], + seashell: [255, 245, 238], + sienna: [160, 82, 45], + silver: [192, 192, 192], + skyblue: [135, 206, 235], + slateblue: [106, 90, 205], + slategray: [112, 128, 144], + slategrey: [112, 128, 144], + snow: [255, 250, 250], + springgreen: [0, 255, 127], + steelblue: [70, 130, 180], + tan: [210, 180, 140], + teal: [0, 128, 128], + thistle: [216, 191, 216], + tomato: [255, 99, 71], + turquoise: [64, 224, 208], + violet: [238, 130, 238], + wheat: [245, 222, 179], + white: [255, 255, 255], + whitesmoke: [245, 245, 245], + yellow: [255, 255, 0], + yellowgreen: [154, 205, 50] + } + }, {}], + 7: [function(t, e, i) { + var n = t(29)(); + n.helpers = t(45), t(27)(n), n.defaults = t(25), n.Element = t(26), n.elements = t(40), n.Interaction = t(28), n.layouts = t(30), n.platform = t(48), n.plugins = t(31), n.Ticks = t(34), t(22)(n), t(23)(n), t(24)(n), t(33)(n), t(32)(n), t(35)(n), t(55)(n), t(53)(n), t(54)(n), t(56)(n), t(57)(n), t(58)(n), t(15)(n), t(16)(n), t(17)(n), t(18)(n), t(19)(n), t(20)(n), t(21)(n), t(8)(n), t(9)(n), t(10)(n), t(11)(n), t(12)(n), t(13)(n), t(14)(n); + var a = t(49); + for (var o in a) a.hasOwnProperty(o) && n.plugins.register(a[o]); + n.platform.initialize(), e.exports = n, "undefined" != typeof window && (window.Chart = n), n.Legend = a.legend._element, n.Title = a.title._element, n.pluginService = n.plugins, n.PluginBase = n.Element.extend({}), n.canvasHelpers = n.helpers.canvas, n.layoutService = n.layouts + }, { + 10: 10, + 11: 11, + 12: 12, + 13: 13, + 14: 14, + 15: 15, + 16: 16, + 17: 17, + 18: 18, + 19: 19, + 20: 20, + 21: 21, + 22: 22, + 23: 23, + 24: 24, + 25: 25, + 26: 26, + 27: 27, + 28: 28, + 29: 29, + 30: 30, + 31: 31, + 32: 32, + 33: 33, + 34: 34, + 35: 35, + 40: 40, + 45: 45, + 48: 48, + 49: 49, + 53: 53, + 54: 54, + 55: 55, + 56: 56, + 57: 57, + 58: 58, + 8: 8, + 9: 9 + }], + 8: [function(t, e, i) { + "use strict"; + e.exports = function(t) { + t.Bar = function(e, i) { + return i.type = "bar", new t(e, i) + } + } + }, {}], + 9: [function(t, e, i) { + "use strict"; + e.exports = function(t) { + t.Bubble = function(e, i) { + return i.type = "bubble", new t(e, i) + } + } + }, {}], + 10: [function(t, e, i) { + "use strict"; + e.exports = function(t) { + t.Doughnut = function(e, i) { + return i.type = "doughnut", new t(e, i) + } + } + }, {}], + 11: [function(t, e, i) { + "use strict"; + e.exports = function(t) { + t.Line = function(e, i) { + return i.type = "line", new t(e, i) + } + } + }, {}], + 12: [function(t, e, i) { + "use strict"; + e.exports = function(t) { + t.PolarArea = function(e, i) { + return i.type = "polarArea", new t(e, i) + } + } + }, {}], + 13: [function(t, e, i) { + "use strict"; + e.exports = function(t) { + t.Radar = function(e, i) { + return i.type = "radar", new t(e, i) + } + } + }, {}], + 14: [function(t, e, i) { + "use strict"; + e.exports = function(t) { + t.Scatter = function(e, i) { + return i.type = "scatter", new t(e, i) + } + } + }, {}], + 15: [function(t, e, i) { + "use strict"; + var n = t(25), + a = t(40), + o = t(45); + n._set("bar", { + hover: { + mode: "label" + }, + scales: { + xAxes: [{ + type: "category", + categoryPercentage: .8, + barPercentage: .9, + offset: !0, + gridLines: { + offsetGridLines: !0 + } + }], + yAxes: [{ + type: "linear" + }] + } + }), n._set("horizontalBar", { + hover: { + mode: "index", + axis: "y" + }, + scales: { + xAxes: [{ + type: "linear", + position: "bottom" + }], + yAxes: [{ + position: "left", + type: "category", + categoryPercentage: .8, + barPercentage: .9, + offset: !0, + gridLines: { + offsetGridLines: !0 + } + }] + }, + elements: { + rectangle: { + borderSkipped: "left" + } + }, + tooltips: { + callbacks: { + title: function(t, e) { + var i = ""; + return t.length > 0 && (t[0].yLabel ? i = t[0].yLabel : e.labels.length > 0 && t[0].index < e.labels.length && (i = e.labels[t[0].index])), i + }, + label: function(t, e) { + return (e.datasets[t.datasetIndex].label || "") + ": " + t.xLabel + } + }, + mode: "index", + axis: "y" + } + }), e.exports = function(t) { + t.controllers.bar = t.DatasetController.extend({ + dataElementType: a.Rectangle, + initialize: function() { + var e; + t.DatasetController.prototype.initialize.apply(this, arguments), (e = this.getMeta()).stack = this.getDataset().stack, e.bar = !0 + }, + update: function(t) { + var e, i, n = this.getMeta().data; + for (this._ruler = this.getRuler(), e = 0, i = n.length; e < i; ++e) this.updateElement(n[e], e, t) + }, + updateElement: function(t, e, i) { + var n = this, + a = n.chart, + r = n.getMeta(), + s = n.getDataset(), + l = t.custom || {}, + u = a.options.elements.rectangle; + t._xScale = n.getScaleForId(r.xAxisID), t._yScale = n.getScaleForId(r.yAxisID), t._datasetIndex = n.index, t._index = e, t._model = { + datasetLabel: s.label, + label: a.data.labels[e], + borderSkipped: l.borderSkipped ? l.borderSkipped : u.borderSkipped, + backgroundColor: l.backgroundColor ? l.backgroundColor : o.valueAtIndexOrDefault(s.backgroundColor, e, u.backgroundColor), + borderColor: l.borderColor ? l.borderColor : o.valueAtIndexOrDefault(s.borderColor, e, u.borderColor), + borderWidth: l.borderWidth ? l.borderWidth : o.valueAtIndexOrDefault(s.borderWidth, e, u.borderWidth) + }, n.updateElementGeometry(t, e, i), t.pivot() + }, + updateElementGeometry: function(t, e, i) { + var n = this, + a = t._model, + o = n.getValueScale(), + r = o.getBasePixel(), + s = o.isHorizontal(), + l = n._ruler || n.getRuler(), + u = n.calculateBarValuePixels(n.index, e), + d = n.calculateBarIndexPixels(n.index, e, l); + a.horizontal = s, a.base = i ? r : u.base, a.x = s ? i ? r : u.head : d.center, a.y = s ? d.center : i ? r : u.head, a.height = s ? d.size : void 0, a.width = s ? void 0 : d.size + }, + getValueScaleId: function() { + return this.getMeta().yAxisID + }, + getIndexScaleId: function() { + return this.getMeta().xAxisID + }, + getValueScale: function() { + return this.getScaleForId(this.getValueScaleId()) + }, + getIndexScale: function() { + return this.getScaleForId(this.getIndexScaleId()) + }, + _getStacks: function(t) { + var e, i, n = this.chart, + a = this.getIndexScale().options.stacked, + o = void 0 === t ? n.data.datasets.length : t + 1, + r = []; + for (e = 0; e < o; ++e)(i = n.getDatasetMeta(e)).bar && n.isDatasetVisible(e) && (!1 === a || !0 === a && -1 === r.indexOf(i.stack) || void 0 === a && (void 0 === i.stack || -1 === r.indexOf(i.stack))) && r.push(i.stack); + return r + }, + getStackCount: function() { + return this._getStacks().length + }, + getStackIndex: function(t, e) { + var i = this._getStacks(t), + n = void 0 !== e ? i.indexOf(e) : -1; + return -1 === n ? i.length - 1 : n + }, + getRuler: function() { + var t, e, i = this.getIndexScale(), + n = this.getStackCount(), + a = this.index, + r = i.isHorizontal(), + s = r ? i.left : i.top, + l = s + (r ? i.width : i.height), + u = []; + for (t = 0, e = this.getMeta().data.length; t < e; ++t) u.push(i.getPixelForValue(null, t, a)); + return { + min: o.isNullOrUndef(i.options.barThickness) ? function(t, e) { + var i, n, a, o, r = t.isHorizontal() ? t.width : t.height, + s = t.getTicks(); + for (a = 1, o = e.length; a < o; ++a) r = Math.min(r, e[a] - e[a - 1]); + for (a = 0, o = s.length; a < o; ++a) n = t.getPixelForTick(a), r = a > 0 ? Math.min(r, n - i) : r, i = n; + return r + }(i, u) : -1, + pixels: u, + start: s, + end: l, + stackCount: n, + scale: i + } + }, + calculateBarValuePixels: function(t, e) { + var i, n, a, o, r, s, l = this.chart, + u = this.getMeta(), + d = this.getValueScale(), + c = l.data.datasets, + h = d.getRightValue(c[t].data[e]), + f = d.options.stacked, + g = u.stack, + p = 0; + if (f || void 0 === f && void 0 !== g) + for (i = 0; i < t; ++i)(n = l.getDatasetMeta(i)).bar && n.stack === g && n.controller.getValueScaleId() === d.id && l.isDatasetVisible(i) && (a = d.getRightValue(c[i].data[e]), (h < 0 && a < 0 || h >= 0 && a > 0) && (p += a)); + return o = d.getPixelForValue(p), { + size: s = ((r = d.getPixelForValue(p + h)) - o) / 2, + base: o, + head: r, + center: r + s / 2 + } + }, + calculateBarIndexPixels: function(t, e, i) { + var n, a, r, s, l, u, d, c, h, f, g, p, m, v, b, x, y, k = i.scale.options, + M = "flex" === k.barThickness ? (h = e, g = k, m = (f = i).pixels, v = m[h], b = h > 0 ? m[h - 1] : null, x = h < m.length - 1 ? m[h + 1] : null, y = g.categoryPercentage, null === b && (b = v - (null === x ? f.end - v : x - v)), null === x && (x = v + v - b), p = v - (v - b) / 2 * y, { + chunk: (x - b) / 2 * y / f.stackCount, + ratio: g.barPercentage, + start: p + }) : (n = e, a = i, u = (r = k).barThickness, d = a.stackCount, c = a.pixels[n], o.isNullOrUndef(u) ? (s = a.min * r.categoryPercentage, l = r.barPercentage) : (s = u * d, l = 1), { + chunk: s / d, + ratio: l, + start: c - s / 2 + }), + w = this.getStackIndex(t, this.getMeta().stack), + S = M.start + M.chunk * w + M.chunk / 2, + C = Math.min(o.valueOrDefault(k.maxBarThickness, 1 / 0), M.chunk * M.ratio); + return { + base: S - C / 2, + head: S + C / 2, + center: S, + size: C + } + }, + draw: function() { + var t = this.chart, + e = this.getValueScale(), + i = this.getMeta().data, + n = this.getDataset(), + a = i.length, + r = 0; + for (o.canvas.clipArea(t.ctx, t.chartArea); r < a; ++r) isNaN(e.getRightValue(n.data[r])) || i[r].draw(); + o.canvas.unclipArea(t.ctx) + }, + setHoverStyle: function(t) { + var e = this.chart.data.datasets[t._datasetIndex], + i = t._index, + n = t.custom || {}, + a = t._model; + a.backgroundColor = n.hoverBackgroundColor ? n.hoverBackgroundColor : o.valueAtIndexOrDefault(e.hoverBackgroundColor, i, o.getHoverColor(a.backgroundColor)), a.borderColor = n.hoverBorderColor ? n.hoverBorderColor : o.valueAtIndexOrDefault(e.hoverBorderColor, i, o.getHoverColor(a.borderColor)), a.borderWidth = n.hoverBorderWidth ? n.hoverBorderWidth : o.valueAtIndexOrDefault(e.hoverBorderWidth, i, a.borderWidth) + }, + removeHoverStyle: function(t) { + var e = this.chart.data.datasets[t._datasetIndex], + i = t._index, + n = t.custom || {}, + a = t._model, + r = this.chart.options.elements.rectangle; + a.backgroundColor = n.backgroundColor ? n.backgroundColor : o.valueAtIndexOrDefault(e.backgroundColor, i, r.backgroundColor), a.borderColor = n.borderColor ? n.borderColor : o.valueAtIndexOrDefault(e.borderColor, i, r.borderColor), a.borderWidth = n.borderWidth ? n.borderWidth : o.valueAtIndexOrDefault(e.borderWidth, i, r.borderWidth) + } + }), t.controllers.horizontalBar = t.controllers.bar.extend({ + getValueScaleId: function() { + return this.getMeta().xAxisID + }, + getIndexScaleId: function() { + return this.getMeta().yAxisID + } + }) + } + }, { + 25: 25, + 40: 40, + 45: 45 + }], + 16: [function(t, e, i) { + "use strict"; + var n = t(25), + a = t(40), + o = t(45); + n._set("bubble", { + hover: { + mode: "single" + }, + scales: { + xAxes: [{ + type: "linear", + position: "bottom", + id: "x-axis-0" + }], + yAxes: [{ + type: "linear", + position: "left", + id: "y-axis-0" + }] + }, + tooltips: { + callbacks: { + title: function() { + return "" + }, + label: function(t, e) { + var i = e.datasets[t.datasetIndex].label || "", + n = e.datasets[t.datasetIndex].data[t.index]; + return i + ": (" + t.xLabel + ", " + t.yLabel + ", " + n.r + ")" + } + } + } + }), e.exports = function(t) { + t.controllers.bubble = t.DatasetController.extend({ + dataElementType: a.Point, + update: function(t) { + var e = this, + i = e.getMeta().data; + o.each(i, function(i, n) { + e.updateElement(i, n, t) + }) + }, + updateElement: function(t, e, i) { + var n = this, + a = n.getMeta(), + o = t.custom || {}, + r = n.getScaleForId(a.xAxisID), + s = n.getScaleForId(a.yAxisID), + l = n._resolveElementOptions(t, e), + u = n.getDataset().data[e], + d = n.index, + c = i ? r.getPixelForDecimal(.5) : r.getPixelForValue("object" == typeof u ? u : NaN, e, d), + h = i ? s.getBasePixel() : s.getPixelForValue(u, e, d); + t._xScale = r, t._yScale = s, t._options = l, t._datasetIndex = d, t._index = e, t._model = { + backgroundColor: l.backgroundColor, + borderColor: l.borderColor, + borderWidth: l.borderWidth, + hitRadius: l.hitRadius, + pointStyle: l.pointStyle, + radius: i ? 0 : l.radius, + skip: o.skip || isNaN(c) || isNaN(h), + x: c, + y: h + }, t.pivot() + }, + setHoverStyle: function(t) { + var e = t._model, + i = t._options; + e.backgroundColor = o.valueOrDefault(i.hoverBackgroundColor, o.getHoverColor(i.backgroundColor)), e.borderColor = o.valueOrDefault(i.hoverBorderColor, o.getHoverColor(i.borderColor)), e.borderWidth = o.valueOrDefault(i.hoverBorderWidth, i.borderWidth), e.radius = i.radius + i.hoverRadius + }, + removeHoverStyle: function(t) { + var e = t._model, + i = t._options; + e.backgroundColor = i.backgroundColor, e.borderColor = i.borderColor, e.borderWidth = i.borderWidth, e.radius = i.radius + }, + _resolveElementOptions: function(t, e) { + var i, n, a, r = this.chart, + s = r.data.datasets[this.index], + l = t.custom || {}, + u = r.options.elements.point, + d = o.options.resolve, + c = s.data[e], + h = {}, + f = { + chart: r, + dataIndex: e, + dataset: s, + datasetIndex: this.index + }, + g = ["backgroundColor", "borderColor", "borderWidth", "hoverBackgroundColor", "hoverBorderColor", "hoverBorderWidth", "hoverRadius", "hitRadius", "pointStyle"]; + for (i = 0, n = g.length; i < n; ++i) h[a = g[i]] = d([l[a], s[a], u[a]], f, e); + return h.radius = d([l.radius, c ? c.r : void 0, s.radius, u.radius], f, e), h + } + }) + } + }, { + 25: 25, + 40: 40, + 45: 45 + }], + 17: [function(t, e, i) { + "use strict"; + var n = t(25), + a = t(40), + o = t(45); + n._set("doughnut", { + animation: { + animateRotate: !0, + animateScale: !1 + }, + hover: { + mode: "single" + }, + legendCallback: function(t) { + var e = []; + e.push('
    '); + var i = t.data, + n = i.datasets, + a = i.labels; + if (n.length) + for (var o = 0; o < n[0].data.length; ++o) e.push('
  • '), a[o] && e.push(a[o]), e.push("
  • "); + return e.push("
"), e.join("") + }, + legend: { + labels: { + generateLabels: function(t) { + var e = t.data; + return e.labels.length && e.datasets.length ? e.labels.map(function(i, n) { + var a = t.getDatasetMeta(0), + r = e.datasets[0], + s = a.data[n], + l = s && s.custom || {}, + u = o.valueAtIndexOrDefault, + d = t.options.elements.arc; + return { + text: i, + fillStyle: l.backgroundColor ? l.backgroundColor : u(r.backgroundColor, n, d.backgroundColor), + strokeStyle: l.borderColor ? l.borderColor : u(r.borderColor, n, d.borderColor), + lineWidth: l.borderWidth ? l.borderWidth : u(r.borderWidth, n, d.borderWidth), + hidden: isNaN(r.data[n]) || a.data[n].hidden, + index: n + } + }) : [] + } + }, + onClick: function(t, e) { + var i, n, a, o = e.index, + r = this.chart; + for (i = 0, n = (r.data.datasets || []).length; i < n; ++i)(a = r.getDatasetMeta(i)).data[o] && (a.data[o].hidden = !a.data[o].hidden); + r.update() + } + }, + cutoutPercentage: 50, + rotation: -.5 * Math.PI, + circumference: 2 * Math.PI, + tooltips: { + callbacks: { + title: function() { + return "" + }, + label: function(t, e) { + var i = e.labels[t.index], + n = ": " + e.datasets[t.datasetIndex].data[t.index]; + return o.isArray(i) ? (i = i.slice())[0] += n : i += n, i + } + } + } + }), n._set("pie", o.clone(n.doughnut)), n._set("pie", { + cutoutPercentage: 0 + }), e.exports = function(t) { + t.controllers.doughnut = t.controllers.pie = t.DatasetController.extend({ + dataElementType: a.Arc, + linkScales: o.noop, + getRingIndex: function(t) { + for (var e = 0, i = 0; i < t; ++i) this.chart.isDatasetVisible(i) && ++e; + return e + }, + update: function(t) { + var e = this, + i = e.chart, + n = i.chartArea, + a = i.options, + r = a.elements.arc, + s = n.right - n.left - r.borderWidth, + l = n.bottom - n.top - r.borderWidth, + u = Math.min(s, l), + d = { + x: 0, + y: 0 + }, + c = e.getMeta(), + h = a.cutoutPercentage, + f = a.circumference; + if (f < 2 * Math.PI) { + var g = a.rotation % (2 * Math.PI), + p = (g += 2 * Math.PI * (g >= Math.PI ? -1 : g < -Math.PI ? 1 : 0)) + f, + m = Math.cos(g), + v = Math.sin(g), + b = Math.cos(p), + x = Math.sin(p), + y = g <= 0 && p >= 0 || g <= 2 * Math.PI && 2 * Math.PI <= p, + k = g <= .5 * Math.PI && .5 * Math.PI <= p || g <= 2.5 * Math.PI && 2.5 * Math.PI <= p, + M = g <= -Math.PI && -Math.PI <= p || g <= Math.PI && Math.PI <= p, + w = g <= .5 * -Math.PI && .5 * -Math.PI <= p || g <= 1.5 * Math.PI && 1.5 * Math.PI <= p, + S = h / 100, + C = M ? -1 : Math.min(m * (m < 0 ? 1 : S), b * (b < 0 ? 1 : S)), + _ = w ? -1 : Math.min(v * (v < 0 ? 1 : S), x * (x < 0 ? 1 : S)), + D = y ? 1 : Math.max(m * (m > 0 ? 1 : S), b * (b > 0 ? 1 : S)), + I = k ? 1 : Math.max(v * (v > 0 ? 1 : S), x * (x > 0 ? 1 : S)), + P = .5 * (D - C), + A = .5 * (I - _); + u = Math.min(s / P, l / A), d = { + x: -.5 * (D + C), + y: -.5 * (I + _) + } + } + i.borderWidth = e.getMaxBorderWidth(c.data), i.outerRadius = Math.max((u - i.borderWidth) / 2, 0), i.innerRadius = Math.max(h ? i.outerRadius / 100 * h : 0, 0), i.radiusLength = (i.outerRadius - i.innerRadius) / i.getVisibleDatasetCount(), i.offsetX = d.x * i.outerRadius, i.offsetY = d.y * i.outerRadius, c.total = e.calculateTotal(), e.outerRadius = i.outerRadius - i.radiusLength * e.getRingIndex(e.index), e.innerRadius = Math.max(e.outerRadius - i.radiusLength, 0), o.each(c.data, function(i, n) { + e.updateElement(i, n, t) + }) + }, + updateElement: function(t, e, i) { + var n = this, + a = n.chart, + r = a.chartArea, + s = a.options, + l = s.animation, + u = (r.left + r.right) / 2, + d = (r.top + r.bottom) / 2, + c = s.rotation, + h = s.rotation, + f = n.getDataset(), + g = i && l.animateRotate ? 0 : t.hidden ? 0 : n.calculateCircumference(f.data[e]) * (s.circumference / (2 * Math.PI)), + p = i && l.animateScale ? 0 : n.innerRadius, + m = i && l.animateScale ? 0 : n.outerRadius, + v = o.valueAtIndexOrDefault; + o.extend(t, { + _datasetIndex: n.index, + _index: e, + _model: { + x: u + a.offsetX, + y: d + a.offsetY, + startAngle: c, + endAngle: h, + circumference: g, + outerRadius: m, + innerRadius: p, + label: v(f.label, e, a.data.labels[e]) + } + }); + var b = t._model; + this.removeHoverStyle(t), i && l.animateRotate || (b.startAngle = 0 === e ? s.rotation : n.getMeta().data[e - 1]._model.endAngle, b.endAngle = b.startAngle + b.circumference), t.pivot() + }, + removeHoverStyle: function(e) { + t.DatasetController.prototype.removeHoverStyle.call(this, e, this.chart.options.elements.arc) + }, + calculateTotal: function() { + var t, e = this.getDataset(), + i = this.getMeta(), + n = 0; + return o.each(i.data, function(i, a) { + t = e.data[a], isNaN(t) || i.hidden || (n += Math.abs(t)) + }), n + }, + calculateCircumference: function(t) { + var e = this.getMeta().total; + return e > 0 && !isNaN(t) ? 2 * Math.PI * (Math.abs(t) / e) : 0 + }, + getMaxBorderWidth: function(t) { + for (var e, i, n = 0, a = this.index, o = t.length, r = 0; r < o; r++) e = t[r]._model ? t[r]._model.borderWidth : 0, n = (i = t[r]._chart ? t[r]._chart.config.data.datasets[a].hoverBorderWidth : 0) > (n = e > n ? e : n) ? i : n; + return n + } + }) + } + }, { + 25: 25, + 40: 40, + 45: 45 + }], + 18: [function(t, e, i) { + "use strict"; + var n = t(25), + a = t(40), + o = t(45); + n._set("line", { + showLines: !0, + spanGaps: !1, + hover: { + mode: "label" + }, + scales: { + xAxes: [{ + type: "category", + id: "x-axis-0" + }], + yAxes: [{ + type: "linear", + id: "y-axis-0" + }] + } + }), e.exports = function(t) { + function e(t, e) { + return o.valueOrDefault(t.showLine, e.showLines) + } + t.controllers.line = t.DatasetController.extend({ + datasetElementType: a.Line, + dataElementType: a.Point, + update: function(t) { + var i, n, a, r = this, + s = r.getMeta(), + l = s.dataset, + u = s.data || [], + d = r.chart.options, + c = d.elements.line, + h = r.getScaleForId(s.yAxisID), + f = r.getDataset(), + g = e(f, d); + for (g && (a = l.custom || {}, void 0 !== f.tension && void 0 === f.lineTension && (f.lineTension = f.tension), l._scale = h, l._datasetIndex = r.index, l._children = u, l._model = { + spanGaps: f.spanGaps ? f.spanGaps : d.spanGaps, + tension: a.tension ? a.tension : o.valueOrDefault(f.lineTension, c.tension), + backgroundColor: a.backgroundColor ? a.backgroundColor : f.backgroundColor || c.backgroundColor, + borderWidth: a.borderWidth ? a.borderWidth : f.borderWidth || c.borderWidth, + borderColor: a.borderColor ? a.borderColor : f.borderColor || c.borderColor, + borderCapStyle: a.borderCapStyle ? a.borderCapStyle : f.borderCapStyle || c.borderCapStyle, + borderDash: a.borderDash ? a.borderDash : f.borderDash || c.borderDash, + borderDashOffset: a.borderDashOffset ? a.borderDashOffset : f.borderDashOffset || c.borderDashOffset, + borderJoinStyle: a.borderJoinStyle ? a.borderJoinStyle : f.borderJoinStyle || c.borderJoinStyle, + fill: a.fill ? a.fill : void 0 !== f.fill ? f.fill : c.fill, + steppedLine: a.steppedLine ? a.steppedLine : o.valueOrDefault(f.steppedLine, c.stepped), + cubicInterpolationMode: a.cubicInterpolationMode ? a.cubicInterpolationMode : o.valueOrDefault(f.cubicInterpolationMode, c.cubicInterpolationMode) + }, l.pivot()), i = 0, n = u.length; i < n; ++i) r.updateElement(u[i], i, t); + for (g && 0 !== l._model.tension && r.updateBezierControlPoints(), i = 0, n = u.length; i < n; ++i) u[i].pivot() + }, + getPointBackgroundColor: function(t, e) { + var i = this.chart.options.elements.point.backgroundColor, + n = this.getDataset(), + a = t.custom || {}; + return a.backgroundColor ? i = a.backgroundColor : n.pointBackgroundColor ? i = o.valueAtIndexOrDefault(n.pointBackgroundColor, e, i) : n.backgroundColor && (i = n.backgroundColor), i + }, + getPointBorderColor: function(t, e) { + var i = this.chart.options.elements.point.borderColor, + n = this.getDataset(), + a = t.custom || {}; + return a.borderColor ? i = a.borderColor : n.pointBorderColor ? i = o.valueAtIndexOrDefault(n.pointBorderColor, e, i) : n.borderColor && (i = n.borderColor), i + }, + getPointBorderWidth: function(t, e) { + var i = this.chart.options.elements.point.borderWidth, + n = this.getDataset(), + a = t.custom || {}; + return isNaN(a.borderWidth) ? !isNaN(n.pointBorderWidth) || o.isArray(n.pointBorderWidth) ? i = o.valueAtIndexOrDefault(n.pointBorderWidth, e, i) : isNaN(n.borderWidth) || (i = n.borderWidth) : i = a.borderWidth, i + }, + updateElement: function(t, e, i) { + var n, a, r = this, + s = r.getMeta(), + l = t.custom || {}, + u = r.getDataset(), + d = r.index, + c = u.data[e], + h = r.getScaleForId(s.yAxisID), + f = r.getScaleForId(s.xAxisID), + g = r.chart.options.elements.point; + void 0 !== u.radius && void 0 === u.pointRadius && (u.pointRadius = u.radius), void 0 !== u.hitRadius && void 0 === u.pointHitRadius && (u.pointHitRadius = u.hitRadius), n = f.getPixelForValue("object" == typeof c ? c : NaN, e, d), a = i ? h.getBasePixel() : r.calculatePointY(c, e, d), t._xScale = f, t._yScale = h, t._datasetIndex = d, t._index = e, t._model = { + x: n, + y: a, + skip: l.skip || isNaN(n) || isNaN(a), + radius: l.radius || o.valueAtIndexOrDefault(u.pointRadius, e, g.radius), + pointStyle: l.pointStyle || o.valueAtIndexOrDefault(u.pointStyle, e, g.pointStyle), + backgroundColor: r.getPointBackgroundColor(t, e), + borderColor: r.getPointBorderColor(t, e), + borderWidth: r.getPointBorderWidth(t, e), + tension: s.dataset._model ? s.dataset._model.tension : 0, + steppedLine: !!s.dataset._model && s.dataset._model.steppedLine, + hitRadius: l.hitRadius || o.valueAtIndexOrDefault(u.pointHitRadius, e, g.hitRadius) + } + }, + calculatePointY: function(t, e, i) { + var n, a, o, r = this.chart, + s = this.getMeta(), + l = this.getScaleForId(s.yAxisID), + u = 0, + d = 0; + if (l.options.stacked) { + for (n = 0; n < i; n++) + if (a = r.data.datasets[n], "line" === (o = r.getDatasetMeta(n)).type && o.yAxisID === l.id && r.isDatasetVisible(n)) { + var c = Number(l.getRightValue(a.data[e])); + c < 0 ? d += c || 0 : u += c || 0 + } var h = Number(l.getRightValue(t)); + return h < 0 ? l.getPixelForValue(d + h) : l.getPixelForValue(u + h) + } + return l.getPixelForValue(t) + }, + updateBezierControlPoints: function() { + var t, e, i, n, a = this.getMeta(), + r = this.chart.chartArea, + s = a.data || []; + + function l(t, e, i) { + return Math.max(Math.min(t, i), e) + } + if (a.dataset._model.spanGaps && (s = s.filter(function(t) { + return !t._model.skip + })), "monotone" === a.dataset._model.cubicInterpolationMode) o.splineCurveMonotone(s); + else + for (t = 0, e = s.length; t < e; ++t) i = s[t]._model, n = o.splineCurve(o.previousItem(s, t)._model, i, o.nextItem(s, t)._model, a.dataset._model.tension), i.controlPointPreviousX = n.previous.x, i.controlPointPreviousY = n.previous.y, i.controlPointNextX = n.next.x, i.controlPointNextY = n.next.y; + if (this.chart.options.elements.line.capBezierPoints) + for (t = 0, e = s.length; t < e; ++t)(i = s[t]._model).controlPointPreviousX = l(i.controlPointPreviousX, r.left, r.right), i.controlPointPreviousY = l(i.controlPointPreviousY, r.top, r.bottom), i.controlPointNextX = l(i.controlPointNextX, r.left, r.right), i.controlPointNextY = l(i.controlPointNextY, r.top, r.bottom) + }, + draw: function() { + var t = this.chart, + i = this.getMeta(), + n = i.data || [], + a = t.chartArea, + r = n.length, + s = 0; + for (o.canvas.clipArea(t.ctx, a), e(this.getDataset(), t.options) && i.dataset.draw(), o.canvas.unclipArea(t.ctx); s < r; ++s) n[s].draw(a) + }, + setHoverStyle: function(t) { + var e = this.chart.data.datasets[t._datasetIndex], + i = t._index, + n = t.custom || {}, + a = t._model; + a.radius = n.hoverRadius || o.valueAtIndexOrDefault(e.pointHoverRadius, i, this.chart.options.elements.point.hoverRadius), a.backgroundColor = n.hoverBackgroundColor || o.valueAtIndexOrDefault(e.pointHoverBackgroundColor, i, o.getHoverColor(a.backgroundColor)), a.borderColor = n.hoverBorderColor || o.valueAtIndexOrDefault(e.pointHoverBorderColor, i, o.getHoverColor(a.borderColor)), a.borderWidth = n.hoverBorderWidth || o.valueAtIndexOrDefault(e.pointHoverBorderWidth, i, a.borderWidth) + }, + removeHoverStyle: function(t) { + var e = this, + i = e.chart.data.datasets[t._datasetIndex], + n = t._index, + a = t.custom || {}, + r = t._model; + void 0 !== i.radius && void 0 === i.pointRadius && (i.pointRadius = i.radius), r.radius = a.radius || o.valueAtIndexOrDefault(i.pointRadius, n, e.chart.options.elements.point.radius), r.backgroundColor = e.getPointBackgroundColor(t, n), r.borderColor = e.getPointBorderColor(t, n), r.borderWidth = e.getPointBorderWidth(t, n) + } + }) + } + }, { + 25: 25, + 40: 40, + 45: 45 + }], + 19: [function(t, e, i) { + "use strict"; + var n = t(25), + a = t(40), + o = t(45); + n._set("polarArea", { + scale: { + type: "radialLinear", + angleLines: { + display: !1 + }, + gridLines: { + circular: !0 + }, + pointLabels: { + display: !1 + }, + ticks: { + beginAtZero: !0 + } + }, + animation: { + animateRotate: !0, + animateScale: !0 + }, + startAngle: -.5 * Math.PI, + legendCallback: function(t) { + var e = []; + e.push('
    '); + var i = t.data, + n = i.datasets, + a = i.labels; + if (n.length) + for (var o = 0; o < n[0].data.length; ++o) e.push('
  • '), a[o] && e.push(a[o]), e.push("
  • "); + return e.push("
"), e.join("") + }, + legend: { + labels: { + generateLabels: function(t) { + var e = t.data; + return e.labels.length && e.datasets.length ? e.labels.map(function(i, n) { + var a = t.getDatasetMeta(0), + r = e.datasets[0], + s = a.data[n].custom || {}, + l = o.valueAtIndexOrDefault, + u = t.options.elements.arc; + return { + text: i, + fillStyle: s.backgroundColor ? s.backgroundColor : l(r.backgroundColor, n, u.backgroundColor), + strokeStyle: s.borderColor ? s.borderColor : l(r.borderColor, n, u.borderColor), + lineWidth: s.borderWidth ? s.borderWidth : l(r.borderWidth, n, u.borderWidth), + hidden: isNaN(r.data[n]) || a.data[n].hidden, + index: n + } + }) : [] + } + }, + onClick: function(t, e) { + var i, n, a, o = e.index, + r = this.chart; + for (i = 0, n = (r.data.datasets || []).length; i < n; ++i)(a = r.getDatasetMeta(i)).data[o].hidden = !a.data[o].hidden; + r.update() + } + }, + tooltips: { + callbacks: { + title: function() { + return "" + }, + label: function(t, e) { + return e.labels[t.index] + ": " + t.yLabel + } + } + } + }), e.exports = function(t) { + t.controllers.polarArea = t.DatasetController.extend({ + dataElementType: a.Arc, + linkScales: o.noop, + update: function(t) { + var e = this, + i = e.chart, + n = i.chartArea, + a = e.getMeta(), + r = i.options, + s = r.elements.arc, + l = Math.min(n.right - n.left, n.bottom - n.top); + i.outerRadius = Math.max((l - s.borderWidth / 2) / 2, 0), i.innerRadius = Math.max(r.cutoutPercentage ? i.outerRadius / 100 * r.cutoutPercentage : 1, 0), i.radiusLength = (i.outerRadius - i.innerRadius) / i.getVisibleDatasetCount(), e.outerRadius = i.outerRadius - i.radiusLength * e.index, e.innerRadius = e.outerRadius - i.radiusLength, a.count = e.countVisibleElements(), o.each(a.data, function(i, n) { + e.updateElement(i, n, t) + }) + }, + updateElement: function(t, e, i) { + for (var n = this, a = n.chart, r = n.getDataset(), s = a.options, l = s.animation, u = a.scale, d = a.data.labels, c = n.calculateCircumference(r.data[e]), h = u.xCenter, f = u.yCenter, g = 0, p = n.getMeta(), m = 0; m < e; ++m) isNaN(r.data[m]) || p.data[m].hidden || ++g; + var v = s.startAngle, + b = t.hidden ? 0 : u.getDistanceFromCenterForValue(r.data[e]), + x = v + c * g, + y = x + (t.hidden ? 0 : c), + k = l.animateScale ? 0 : u.getDistanceFromCenterForValue(r.data[e]); + o.extend(t, { + _datasetIndex: n.index, + _index: e, + _scale: u, + _model: { + x: h, + y: f, + innerRadius: 0, + outerRadius: i ? k : b, + startAngle: i && l.animateRotate ? v : x, + endAngle: i && l.animateRotate ? v : y, + label: o.valueAtIndexOrDefault(d, e, d[e]) + } + }), n.removeHoverStyle(t), t.pivot() + }, + removeHoverStyle: function(e) { + t.DatasetController.prototype.removeHoverStyle.call(this, e, this.chart.options.elements.arc) + }, + countVisibleElements: function() { + var t = this.getDataset(), + e = this.getMeta(), + i = 0; + return o.each(e.data, function(e, n) { + isNaN(t.data[n]) || e.hidden || i++ + }), i + }, + calculateCircumference: function(t) { + var e = this.getMeta().count; + return e > 0 && !isNaN(t) ? 2 * Math.PI / e : 0 + } + }) + } + }, { + 25: 25, + 40: 40, + 45: 45 + }], + 20: [function(t, e, i) { + "use strict"; + var n = t(25), + a = t(40), + o = t(45); + n._set("radar", { + scale: { + type: "radialLinear" + }, + elements: { + line: { + tension: 0 + } + } + }), e.exports = function(t) { + t.controllers.radar = t.DatasetController.extend({ + datasetElementType: a.Line, + dataElementType: a.Point, + linkScales: o.noop, + update: function(t) { + var e = this, + i = e.getMeta(), + n = i.dataset, + a = i.data, + r = n.custom || {}, + s = e.getDataset(), + l = e.chart.options.elements.line, + u = e.chart.scale; + void 0 !== s.tension && void 0 === s.lineTension && (s.lineTension = s.tension), o.extend(i.dataset, { + _datasetIndex: e.index, + _scale: u, + _children: a, + _loop: !0, + _model: { + tension: r.tension ? r.tension : o.valueOrDefault(s.lineTension, l.tension), + backgroundColor: r.backgroundColor ? r.backgroundColor : s.backgroundColor || l.backgroundColor, + borderWidth: r.borderWidth ? r.borderWidth : s.borderWidth || l.borderWidth, + borderColor: r.borderColor ? r.borderColor : s.borderColor || l.borderColor, + fill: r.fill ? r.fill : void 0 !== s.fill ? s.fill : l.fill, + borderCapStyle: r.borderCapStyle ? r.borderCapStyle : s.borderCapStyle || l.borderCapStyle, + borderDash: r.borderDash ? r.borderDash : s.borderDash || l.borderDash, + borderDashOffset: r.borderDashOffset ? r.borderDashOffset : s.borderDashOffset || l.borderDashOffset, + borderJoinStyle: r.borderJoinStyle ? r.borderJoinStyle : s.borderJoinStyle || l.borderJoinStyle + } + }), i.dataset.pivot(), o.each(a, function(i, n) { + e.updateElement(i, n, t) + }, e), e.updateBezierControlPoints() + }, + updateElement: function(t, e, i) { + var n = this, + a = t.custom || {}, + r = n.getDataset(), + s = n.chart.scale, + l = n.chart.options.elements.point, + u = s.getPointPositionForValue(e, r.data[e]); + void 0 !== r.radius && void 0 === r.pointRadius && (r.pointRadius = r.radius), void 0 !== r.hitRadius && void 0 === r.pointHitRadius && (r.pointHitRadius = r.hitRadius), o.extend(t, { + _datasetIndex: n.index, + _index: e, + _scale: s, + _model: { + x: i ? s.xCenter : u.x, + y: i ? s.yCenter : u.y, + tension: a.tension ? a.tension : o.valueOrDefault(r.lineTension, n.chart.options.elements.line.tension), + radius: a.radius ? a.radius : o.valueAtIndexOrDefault(r.pointRadius, e, l.radius), + backgroundColor: a.backgroundColor ? a.backgroundColor : o.valueAtIndexOrDefault(r.pointBackgroundColor, e, l.backgroundColor), + borderColor: a.borderColor ? a.borderColor : o.valueAtIndexOrDefault(r.pointBorderColor, e, l.borderColor), + borderWidth: a.borderWidth ? a.borderWidth : o.valueAtIndexOrDefault(r.pointBorderWidth, e, l.borderWidth), + pointStyle: a.pointStyle ? a.pointStyle : o.valueAtIndexOrDefault(r.pointStyle, e, l.pointStyle), + hitRadius: a.hitRadius ? a.hitRadius : o.valueAtIndexOrDefault(r.pointHitRadius, e, l.hitRadius) + } + }), t._model.skip = a.skip ? a.skip : isNaN(t._model.x) || isNaN(t._model.y) + }, + updateBezierControlPoints: function() { + var t = this.chart.chartArea, + e = this.getMeta(); + o.each(e.data, function(i, n) { + var a = i._model, + r = o.splineCurve(o.previousItem(e.data, n, !0)._model, a, o.nextItem(e.data, n, !0)._model, a.tension); + a.controlPointPreviousX = Math.max(Math.min(r.previous.x, t.right), t.left), a.controlPointPreviousY = Math.max(Math.min(r.previous.y, t.bottom), t.top), a.controlPointNextX = Math.max(Math.min(r.next.x, t.right), t.left), a.controlPointNextY = Math.max(Math.min(r.next.y, t.bottom), t.top), i.pivot() + }) + }, + setHoverStyle: function(t) { + var e = this.chart.data.datasets[t._datasetIndex], + i = t.custom || {}, + n = t._index, + a = t._model; + a.radius = i.hoverRadius ? i.hoverRadius : o.valueAtIndexOrDefault(e.pointHoverRadius, n, this.chart.options.elements.point.hoverRadius), a.backgroundColor = i.hoverBackgroundColor ? i.hoverBackgroundColor : o.valueAtIndexOrDefault(e.pointHoverBackgroundColor, n, o.getHoverColor(a.backgroundColor)), a.borderColor = i.hoverBorderColor ? i.hoverBorderColor : o.valueAtIndexOrDefault(e.pointHoverBorderColor, n, o.getHoverColor(a.borderColor)), a.borderWidth = i.hoverBorderWidth ? i.hoverBorderWidth : o.valueAtIndexOrDefault(e.pointHoverBorderWidth, n, a.borderWidth) + }, + removeHoverStyle: function(t) { + var e = this.chart.data.datasets[t._datasetIndex], + i = t.custom || {}, + n = t._index, + a = t._model, + r = this.chart.options.elements.point; + a.radius = i.radius ? i.radius : o.valueAtIndexOrDefault(e.pointRadius, n, r.radius), a.backgroundColor = i.backgroundColor ? i.backgroundColor : o.valueAtIndexOrDefault(e.pointBackgroundColor, n, r.backgroundColor), a.borderColor = i.borderColor ? i.borderColor : o.valueAtIndexOrDefault(e.pointBorderColor, n, r.borderColor), a.borderWidth = i.borderWidth ? i.borderWidth : o.valueAtIndexOrDefault(e.pointBorderWidth, n, r.borderWidth) + } + }) + } + }, { + 25: 25, + 40: 40, + 45: 45 + }], + 21: [function(t, e, i) { + "use strict"; + t(25)._set("scatter", { + hover: { + mode: "single" + }, + scales: { + xAxes: [{ + id: "x-axis-1", + type: "linear", + position: "bottom" + }], + yAxes: [{ + id: "y-axis-1", + type: "linear", + position: "left" + }] + }, + showLines: !1, + tooltips: { + callbacks: { + title: function() { + return "" + }, + label: function(t) { + return "(" + t.xLabel + ", " + t.yLabel + ")" + } + } + } + }), e.exports = function(t) { + t.controllers.scatter = t.controllers.line + } + }, { + 25: 25 + }], + 22: [function(t, e, i) { + "use strict"; + var n = t(25), + a = t(26), + o = t(45); + n._set("global", { + animation: { + duration: 1e3, + easing: "easeOutQuart", + onProgress: o.noop, + onComplete: o.noop + } + }), e.exports = function(t) { + t.Animation = a.extend({ + chart: null, + currentStep: 0, + numSteps: 60, + easing: "", + render: null, + onAnimationProgress: null, + onAnimationComplete: null + }), t.animationService = { + frameDuration: 17, + animations: [], + dropFrames: 0, + request: null, + addAnimation: function(t, e, i, n) { + var a, o, r = this.animations; + for (e.chart = t, n || (t.animating = !0), a = 0, o = r.length; a < o; ++a) + if (r[a].chart === t) return void(r[a] = e); + r.push(e), 1 === r.length && this.requestAnimationFrame() + }, + cancelAnimation: function(t) { + var e = o.findIndex(this.animations, function(e) { + return e.chart === t + }); - 1 !== e && (this.animations.splice(e, 1), t.animating = !1) + }, + requestAnimationFrame: function() { + var t = this; + null === t.request && (t.request = o.requestAnimFrame.call(window, function() { + t.request = null, t.startDigest() + })) + }, + startDigest: function() { + var t = this, + e = Date.now(), + i = 0; + t.dropFrames > 1 && (i = Math.floor(t.dropFrames), t.dropFrames = t.dropFrames % 1), t.advance(1 + i); + var n = Date.now(); + t.dropFrames += (n - e) / t.frameDuration, t.animations.length > 0 && t.requestAnimationFrame() + }, + advance: function(t) { + for (var e, i, n = this.animations, a = 0; a < n.length;) i = (e = n[a]).chart, e.currentStep = (e.currentStep || 0) + t, e.currentStep = Math.min(e.currentStep, e.numSteps), o.callback(e.render, [i, e], i), o.callback(e.onAnimationProgress, [e], i), e.currentStep >= e.numSteps ? (o.callback(e.onAnimationComplete, [e], i), i.animating = !1, n.splice(a, 1)) : ++a + } + }, Object.defineProperty(t.Animation.prototype, "animationObject", { + get: function() { + return this + } + }), Object.defineProperty(t.Animation.prototype, "chartInstance", { + get: function() { + return this.chart + }, + set: function(t) { + this.chart = t + } + }) + } + }, { + 25: 25, + 26: 26, + 45: 45 + }], + 23: [function(t, e, i) { + "use strict"; + var n = t(25), + a = t(45), + o = t(28), + r = t(30), + s = t(48), + l = t(31); + e.exports = function(t) { + function e(t) { + return "top" === t || "bottom" === t + } + t.types = {}, t.instances = {}, t.controllers = {}, a.extend(t.prototype, { + construct: function(e, i) { + var o, r, l = this; + (r = (o = (o = i) || {}).data = o.data || {}).datasets = r.datasets || [], r.labels = r.labels || [], o.options = a.configMerge(n.global, n[o.type], o.options || {}), i = o; + var u = s.acquireContext(e, i), + d = u && u.canvas, + c = d && d.height, + h = d && d.width; + l.id = a.uid(), l.ctx = u, l.canvas = d, l.config = i, l.width = h, l.height = c, l.aspectRatio = c ? h / c : null, l.options = i.options, l._bufferedRender = !1, l.chart = l, l.controller = l, t.instances[l.id] = l, Object.defineProperty(l, "data", { + get: function() { + return l.config.data + }, + set: function(t) { + l.config.data = t + } + }), u && d ? (l.initialize(), l.update()) : console.error("Failed to create chart: can't acquire context from the given item") + }, + initialize: function() { + var t = this; + return l.notify(t, "beforeInit"), a.retinaScale(t, t.options.devicePixelRatio), t.bindEvents(), t.options.responsive && t.resize(!0), t.ensureScalesHaveIDs(), t.buildOrUpdateScales(), t.initToolTip(), l.notify(t, "afterInit"), t + }, + clear: function() { + return a.canvas.clear(this), this + }, + stop: function() { + return t.animationService.cancelAnimation(this), this + }, + resize: function(t) { + var e = this, + i = e.options, + n = e.canvas, + o = i.maintainAspectRatio && e.aspectRatio || null, + r = Math.max(0, Math.floor(a.getMaximumWidth(n))), + s = Math.max(0, Math.floor(o ? r / o : a.getMaximumHeight(n))); + if ((e.width !== r || e.height !== s) && (n.width = e.width = r, n.height = e.height = s, n.style.width = r + "px", n.style.height = s + "px", a.retinaScale(e, i.devicePixelRatio), !t)) { + var u = { + width: r, + height: s + }; + l.notify(e, "resize", [u]), e.options.onResize && e.options.onResize(e, u), e.stop(), e.update(e.options.responsiveAnimationDuration) + } + }, + ensureScalesHaveIDs: function() { + var t = this.options, + e = t.scales || {}, + i = t.scale; + a.each(e.xAxes, function(t, e) { + t.id = t.id || "x-axis-" + e + }), a.each(e.yAxes, function(t, e) { + t.id = t.id || "y-axis-" + e + }), i && (i.id = i.id || "scale") + }, + buildOrUpdateScales: function() { + var i = this, + n = i.options, + o = i.scales || {}, + r = [], + s = Object.keys(o).reduce(function(t, e) { + return t[e] = !1, t + }, {}); + n.scales && (r = r.concat((n.scales.xAxes || []).map(function(t) { + return { + options: t, + dtype: "category", + dposition: "bottom" + } + }), (n.scales.yAxes || []).map(function(t) { + return { + options: t, + dtype: "linear", + dposition: "left" + } + }))), n.scale && r.push({ + options: n.scale, + dtype: "radialLinear", + isDefault: !0, + dposition: "chartArea" + }), a.each(r, function(n) { + var r = n.options, + l = r.id, + u = a.valueOrDefault(r.type, n.dtype); + e(r.position) !== e(n.dposition) && (r.position = n.dposition), s[l] = !0; + var d = null; + if (l in o && o[l].type === u)(d = o[l]).options = r, d.ctx = i.ctx, d.chart = i; + else { + var c = t.scaleService.getScaleConstructor(u); + if (!c) return; + d = new c({ + id: l, + type: u, + options: r, + ctx: i.ctx, + chart: i + }), o[d.id] = d + } + d.mergeTicksOptions(), n.isDefault && (i.scale = d) + }), a.each(s, function(t, e) { + t || delete o[e] + }), i.scales = o, t.scaleService.addScalesToLayout(this) + }, + buildOrUpdateControllers: function() { + var e = this, + i = [], + n = []; + return a.each(e.data.datasets, function(a, o) { + var r = e.getDatasetMeta(o), + s = a.type || e.config.type; + if (r.type && r.type !== s && (e.destroyDatasetMeta(o), r = e.getDatasetMeta(o)), r.type = s, i.push(r.type), r.controller) r.controller.updateIndex(o), r.controller.linkScales(); + else { + var l = t.controllers[r.type]; + if (void 0 === l) throw new Error('"' + r.type + '" is not a chart type.'); + r.controller = new l(e, o), n.push(r.controller) + } + }, e), n + }, + resetElements: function() { + var t = this; + a.each(t.data.datasets, function(e, i) { + t.getDatasetMeta(i).controller.reset() + }, t) + }, + reset: function() { + this.resetElements(), this.tooltip.initialize() + }, + update: function(e) { + var i, n, o = this; + if (e && "object" == typeof e || (e = { + duration: e, + lazy: arguments[1] + }), n = (i = o).options, a.each(i.scales, function(t) { + r.removeBox(i, t) + }), n = a.configMerge(t.defaults.global, t.defaults[i.config.type], n), i.options = i.config.options = n, i.ensureScalesHaveIDs(), i.buildOrUpdateScales(), i.tooltip._options = n.tooltips, i.tooltip.initialize(), l._invalidate(o), !1 !== l.notify(o, "beforeUpdate")) { + o.tooltip._data = o.data; + var s = o.buildOrUpdateControllers(); + a.each(o.data.datasets, function(t, e) { + o.getDatasetMeta(e).controller.buildOrUpdateElements() + }, o), o.updateLayout(), o.options.animation && o.options.animation.duration && a.each(s, function(t) { + t.reset() + }), o.updateDatasets(), o.tooltip.initialize(), o.lastActive = [], l.notify(o, "afterUpdate"), o._bufferedRender ? o._bufferedRequest = { + duration: e.duration, + easing: e.easing, + lazy: e.lazy + } : o.render(e) + } + }, + updateLayout: function() { + !1 !== l.notify(this, "beforeLayout") && (r.update(this, this.width, this.height), l.notify(this, "afterScaleUpdate"), l.notify(this, "afterLayout")) + }, + updateDatasets: function() { + if (!1 !== l.notify(this, "beforeDatasetsUpdate")) { + for (var t = 0, e = this.data.datasets.length; t < e; ++t) this.updateDataset(t); + l.notify(this, "afterDatasetsUpdate") + } + }, + updateDataset: function(t) { + var e = this.getDatasetMeta(t), + i = { + meta: e, + index: t + }; + !1 !== l.notify(this, "beforeDatasetUpdate", [i]) && (e.controller.update(), l.notify(this, "afterDatasetUpdate", [i])) + }, + render: function(e) { + var i = this; + e && "object" == typeof e || (e = { + duration: e, + lazy: arguments[1] + }); + var n = e.duration, + o = e.lazy; + if (!1 !== l.notify(i, "beforeRender")) { + var r = i.options.animation, + s = function(t) { + l.notify(i, "afterRender"), a.callback(r && r.onComplete, [t], i) + }; + if (r && (void 0 !== n && 0 !== n || void 0 === n && 0 !== r.duration)) { + var u = new t.Animation({ + numSteps: (n || r.duration) / 16.66, + easing: e.easing || r.easing, + render: function(t, e) { + var i = a.easing.effects[e.easing], + n = e.currentStep, + o = n / e.numSteps; + t.draw(i(o), o, n) + }, + onAnimationProgress: r.onProgress, + onAnimationComplete: s + }); + t.animationService.addAnimation(i, u, n, o) + } else i.draw(), s(new t.Animation({ + numSteps: 0, + chart: i + })); + return i + } + }, + draw: function(t) { + var e = this; + e.clear(), a.isNullOrUndef(t) && (t = 1), e.transition(t), !1 !== l.notify(e, "beforeDraw", [t]) && (a.each(e.boxes, function(t) { + t.draw(e.chartArea) + }, e), e.scale && e.scale.draw(), e.drawDatasets(t), e._drawTooltip(t), l.notify(e, "afterDraw", [t])) + }, + transition: function(t) { + for (var e = 0, i = (this.data.datasets || []).length; e < i; ++e) this.isDatasetVisible(e) && this.getDatasetMeta(e).controller.transition(t); + this.tooltip.transition(t) + }, + drawDatasets: function(t) { + var e = this; + if (!1 !== l.notify(e, "beforeDatasetsDraw", [t])) { + for (var i = (e.data.datasets || []).length - 1; i >= 0; --i) e.isDatasetVisible(i) && e.drawDataset(i, t); + l.notify(e, "afterDatasetsDraw", [t]) + } + }, + drawDataset: function(t, e) { + var i = this.getDatasetMeta(t), + n = { + meta: i, + index: t, + easingValue: e + }; + !1 !== l.notify(this, "beforeDatasetDraw", [n]) && (i.controller.draw(e), l.notify(this, "afterDatasetDraw", [n])) + }, + _drawTooltip: function(t) { + var e = this.tooltip, + i = { + tooltip: e, + easingValue: t + }; + !1 !== l.notify(this, "beforeTooltipDraw", [i]) && (e.draw(), l.notify(this, "afterTooltipDraw", [i])) + }, + getElementAtEvent: function(t) { + return o.modes.single(this, t) + }, + getElementsAtEvent: function(t) { + return o.modes.label(this, t, { + intersect: !0 + }) + }, + getElementsAtXAxis: function(t) { + return o.modes["x-axis"](this, t, { + intersect: !0 + }) + }, + getElementsAtEventForMode: function(t, e, i) { + var n = o.modes[e]; + return "function" == typeof n ? n(this, t, i) : [] + }, + getDatasetAtEvent: function(t) { + return o.modes.dataset(this, t, { + intersect: !0 + }) + }, + getDatasetMeta: function(t) { + var e = this.data.datasets[t]; + e._meta || (e._meta = {}); + var i = e._meta[this.id]; + return i || (i = e._meta[this.id] = { + type: null, + data: [], + dataset: null, + controller: null, + hidden: null, + xAxisID: null, + yAxisID: null + }), i + }, + getVisibleDatasetCount: function() { + for (var t = 0, e = 0, i = this.data.datasets.length; e < i; ++e) this.isDatasetVisible(e) && t++; + return t + }, + isDatasetVisible: function(t) { + var e = this.getDatasetMeta(t); + return "boolean" == typeof e.hidden ? !e.hidden : !this.data.datasets[t].hidden + }, + generateLegend: function() { + return this.options.legendCallback(this) + }, + destroyDatasetMeta: function(t) { + var e = this.id, + i = this.data.datasets[t], + n = i._meta && i._meta[e]; + n && (n.controller.destroy(), delete i._meta[e]) + }, + destroy: function() { + var e, i, n = this, + o = n.canvas; + for (n.stop(), e = 0, i = n.data.datasets.length; e < i; ++e) n.destroyDatasetMeta(e); + o && (n.unbindEvents(), a.canvas.clear(n), s.releaseContext(n.ctx), n.canvas = null, n.ctx = null), l.notify(n, "destroy"), delete t.instances[n.id] + }, + toBase64Image: function() { + return this.canvas.toDataURL.apply(this.canvas, arguments) + }, + initToolTip: function() { + var e = this; + e.tooltip = new t.Tooltip({ + _chart: e, + _chartInstance: e, + _data: e.data, + _options: e.options.tooltips + }, e) + }, + bindEvents: function() { + var t = this, + e = t._listeners = {}, + i = function() { + t.eventHandler.apply(t, arguments) + }; + a.each(t.options.events, function(n) { + s.addEventListener(t, n, i), e[n] = i + }), t.options.responsive && (i = function() { + t.resize() + }, s.addEventListener(t, "resize", i), e.resize = i) + }, + unbindEvents: function() { + var t = this, + e = t._listeners; + e && (delete t._listeners, a.each(e, function(e, i) { + s.removeEventListener(t, i, e) + })) + }, + updateHoverStyle: function(t, e, i) { + var n, a, o, r = i ? "setHoverStyle" : "removeHoverStyle"; + for (a = 0, o = t.length; a < o; ++a)(n = t[a]) && this.getDatasetMeta(n._datasetIndex).controller[r](n) + }, + eventHandler: function(t) { + var e = this, + i = e.tooltip; + if (!1 !== l.notify(e, "beforeEvent", [t])) { + e._bufferedRender = !0, e._bufferedRequest = null; + var n = e.handleEvent(t); + i && (n = i._start ? i.handleEvent(t) : n | i.handleEvent(t)), l.notify(e, "afterEvent", [t]); + var a = e._bufferedRequest; + return a ? e.render(a) : n && !e.animating && (e.stop(), e.render(e.options.hover.animationDuration, !0)), e._bufferedRender = !1, e._bufferedRequest = null, e + } + }, + handleEvent: function(t) { + var e, i = this, + n = i.options || {}, + o = n.hover; + return i.lastActive = i.lastActive || [], "mouseout" === t.type ? i.active = [] : i.active = i.getElementsAtEventForMode(t, o.mode, o), a.callback(n.onHover || n.hover.onHover, [t.native, i.active], i), "mouseup" !== t.type && "click" !== t.type || n.onClick && n.onClick.call(i, t.native, i.active), i.lastActive.length && i.updateHoverStyle(i.lastActive, o.mode, !1), i.active.length && o.mode && i.updateHoverStyle(i.active, o.mode, !0), e = !a.arrayEquals(i.active, i.lastActive), i.lastActive = i.active, e + } + }), t.Controller = t + } + }, { + 25: 25, + 28: 28, + 30: 30, + 31: 31, + 45: 45, + 48: 48 + }], + 24: [function(t, e, i) { + "use strict"; + var n = t(45); + e.exports = function(t) { + var e = ["push", "pop", "shift", "splice", "unshift"]; + + function i(t, i) { + var n = t._chartjs; + if (n) { + var a = n.listeners, + o = a.indexOf(i); - 1 !== o && a.splice(o, 1), a.length > 0 || (e.forEach(function(e) { + delete t[e] + }), delete t._chartjs) + } + } + t.DatasetController = function(t, e) { + this.initialize(t, e) + }, n.extend(t.DatasetController.prototype, { + datasetElementType: null, + dataElementType: null, + initialize: function(t, e) { + this.chart = t, this.index = e, this.linkScales(), this.addElements() + }, + updateIndex: function(t) { + this.index = t + }, + linkScales: function() { + var t = this, + e = t.getMeta(), + i = t.getDataset(); + null !== e.xAxisID && e.xAxisID in t.chart.scales || (e.xAxisID = i.xAxisID || t.chart.options.scales.xAxes[0].id), null !== e.yAxisID && e.yAxisID in t.chart.scales || (e.yAxisID = i.yAxisID || t.chart.options.scales.yAxes[0].id) + }, + getDataset: function() { + return this.chart.data.datasets[this.index] + }, + getMeta: function() { + return this.chart.getDatasetMeta(this.index) + }, + getScaleForId: function(t) { + return this.chart.scales[t] + }, + reset: function() { + this.update(!0) + }, + destroy: function() { + this._data && i(this._data, this) + }, + createMetaDataset: function() { + var t = this.datasetElementType; + return t && new t({ + _chart: this.chart, + _datasetIndex: this.index + }) + }, + createMetaData: function(t) { + var e = this.dataElementType; + return e && new e({ + _chart: this.chart, + _datasetIndex: this.index, + _index: t + }) + }, + addElements: function() { + var t, e, i = this.getMeta(), + n = this.getDataset().data || [], + a = i.data; + for (t = 0, e = n.length; t < e; ++t) a[t] = a[t] || this.createMetaData(t); + i.dataset = i.dataset || this.createMetaDataset() + }, + addElementAndReset: function(t) { + var e = this.createMetaData(t); + this.getMeta().data.splice(t, 0, e), this.updateElement(e, t, !0) + }, + buildOrUpdateElements: function() { + var t, a, o = this, + r = o.getDataset(), + s = r.data || (r.data = []); + o._data !== s && (o._data && i(o._data, o), a = o, (t = s)._chartjs ? t._chartjs.listeners.push(a) : (Object.defineProperty(t, "_chartjs", { + configurable: !0, + enumerable: !1, + value: { + listeners: [a] + } + }), e.forEach(function(e) { + var i = "onData" + e.charAt(0).toUpperCase() + e.slice(1), + a = t[e]; + Object.defineProperty(t, e, { + configurable: !0, + enumerable: !1, + value: function() { + var e = Array.prototype.slice.call(arguments), + o = a.apply(this, e); + return n.each(t._chartjs.listeners, function(t) { + "function" == typeof t[i] && t[i].apply(t, e) + }), o + } + }) + })), o._data = s), o.resyncElements() + }, + update: n.noop, + transition: function(t) { + for (var e = this.getMeta(), i = e.data || [], n = i.length, a = 0; a < n; ++a) i[a].transition(t); + e.dataset && e.dataset.transition(t) + }, + draw: function() { + var t = this.getMeta(), + e = t.data || [], + i = e.length, + n = 0; + for (t.dataset && t.dataset.draw(); n < i; ++n) e[n].draw() + }, + removeHoverStyle: function(t, e) { + var i = this.chart.data.datasets[t._datasetIndex], + a = t._index, + o = t.custom || {}, + r = n.valueAtIndexOrDefault, + s = t._model; + s.backgroundColor = o.backgroundColor ? o.backgroundColor : r(i.backgroundColor, a, e.backgroundColor), s.borderColor = o.borderColor ? o.borderColor : r(i.borderColor, a, e.borderColor), s.borderWidth = o.borderWidth ? o.borderWidth : r(i.borderWidth, a, e.borderWidth) + }, + setHoverStyle: function(t) { + var e = this.chart.data.datasets[t._datasetIndex], + i = t._index, + a = t.custom || {}, + o = n.valueAtIndexOrDefault, + r = n.getHoverColor, + s = t._model; + s.backgroundColor = a.hoverBackgroundColor ? a.hoverBackgroundColor : o(e.hoverBackgroundColor, i, r(s.backgroundColor)), s.borderColor = a.hoverBorderColor ? a.hoverBorderColor : o(e.hoverBorderColor, i, r(s.borderColor)), s.borderWidth = a.hoverBorderWidth ? a.hoverBorderWidth : o(e.hoverBorderWidth, i, s.borderWidth) + }, + resyncElements: function() { + var t = this.getMeta(), + e = this.getDataset().data, + i = t.data.length, + n = e.length; + n < i ? t.data.splice(n, i - n) : n > i && this.insertElements(i, n - i) + }, + insertElements: function(t, e) { + for (var i = 0; i < e; ++i) this.addElementAndReset(t + i) + }, + onDataPush: function() { + this.insertElements(this.getDataset().data.length - 1, arguments.length) + }, + onDataPop: function() { + this.getMeta().data.pop() + }, + onDataShift: function() { + this.getMeta().data.shift() + }, + onDataSplice: function(t, e) { + this.getMeta().data.splice(t, e), this.insertElements(t, arguments.length - 2) + }, + onDataUnshift: function() { + this.insertElements(0, arguments.length) + } + }), t.DatasetController.extend = n.inherits + } + }, { + 45: 45 + }], + 25: [function(t, e, i) { + "use strict"; + var n = t(45); + e.exports = { + _set: function(t, e) { + return n.merge(this[t] || (this[t] = {}), e) + } + } + }, { + 45: 45 + }], + 26: [function(t, e, i) { + "use strict"; + var n = t(3), + a = t(45); + var o = function(t) { + a.extend(this, t), this.initialize.apply(this, arguments) + }; + a.extend(o.prototype, { + initialize: function() { + this.hidden = !1 + }, + pivot: function() { + var t = this; + return t._view || (t._view = a.clone(t._model)), t._start = {}, t + }, + transition: function(t) { + var e = this, + i = e._model, + a = e._start, + o = e._view; + return i && 1 !== t ? (o || (o = e._view = {}), a || (a = e._start = {}), function(t, e, i, a) { + var o, r, s, l, u, d, c, h, f, g = Object.keys(i); + for (o = 0, r = g.length; o < r; ++o) + if (d = i[s = g[o]], e.hasOwnProperty(s) || (e[s] = d), (l = e[s]) !== d && "_" !== s[0]) { + if (t.hasOwnProperty(s) || (t[s] = l), (c = typeof d) == typeof(u = t[s])) + if ("string" === c) { + if ((h = n(u)).valid && (f = n(d)).valid) { + e[s] = f.mix(h, a).rgbString(); + continue + } + } else if ("number" === c && isFinite(u) && isFinite(d)) { + e[s] = u + (d - u) * a; + continue + } + e[s] = d + } + }(a, o, i, t), e) : (e._view = i, e._start = null, e) + }, + tooltipPosition: function() { + return { + x: this._model.x, + y: this._model.y + } + }, + hasValue: function() { + return a.isNumber(this._model.x) && a.isNumber(this._model.y) + } + }), o.extend = a.inherits, e.exports = o + }, { + 3: 3, + 45: 45 + }], + 27: [function(t, e, i) { + "use strict"; + var n = t(3), + a = t(25), + o = t(45); + e.exports = function(t) { + function e(t, e, i) { + var n; + return "string" == typeof t ? (n = parseInt(t, 10), -1 !== t.indexOf("%") && (n = n / 100 * e.parentNode[i])) : n = t, n + } + + function i(t) { + return null != t && "none" !== t + } + + function r(t, n, a) { + var o = document.defaultView, + r = t.parentNode, + s = o.getComputedStyle(t)[n], + l = o.getComputedStyle(r)[n], + u = i(s), + d = i(l), + c = Number.POSITIVE_INFINITY; + return u || d ? Math.min(u ? e(s, t, a) : c, d ? e(l, r, a) : c) : "none" + } + o.configMerge = function() { + return o.merge(o.clone(arguments[0]), [].slice.call(arguments, 1), { + merger: function(e, i, n, a) { + var r = i[e] || {}, + s = n[e]; + "scales" === e ? i[e] = o.scaleMerge(r, s) : "scale" === e ? i[e] = o.merge(r, [t.scaleService.getScaleDefaults(s.type), s]) : o._merger(e, i, n, a) + } + }) + }, o.scaleMerge = function() { + return o.merge(o.clone(arguments[0]), [].slice.call(arguments, 1), { + merger: function(e, i, n, a) { + if ("xAxes" === e || "yAxes" === e) { + var r, s, l, u = n[e].length; + for (i[e] || (i[e] = []), r = 0; r < u; ++r) l = n[e][r], s = o.valueOrDefault(l.type, "xAxes" === e ? "category" : "linear"), r >= i[e].length && i[e].push({}), !i[e][r].type || l.type && l.type !== i[e][r].type ? o.merge(i[e][r], [t.scaleService.getScaleDefaults(s), l]) : o.merge(i[e][r], l) + } else o._merger(e, i, n, a) + } + }) + }, o.where = function(t, e) { + if (o.isArray(t) && Array.prototype.filter) return t.filter(e); + var i = []; + return o.each(t, function(t) { + e(t) && i.push(t) + }), i + }, o.findIndex = Array.prototype.findIndex ? function(t, e, i) { + return t.findIndex(e, i) + } : function(t, e, i) { + i = void 0 === i ? t : i; + for (var n = 0, a = t.length; n < a; ++n) + if (e.call(i, t[n], n, t)) return n; + return -1 + }, o.findNextWhere = function(t, e, i) { + o.isNullOrUndef(i) && (i = -1); + for (var n = i + 1; n < t.length; n++) { + var a = t[n]; + if (e(a)) return a + } + }, o.findPreviousWhere = function(t, e, i) { + o.isNullOrUndef(i) && (i = t.length); + for (var n = i - 1; n >= 0; n--) { + var a = t[n]; + if (e(a)) return a + } + }, o.isNumber = function(t) { + return !isNaN(parseFloat(t)) && isFinite(t) + }, o.almostEquals = function(t, e, i) { + return Math.abs(t - e) < i + }, o.almostWhole = function(t, e) { + var i = Math.round(t); + return i - e < t && i + e > t + }, o.max = function(t) { + return t.reduce(function(t, e) { + return isNaN(e) ? t : Math.max(t, e) + }, Number.NEGATIVE_INFINITY) + }, o.min = function(t) { + return t.reduce(function(t, e) { + return isNaN(e) ? t : Math.min(t, e) + }, Number.POSITIVE_INFINITY) + }, o.sign = Math.sign ? function(t) { + return Math.sign(t) + } : function(t) { + return 0 === (t = +t) || isNaN(t) ? t : t > 0 ? 1 : -1 + }, o.log10 = Math.log10 ? function(t) { + return Math.log10(t) + } : function(t) { + var e = Math.log(t) * Math.LOG10E, + i = Math.round(e); + return t === Math.pow(10, i) ? i : e + }, o.toRadians = function(t) { + return t * (Math.PI / 180) + }, o.toDegrees = function(t) { + return t * (180 / Math.PI) + }, o.getAngleFromPoint = function(t, e) { + var i = e.x - t.x, + n = e.y - t.y, + a = Math.sqrt(i * i + n * n), + o = Math.atan2(n, i); + return o < -.5 * Math.PI && (o += 2 * Math.PI), { + angle: o, + distance: a + } + }, o.distanceBetweenPoints = function(t, e) { + return Math.sqrt(Math.pow(e.x - t.x, 2) + Math.pow(e.y - t.y, 2)) + }, o.aliasPixel = function(t) { + return t % 2 == 0 ? 0 : .5 + }, o.splineCurve = function(t, e, i, n) { + var a = t.skip ? e : t, + o = e, + r = i.skip ? e : i, + s = Math.sqrt(Math.pow(o.x - a.x, 2) + Math.pow(o.y - a.y, 2)), + l = Math.sqrt(Math.pow(r.x - o.x, 2) + Math.pow(r.y - o.y, 2)), + u = s / (s + l), + d = l / (s + l), + c = n * (u = isNaN(u) ? 0 : u), + h = n * (d = isNaN(d) ? 0 : d); + return { + previous: { + x: o.x - c * (r.x - a.x), + y: o.y - c * (r.y - a.y) + }, + next: { + x: o.x + h * (r.x - a.x), + y: o.y + h * (r.y - a.y) + } + } + }, o.EPSILON = Number.EPSILON || 1e-14, o.splineCurveMonotone = function(t) { + var e, i, n, a, r, s, l, u, d, c = (t || []).map(function(t) { + return { + model: t._model, + deltaK: 0, + mK: 0 + } + }), + h = c.length; + for (e = 0; e < h; ++e) + if (!(n = c[e]).model.skip) { + if (i = e > 0 ? c[e - 1] : null, (a = e < h - 1 ? c[e + 1] : null) && !a.model.skip) { + var f = a.model.x - n.model.x; + n.deltaK = 0 !== f ? (a.model.y - n.model.y) / f : 0 + }!i || i.model.skip ? n.mK = n.deltaK : !a || a.model.skip ? n.mK = i.deltaK : this.sign(i.deltaK) !== this.sign(n.deltaK) ? n.mK = 0 : n.mK = (i.deltaK + n.deltaK) / 2 + } for (e = 0; e < h - 1; ++e) n = c[e], a = c[e + 1], n.model.skip || a.model.skip || (o.almostEquals(n.deltaK, 0, this.EPSILON) ? n.mK = a.mK = 0 : (r = n.mK / n.deltaK, s = a.mK / n.deltaK, (u = Math.pow(r, 2) + Math.pow(s, 2)) <= 9 || (l = 3 / Math.sqrt(u), n.mK = r * l * n.deltaK, a.mK = s * l * n.deltaK))); + for (e = 0; e < h; ++e)(n = c[e]).model.skip || (i = e > 0 ? c[e - 1] : null, a = e < h - 1 ? c[e + 1] : null, i && !i.model.skip && (d = (n.model.x - i.model.x) / 3, n.model.controlPointPreviousX = n.model.x - d, n.model.controlPointPreviousY = n.model.y - d * n.mK), a && !a.model.skip && (d = (a.model.x - n.model.x) / 3, n.model.controlPointNextX = n.model.x + d, n.model.controlPointNextY = n.model.y + d * n.mK)) + }, o.nextItem = function(t, e, i) { + return i ? e >= t.length - 1 ? t[0] : t[e + 1] : e >= t.length - 1 ? t[t.length - 1] : t[e + 1] + }, o.previousItem = function(t, e, i) { + return i ? e <= 0 ? t[t.length - 1] : t[e - 1] : e <= 0 ? t[0] : t[e - 1] + }, o.niceNum = function(t, e) { + var i = Math.floor(o.log10(t)), + n = t / Math.pow(10, i); + return (e ? n < 1.5 ? 1 : n < 3 ? 2 : n < 7 ? 5 : 10 : n <= 1 ? 1 : n <= 2 ? 2 : n <= 5 ? 5 : 10) * Math.pow(10, i) + }, o.requestAnimFrame = "undefined" == typeof window ? function(t) { + t() + } : window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || function(t) { + return window.setTimeout(t, 1e3 / 60) + }, o.getRelativePosition = function(t, e) { + var i, n, a = t.originalEvent || t, + r = t.currentTarget || t.srcElement, + s = r.getBoundingClientRect(), + l = a.touches; + l && l.length > 0 ? (i = l[0].clientX, n = l[0].clientY) : (i = a.clientX, n = a.clientY); + var u = parseFloat(o.getStyle(r, "padding-left")), + d = parseFloat(o.getStyle(r, "padding-top")), + c = parseFloat(o.getStyle(r, "padding-right")), + h = parseFloat(o.getStyle(r, "padding-bottom")), + f = s.right - s.left - u - c, + g = s.bottom - s.top - d - h; + return { + x: i = Math.round((i - s.left - u) / f * r.width / e.currentDevicePixelRatio), + y: n = Math.round((n - s.top - d) / g * r.height / e.currentDevicePixelRatio) + } + }, o.getConstraintWidth = function(t) { + return r(t, "max-width", "clientWidth") + }, o.getConstraintHeight = function(t) { + return r(t, "max-height", "clientHeight") + }, o.getMaximumWidth = function(t) { + var e = t.parentNode; + if (!e) return t.clientWidth; + var i = parseInt(o.getStyle(e, "padding-left"), 10), + n = parseInt(o.getStyle(e, "padding-right"), 10), + a = e.clientWidth - i - n, + r = o.getConstraintWidth(t); + return isNaN(r) ? a : Math.min(a, r) + }, o.getMaximumHeight = function(t) { + var e = t.parentNode; + if (!e) return t.clientHeight; + var i = parseInt(o.getStyle(e, "padding-top"), 10), + n = parseInt(o.getStyle(e, "padding-bottom"), 10), + a = e.clientHeight - i - n, + r = o.getConstraintHeight(t); + return isNaN(r) ? a : Math.min(a, r) + }, o.getStyle = function(t, e) { + return t.currentStyle ? t.currentStyle[e] : document.defaultView.getComputedStyle(t, null).getPropertyValue(e) + }, o.retinaScale = function(t, e) { + var i = t.currentDevicePixelRatio = e || window.devicePixelRatio || 1; + if (1 !== i) { + var n = t.canvas, + a = t.height, + o = t.width; + n.height = a * i, n.width = o * i, t.ctx.scale(i, i), n.style.height || n.style.width || (n.style.height = a + "px", n.style.width = o + "px") + } + }, o.fontString = function(t, e, i) { + return e + " " + t + "px " + i + }, o.longestText = function(t, e, i, n) { + var a = (n = n || {}).data = n.data || {}, + r = n.garbageCollect = n.garbageCollect || []; + n.font !== e && (a = n.data = {}, r = n.garbageCollect = [], n.font = e), t.font = e; + var s = 0; + o.each(i, function(e) { + null != e && !0 !== o.isArray(e) ? s = o.measureText(t, a, r, s, e) : o.isArray(e) && o.each(e, function(e) { + null == e || o.isArray(e) || (s = o.measureText(t, a, r, s, e)) + }) + }); + var l = r.length / 2; + if (l > i.length) { + for (var u = 0; u < l; u++) delete a[r[u]]; + r.splice(0, l) + } + return s + }, o.measureText = function(t, e, i, n, a) { + var o = e[a]; + return o || (o = e[a] = t.measureText(a).width, i.push(a)), o > n && (n = o), n + }, o.numberOfLabelLines = function(t) { + var e = 1; + return o.each(t, function(t) { + o.isArray(t) && t.length > e && (e = t.length) + }), e + }, o.color = n ? function(t) { + return t instanceof CanvasGradient && (t = a.global.defaultColor), n(t) + } : function(t) { + return console.error("Color.js not found!"), t + }, o.getHoverColor = function(t) { + return t instanceof CanvasPattern ? t : o.color(t).saturate(.5).darken(.1).rgbString() + } + } + }, { + 25: 25, + 3: 3, + 45: 45 + }], + 28: [function(t, e, i) { + "use strict"; + var n = t(45); + + function a(t, e) { + return t.native ? { + x: t.x, + y: t.y + } : n.getRelativePosition(t, e) + } + + function o(t, e) { + var i, n, a, o, r; + for (n = 0, o = t.data.datasets.length; n < o; ++n) + if (t.isDatasetVisible(n)) + for (a = 0, r = (i = t.getDatasetMeta(n)).data.length; a < r; ++a) { + var s = i.data[a]; + s._view.skip || e(s) + } + } + + function r(t, e) { + var i = []; + return o(t, function(t) { + t.inRange(e.x, e.y) && i.push(t) + }), i + } + + function s(t, e, i, n) { + var a = Number.POSITIVE_INFINITY, + r = []; + return o(t, function(t) { + if (!i || t.inRange(e.x, e.y)) { + var o = t.getCenterPoint(), + s = n(e, o); + s < a ? (r = [t], a = s) : s === a && r.push(t) + } + }), r + } + + function l(t) { + var e = -1 !== t.indexOf("x"), + i = -1 !== t.indexOf("y"); + return function(t, n) { + var a = e ? Math.abs(t.x - n.x) : 0, + o = i ? Math.abs(t.y - n.y) : 0; + return Math.sqrt(Math.pow(a, 2) + Math.pow(o, 2)) + } + } + + function u(t, e, i) { + var n = a(e, t); + i.axis = i.axis || "x"; + var o = l(i.axis), + u = i.intersect ? r(t, n) : s(t, n, !1, o), + d = []; + return u.length ? (t.data.datasets.forEach(function(e, i) { + if (t.isDatasetVisible(i)) { + var n = t.getDatasetMeta(i).data[u[0]._index]; + n && !n._view.skip && d.push(n) + } + }), d) : [] + } + e.exports = { + modes: { + single: function(t, e) { + var i = a(e, t), + n = []; + return o(t, function(t) { + if (t.inRange(i.x, i.y)) return n.push(t), n + }), n.slice(0, 1) + }, + label: u, + index: u, + dataset: function(t, e, i) { + var n = a(e, t); + i.axis = i.axis || "xy"; + var o = l(i.axis), + u = i.intersect ? r(t, n) : s(t, n, !1, o); + return u.length > 0 && (u = t.getDatasetMeta(u[0]._datasetIndex).data), u + }, + "x-axis": function(t, e) { + return u(t, e, { + intersect: !1 + }) + }, + point: function(t, e) { + return r(t, a(e, t)) + }, + nearest: function(t, e, i) { + var n = a(e, t); + i.axis = i.axis || "xy"; + var o = l(i.axis), + r = s(t, n, i.intersect, o); + return r.length > 1 && r.sort(function(t, e) { + var i = t.getArea() - e.getArea(); + return 0 === i && (i = t._datasetIndex - e._datasetIndex), i + }), r.slice(0, 1) + }, + x: function(t, e, i) { + var n = a(e, t), + r = [], + s = !1; + return o(t, function(t) { + t.inXRange(n.x) && r.push(t), t.inRange(n.x, n.y) && (s = !0) + }), i.intersect && !s && (r = []), r + }, + y: function(t, e, i) { + var n = a(e, t), + r = [], + s = !1; + return o(t, function(t) { + t.inYRange(n.y) && r.push(t), t.inRange(n.x, n.y) && (s = !0) + }), i.intersect && !s && (r = []), r + } + } + } + }, { + 45: 45 + }], + 29: [function(t, e, i) { + "use strict"; + t(25)._set("global", { + responsive: !0, + responsiveAnimationDuration: 0, + maintainAspectRatio: !0, + events: ["mousemove", "mouseout", "click", "touchstart", "touchmove"], + hover: { + onHover: null, + mode: "nearest", + intersect: !0, + animationDuration: 400 + }, + onClick: function(t, e) { + var day = e[0]._model.label; + window.location.href = window.location.href + '?panelType=details&day=' + day; + }, + defaultColor: "rgba(0,0,0,0.1)", + defaultFontColor: "#666", + defaultFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", + defaultFontSize: 12, + defaultFontStyle: "normal", + showLines: !0, + elements: {}, + layout: { + padding: { + top: 0, + right: 0, + bottom: 0, + left: 0 + } + } + }), e.exports = function() { + var t = function(t, e) { + return this.construct(t, e), this + }; + return t.Chart = t, t + } + }, { + 25: 25 + }], + 30: [function(t, e, i) { + "use strict"; + var n = t(45); + + function a(t, e) { + return n.where(t, function(t) { + return t.position === e + }) + } + + function o(t, e) { + t.forEach(function(t, e) { + return t._tmpIndex_ = e, t + }), t.sort(function(t, i) { + var n = e ? i : t, + a = e ? t : i; + return n.weight === a.weight ? n._tmpIndex_ - a._tmpIndex_ : n.weight - a.weight + }), t.forEach(function(t) { + delete t._tmpIndex_ + }) + } + e.exports = { + defaults: {}, + addBox: function(t, e) { + t.boxes || (t.boxes = []), e.fullWidth = e.fullWidth || !1, e.position = e.position || "top", e.weight = e.weight || 0, t.boxes.push(e) + }, + removeBox: function(t, e) { + var i = t.boxes ? t.boxes.indexOf(e) : -1; - 1 !== i && t.boxes.splice(i, 1) + }, + configure: function(t, e, i) { + for (var n, a = ["fullWidth", "position", "weight"], o = a.length, r = 0; r < o; ++r) n = a[r], i.hasOwnProperty(n) && (e[n] = i[n]) + }, + update: function(t, e, i) { + if (t) { + var r = t.options.layout || {}, + s = n.options.toPadding(r.padding), + l = s.left, + u = s.right, + d = s.top, + c = s.bottom, + h = a(t.boxes, "left"), + f = a(t.boxes, "right"), + g = a(t.boxes, "top"), + p = a(t.boxes, "bottom"), + m = a(t.boxes, "chartArea"); + o(h, !0), o(f, !1), o(g, !0), o(p, !1); + var v = e - l - u, + b = i - d - c, + x = b / 2, + y = (e - v / 2) / (h.length + f.length), + k = (i - x) / (g.length + p.length), + M = v, + w = b, + S = []; + n.each(h.concat(f, g, p), function(t) { + var e, i = t.isHorizontal(); + i ? (e = t.update(t.fullWidth ? v : M, k), w -= e.height) : (e = t.update(y, w), M -= e.width), S.push({ + horizontal: i, + minSize: e, + box: t + }) + }); + var C = 0, + _ = 0, + D = 0, + I = 0; + n.each(g.concat(p), function(t) { + if (t.getPadding) { + var e = t.getPadding(); + C = Math.max(C, e.left), _ = Math.max(_, e.right) + } + }), n.each(h.concat(f), function(t) { + if (t.getPadding) { + var e = t.getPadding(); + D = Math.max(D, e.top), I = Math.max(I, e.bottom) + } + }); + var P = l, + A = u, + T = d, + F = c; + n.each(h.concat(f), N), n.each(h, function(t) { + P += t.width + }), n.each(f, function(t) { + A += t.width + }), n.each(g.concat(p), N), n.each(g, function(t) { + T += t.height + }), n.each(p, function(t) { + F += t.height + }), n.each(h.concat(f), function(t) { + var e = n.findNextWhere(S, function(e) { + return e.box === t + }), + i = { + left: 0, + right: 0, + top: T, + bottom: F + }; + e && t.update(e.minSize.width, w, i) + }), P = l, A = u, T = d, F = c, n.each(h, function(t) { + P += t.width + }), n.each(f, function(t) { + A += t.width + }), n.each(g, function(t) { + T += t.height + }), n.each(p, function(t) { + F += t.height + }); + var O = Math.max(C - P, 0); + P += O, A += Math.max(_ - A, 0); + var R = Math.max(D - T, 0); + T += R, F += Math.max(I - F, 0); + var L = i - T - F, + z = e - P - A; + z === M && L === w || (n.each(h, function(t) { + t.height = L + }), n.each(f, function(t) { + t.height = L + }), n.each(g, function(t) { + t.fullWidth || (t.width = z) + }), n.each(p, function(t) { + t.fullWidth || (t.width = z) + }), w = L, M = z); + var B = l + O, + W = d + R; + n.each(h.concat(g), V), B += M, W += w, n.each(f, V), n.each(p, V), t.chartArea = { + left: P, + top: T, + right: P + M, + bottom: T + w + }, n.each(m, function(e) { + e.left = t.chartArea.left, e.top = t.chartArea.top, e.right = t.chartArea.right, e.bottom = t.chartArea.bottom, e.update(M, w) + }) + } + + function N(t) { + var e = n.findNextWhere(S, function(e) { + return e.box === t + }); + if (e) + if (t.isHorizontal()) { + var i = { + left: Math.max(P, C), + right: Math.max(A, _), + top: 0, + bottom: 0 + }; + t.update(t.fullWidth ? v : M, b / 2, i) + } else t.update(e.minSize.width, w) + } + + function V(t) { + t.isHorizontal() ? (t.left = t.fullWidth ? l : P, t.right = t.fullWidth ? e - u : P + M, t.top = W, t.bottom = W + t.height, W = t.bottom) : (t.left = B, t.right = B + t.width, t.top = T, t.bottom = T + w, B = t.right) + } + } + } + }, { + 45: 45 + }], + 31: [function(t, e, i) { + "use strict"; + var n = t(25), + a = t(45); + n._set("global", { + plugins: {} + }), e.exports = { + _plugins: [], + _cacheId: 0, + register: function(t) { + var e = this._plugins; + [].concat(t).forEach(function(t) { + -1 === e.indexOf(t) && e.push(t) + }), this._cacheId++ + }, + unregister: function(t) { + var e = this._plugins; + [].concat(t).forEach(function(t) { + var i = e.indexOf(t); - 1 !== i && e.splice(i, 1) + }), this._cacheId++ + }, + clear: function() { + this._plugins = [], this._cacheId++ + }, + count: function() { + return this._plugins.length + }, + getAll: function() { + return this._plugins + }, + notify: function(t, e, i) { + var n, a, o, r, s, l = this.descriptors(t), + u = l.length; + for (n = 0; n < u; ++n) + if ("function" == typeof(s = (o = (a = l[n]).plugin)[e]) && ((r = [t].concat(i || [])).push(a.options), !1 === s.apply(o, r))) return !1; + return !0 + }, + descriptors: function(t) { + var e = t.$plugins || (t.$plugins = {}); + if (e.id === this._cacheId) return e.descriptors; + var i = [], + o = [], + r = t && t.config || {}, + s = r.options && r.options.plugins || {}; + return this._plugins.concat(r.plugins || []).forEach(function(t) { + if (-1 === i.indexOf(t)) { + var e = t.id, + r = s[e]; + !1 !== r && (!0 === r && (r = a.clone(n.global.plugins[e])), i.push(t), o.push({ + plugin: t, + options: r || {} + })) + } + }), e.descriptors = o, e.id = this._cacheId, o + }, + _invalidate: function(t) { + delete t.$plugins + } + } + }, { + 25: 25, + 45: 45 + }], + 32: [function(t, e, i) { + "use strict"; + var n = t(25), + a = t(26), + o = t(45), + r = t(34); + + function s(t) { + var e, i, n = []; + for (e = 0, i = t.length; e < i; ++e) n.push(t[e].label); + return n + } + + function l(t, e, i) { + var n = t.getPixelForTick(e); + return i && (n -= 0 === e ? (t.getPixelForTick(1) - n) / 2 : (n - t.getPixelForTick(e - 1)) / 2), n + } + n._set("scale", { + display: !0, + position: "left", + offset: !1, + gridLines: { + display: !0, + color: "rgba(0, 0, 0, 0.1)", + lineWidth: 1, + drawBorder: !0, + drawOnChartArea: !0, + drawTicks: !0, + tickMarkLength: 10, + zeroLineWidth: 1, + zeroLineColor: "rgba(0,0,0,0.25)", + zeroLineBorderDash: [], + zeroLineBorderDashOffset: 0, + offsetGridLines: !1, + borderDash: [], + borderDashOffset: 0 + }, + scaleLabel: { + display: !1, + labelString: "", + lineHeight: 1.2, + padding: { + top: 4, + bottom: 4 + } + }, + ticks: { + beginAtZero: !1, + minRotation: 0, + maxRotation: 50, + mirror: !1, + padding: 0, + reverse: !1, + display: !0, + autoSkip: !0, + autoSkipPadding: 0, + labelOffset: 0, + callback: r.formatters.values, + minor: {}, + major: {} + } + }), e.exports = function(t) { + function e(t, e, i) { + return o.isArray(e) ? o.longestText(t, i, e) : t.measureText(e).width + } + + function i(t) { + var e = o.valueOrDefault, + i = n.global, + a = e(t.fontSize, i.defaultFontSize), + r = e(t.fontStyle, i.defaultFontStyle), + s = e(t.fontFamily, i.defaultFontFamily); + return { + size: a, + style: r, + family: s, + font: o.fontString(a, r, s) + } + } + + function r(t) { + return o.options.toLineHeight(o.valueOrDefault(t.lineHeight, 1.2), o.valueOrDefault(t.fontSize, n.global.defaultFontSize)) + } + t.Scale = a.extend({ + getPadding: function() { + return { + left: this.paddingLeft || 0, + top: this.paddingTop || 0, + right: this.paddingRight || 0, + bottom: this.paddingBottom || 0 + } + }, + getTicks: function() { + return this._ticks + }, + mergeTicksOptions: function() { + var t = this.options.ticks; + for (var e in !1 === t.minor && (t.minor = { + display: !1 + }), !1 === t.major && (t.major = { + display: !1 + }), t) "major" !== e && "minor" !== e && (void 0 === t.minor[e] && (t.minor[e] = t[e]), void 0 === t.major[e] && (t.major[e] = t[e])) + }, + beforeUpdate: function() { + o.callback(this.options.beforeUpdate, [this]) + }, + update: function(t, e, i) { + var n, a, r, s, l, u, d = this; + for (d.beforeUpdate(), d.maxWidth = t, d.maxHeight = e, d.margins = o.extend({ + left: 0, + right: 0, + top: 0, + bottom: 0 + }, i), d.longestTextCache = d.longestTextCache || {}, d.beforeSetDimensions(), d.setDimensions(), d.afterSetDimensions(), d.beforeDataLimits(), d.determineDataLimits(), d.afterDataLimits(), d.beforeBuildTicks(), l = d.buildTicks() || [], d.afterBuildTicks(), d.beforeTickToLabelConversion(), r = d.convertTicksToLabels(l) || d.ticks, d.afterTickToLabelConversion(), d.ticks = r, n = 0, a = r.length; n < a; ++n) s = r[n], (u = l[n]) ? u.label = s : l.push(u = { + label: s, + major: !1 + }); + return d._ticks = l, d.beforeCalculateTickRotation(), d.calculateTickRotation(), d.afterCalculateTickRotation(), d.beforeFit(), d.fit(), d.afterFit(), d.afterUpdate(), d.minSize + }, + afterUpdate: function() { + o.callback(this.options.afterUpdate, [this]) + }, + beforeSetDimensions: function() { + o.callback(this.options.beforeSetDimensions, [this]) + }, + setDimensions: function() { + var t = this; + t.isHorizontal() ? (t.width = t.maxWidth, t.left = 0, t.right = t.width) : (t.height = t.maxHeight, t.top = 0, t.bottom = t.height), t.paddingLeft = 0, t.paddingTop = 0, t.paddingRight = 0, t.paddingBottom = 0 + }, + afterSetDimensions: function() { + o.callback(this.options.afterSetDimensions, [this]) + }, + beforeDataLimits: function() { + o.callback(this.options.beforeDataLimits, [this]) + }, + determineDataLimits: o.noop, + afterDataLimits: function() { + o.callback(this.options.afterDataLimits, [this]) + }, + beforeBuildTicks: function() { + o.callback(this.options.beforeBuildTicks, [this]) + }, + buildTicks: o.noop, + afterBuildTicks: function() { + o.callback(this.options.afterBuildTicks, [this]) + }, + beforeTickToLabelConversion: function() { + o.callback(this.options.beforeTickToLabelConversion, [this]) + }, + convertTicksToLabels: function() { + var t = this.options.ticks; + this.ticks = this.ticks.map(t.userCallback || t.callback, this) + }, + afterTickToLabelConversion: function() { + o.callback(this.options.afterTickToLabelConversion, [this]) + }, + beforeCalculateTickRotation: function() { + o.callback(this.options.beforeCalculateTickRotation, [this]) + }, + calculateTickRotation: function() { + var t = this, + e = t.ctx, + n = t.options.ticks, + a = s(t._ticks), + r = i(n); + e.font = r.font; + var l = n.minRotation || 0; + if (a.length && t.options.display && t.isHorizontal()) + for (var u, d = o.longestText(e, r.font, a, t.longestTextCache), c = d, h = t.getPixelForTick(1) - t.getPixelForTick(0) - 6; c > h && l < n.maxRotation;) { + var f = o.toRadians(l); + if (u = Math.cos(f), Math.sin(f) * d > t.maxHeight) { + l--; + break + } + l++, c = u * d + } + t.labelRotation = l + }, + afterCalculateTickRotation: function() { + o.callback(this.options.afterCalculateTickRotation, [this]) + }, + beforeFit: function() { + o.callback(this.options.beforeFit, [this]) + }, + fit: function() { + var t = this, + n = t.minSize = { + width: 0, + height: 0 + }, + a = s(t._ticks), + l = t.options, + u = l.ticks, + d = l.scaleLabel, + c = l.gridLines, + h = l.display, + f = t.isHorizontal(), + g = i(u), + p = l.gridLines.tickMarkLength; + if (n.width = f ? t.isFullWidth() ? t.maxWidth - t.margins.left - t.margins.right : t.maxWidth : h && c.drawTicks ? p : 0, n.height = f ? h && c.drawTicks ? p : 0 : t.maxHeight, d.display && h) { + var m = r(d) + o.options.toPadding(d.padding).height; + f ? n.height += m : n.width += m + } + if (u.display && h) { + var v = o.longestText(t.ctx, g.font, a, t.longestTextCache), + b = o.numberOfLabelLines(a), + x = .5 * g.size, + y = t.options.ticks.padding; + if (f) { + t.longestLabelWidth = v; + var k = o.toRadians(t.labelRotation), + M = Math.cos(k), + w = Math.sin(k) * v + g.size * b + x * (b - 1) + x; + n.height = Math.min(t.maxHeight, n.height + w + y), t.ctx.font = g.font; + var S = e(t.ctx, a[0], g.font), + C = e(t.ctx, a[a.length - 1], g.font); + 0 !== t.labelRotation ? (t.paddingLeft = "bottom" === l.position ? M * S + 3 : M * x + 3, t.paddingRight = "bottom" === l.position ? M * x + 3 : M * C + 3) : (t.paddingLeft = S / 2 + 3, t.paddingRight = C / 2 + 3) + } else u.mirror ? v = 0 : v += y + x, n.width = Math.min(t.maxWidth, n.width + v), t.paddingTop = g.size / 2, t.paddingBottom = g.size / 2 + } + t.handleMargins(), t.width = n.width, t.height = n.height + }, + handleMargins: function() { + var t = this; + t.margins && (t.paddingLeft = Math.max(t.paddingLeft - t.margins.left, 0), t.paddingTop = Math.max(t.paddingTop - t.margins.top, 0), t.paddingRight = Math.max(t.paddingRight - t.margins.right, 0), t.paddingBottom = Math.max(t.paddingBottom - t.margins.bottom, 0)) + }, + afterFit: function() { + o.callback(this.options.afterFit, [this]) + }, + isHorizontal: function() { + return "top" === this.options.position || "bottom" === this.options.position + }, + isFullWidth: function() { + return this.options.fullWidth + }, + getRightValue: function(t) { + if (o.isNullOrUndef(t)) return NaN; + if ("number" == typeof t && !isFinite(t)) return NaN; + if (t) + if (this.isHorizontal()) { + if (void 0 !== t.x) return this.getRightValue(t.x) + } else if (void 0 !== t.y) return this.getRightValue(t.y); + return t + }, + getLabelForIndex: o.noop, + getPixelForValue: o.noop, + getValueForPixel: o.noop, + getPixelForTick: function(t) { + var e = this, + i = e.options.offset; + if (e.isHorizontal()) { + var n = (e.width - (e.paddingLeft + e.paddingRight)) / Math.max(e._ticks.length - (i ? 0 : 1), 1), + a = n * t + e.paddingLeft; + i && (a += n / 2); + var o = e.left + Math.round(a); + return o += e.isFullWidth() ? e.margins.left : 0 + } + var r = e.height - (e.paddingTop + e.paddingBottom); + return e.top + t * (r / (e._ticks.length - 1)) + }, + getPixelForDecimal: function(t) { + var e = this; + if (e.isHorizontal()) { + var i = (e.width - (e.paddingLeft + e.paddingRight)) * t + e.paddingLeft, + n = e.left + Math.round(i); + return n += e.isFullWidth() ? e.margins.left : 0 + } + return e.top + t * e.height + }, + getBasePixel: function() { + return this.getPixelForValue(this.getBaseValue()) + }, + getBaseValue: function() { + var t = this.min, + e = this.max; + return this.beginAtZero ? 0 : t < 0 && e < 0 ? e : t > 0 && e > 0 ? t : 0 + }, + _autoSkip: function(t) { + var e, i, n, a, r = this, + s = r.isHorizontal(), + l = r.options.ticks.minor, + u = t.length, + d = o.toRadians(r.labelRotation), + c = Math.cos(d), + h = r.longestLabelWidth * c, + f = []; + for (l.maxTicksLimit && (a = l.maxTicksLimit), s && (e = !1, (h + l.autoSkipPadding) * u > r.width - (r.paddingLeft + r.paddingRight) && (e = 1 + Math.floor((h + l.autoSkipPadding) * u / (r.width - (r.paddingLeft + r.paddingRight)))), a && u > a && (e = Math.max(e, Math.floor(u / a)))), i = 0; i < u; i++) n = t[i], (e > 1 && i % e > 0 || i % e == 0 && i + e >= u) && i !== u - 1 && delete n.label, f.push(n); + return f + }, + draw: function(t) { + var e = this, + a = e.options; + if (a.display) { + var s = e.ctx, + u = n.global, + d = a.ticks.minor, + c = a.ticks.major || d, + h = a.gridLines, + f = a.scaleLabel, + g = 0 !== e.labelRotation, + p = e.isHorizontal(), + m = d.autoSkip ? e._autoSkip(e.getTicks()) : e.getTicks(), + v = o.valueOrDefault(d.fontColor, u.defaultFontColor), + b = i(d), + x = o.valueOrDefault(c.fontColor, u.defaultFontColor), + y = i(c), + k = h.drawTicks ? h.tickMarkLength : 0, + M = o.valueOrDefault(f.fontColor, u.defaultFontColor), + w = i(f), + S = o.options.toPadding(f.padding), + C = o.toRadians(e.labelRotation), + _ = [], + D = e.options.gridLines.lineWidth, + I = "right" === a.position ? e.right : e.right - D - k, + P = "right" === a.position ? e.right + k : e.right, + A = "bottom" === a.position ? e.top + D : e.bottom - k - D, + T = "bottom" === a.position ? e.top + D + k : e.bottom + D; + if (o.each(m, function(i, n) { + if (!o.isNullOrUndef(i.label)) { + var r, s, c, f, v, b, x, y, M, w, S, F, O, R, L = i.label; + n === e.zeroLineIndex && a.offset === h.offsetGridLines ? (r = h.zeroLineWidth, s = h.zeroLineColor, c = h.zeroLineBorderDash, f = h.zeroLineBorderDashOffset) : (r = o.valueAtIndexOrDefault(h.lineWidth, n), s = o.valueAtIndexOrDefault(h.color, n), c = o.valueOrDefault(h.borderDash, u.borderDash), f = o.valueOrDefault(h.borderDashOffset, u.borderDashOffset)); + var z = "middle", + B = "middle", + W = d.padding; + if (p) { + var N = k + W; + "bottom" === a.position ? (B = g ? "middle" : "top", z = g ? "right" : "center", R = e.top + N) : (B = g ? "middle" : "bottom", z = g ? "left" : "center", R = e.bottom - N); + var V = l(e, n, h.offsetGridLines && m.length > 1); + V < e.left && (s = "rgba(0,0,0,0)"), V += o.aliasPixel(r), O = e.getPixelForTick(n) + d.labelOffset, v = x = M = S = V, b = A, y = T, w = t.top, F = t.bottom + D + } else { + var E, H = "left" === a.position; + d.mirror ? (z = H ? "left" : "right", E = W) : (z = H ? "right" : "left", E = k + W), O = H ? e.right - E : e.left + E; + var j = l(e, n, h.offsetGridLines && m.length > 1); + j < e.top && (s = "rgba(0,0,0,0)"), j += o.aliasPixel(r), R = e.getPixelForTick(n) + d.labelOffset, v = I, x = P, M = t.left, S = t.right + D, b = y = w = F = j + } + _.push({ + tx1: v, + ty1: b, + tx2: x, + ty2: y, + x1: M, + y1: w, + x2: S, + y2: F, + labelX: O, + labelY: R, + glWidth: r, + glColor: s, + glBorderDash: c, + glBorderDashOffset: f, + rotation: -1 * C, + label: L, + major: i.major, + textBaseline: B, + textAlign: z + }) + } + }), o.each(_, function(t) { + if (h.display && (s.save(), s.lineWidth = t.glWidth, s.strokeStyle = t.glColor, s.setLineDash && (s.setLineDash(t.glBorderDash), s.lineDashOffset = t.glBorderDashOffset), s.beginPath(), h.drawTicks && (s.moveTo(t.tx1, t.ty1), s.lineTo(t.tx2, t.ty2)), h.drawOnChartArea && (s.moveTo(t.x1, t.y1), s.lineTo(t.x2, t.y2)), s.stroke(), s.restore()), d.display) { + s.save(), s.translate(t.labelX, t.labelY), s.rotate(t.rotation), s.font = t.major ? y.font : b.font, s.fillStyle = t.major ? x : v, s.textBaseline = t.textBaseline, s.textAlign = t.textAlign; + var i = t.label; + if (o.isArray(i)) + for (var n = i.length, a = 1.5 * b.size, r = e.isHorizontal() ? 0 : -a * (n - 1) / 2, l = 0; l < n; ++l) s.fillText("" + i[l], 0, r), r += a; + else s.fillText(i, 0, 0); + s.restore() + } + }), f.display) { + var F, O, R = 0, + L = r(f) / 2; + if (p) F = e.left + (e.right - e.left) / 2, O = "bottom" === a.position ? e.bottom - L - S.bottom : e.top + L + S.top; + else { + var z = "left" === a.position; + F = z ? e.left + L + S.top : e.right - L - S.top, O = e.top + (e.bottom - e.top) / 2, R = z ? -.5 * Math.PI : .5 * Math.PI + } + s.save(), s.translate(F, O), s.rotate(R), s.textAlign = "center", s.textBaseline = "middle", s.fillStyle = M, s.font = w.font, s.fillText(f.labelString, 0, 0), s.restore() + } + if (h.drawBorder) { + s.lineWidth = o.valueAtIndexOrDefault(h.lineWidth, 0), s.strokeStyle = o.valueAtIndexOrDefault(h.color, 0); + var B = e.left, + W = e.right + D, + N = e.top, + V = e.bottom + D, + E = o.aliasPixel(s.lineWidth); + p ? (N = V = "top" === a.position ? e.bottom : e.top, N += E, V += E) : (B = W = "left" === a.position ? e.right : e.left, B += E, W += E), s.beginPath(), s.moveTo(B, N), s.lineTo(W, V), s.stroke() + } + } + } + }) + } + }, { + 25: 25, + 26: 26, + 34: 34, + 45: 45 + }], + 33: [function(t, e, i) { + "use strict"; + var n = t(25), + a = t(45), + o = t(30); + e.exports = function(t) { + t.scaleService = { + constructors: {}, + defaults: {}, + registerScaleType: function(t, e, i) { + this.constructors[t] = e, this.defaults[t] = a.clone(i) + }, + getScaleConstructor: function(t) { + return this.constructors.hasOwnProperty(t) ? this.constructors[t] : void 0 + }, + getScaleDefaults: function(t) { + return this.defaults.hasOwnProperty(t) ? a.merge({}, [n.scale, this.defaults[t]]) : {} + }, + updateScaleDefaults: function(t, e) { + this.defaults.hasOwnProperty(t) && (this.defaults[t] = a.extend(this.defaults[t], e)) + }, + addScalesToLayout: function(t) { + a.each(t.scales, function(e) { + e.fullWidth = e.options.fullWidth, e.position = e.options.position, e.weight = e.options.weight, o.addBox(t, e) + }) + } + } + } + }, { + 25: 25, + 30: 30, + 45: 45 + }], + 34: [function(t, e, i) { + "use strict"; + var n = t(45); + e.exports = { + formatters: { + values: function(t) { + return n.isArray(t) ? t : "" + t + }, + linear: function(t, e, i) { + var a = i.length > 3 ? i[2] - i[1] : i[1] - i[0]; + Math.abs(a) > 1 && t !== Math.floor(t) && (a = t - Math.floor(t)); + var o = n.log10(Math.abs(a)), + r = ""; + if (0 !== t) { + var s = -1 * Math.floor(o); + s = Math.max(Math.min(s, 20), 0), r = t.toFixed(s) + } else r = "0"; + return r + }, + logarithmic: function(t, e, i) { + var a = t / Math.pow(10, Math.floor(n.log10(t))); + return 0 === t ? "0" : 1 === a || 2 === a || 5 === a || 0 === e || e === i.length - 1 ? t.toExponential() : "" + } + } + } + }, { + 45: 45 + }], + 35: [function(t, e, i) { + "use strict"; + var n = t(25), + a = t(26), + o = t(45); + n._set("global", { + tooltips: { + enabled: !0, + custom: null, + mode: "nearest", + position: "average", + intersect: !0, + backgroundColor: "rgba(0,0,0,0.8)", + titleFontStyle: "bold", + titleSpacing: 2, + titleMarginBottom: 6, + titleFontColor: "#fff", + titleAlign: "left", + bodySpacing: 2, + bodyFontColor: "#fff", + bodyAlign: "left", + footerFontStyle: "bold", + footerSpacing: 2, + footerMarginTop: 6, + footerFontColor: "#fff", + footerAlign: "left", + yPadding: 6, + xPadding: 6, + caretPadding: 2, + caretSize: 5, + cornerRadius: 6, + multiKeyBackground: "#fff", + displayColors: !0, + borderColor: "rgba(0,0,0,0)", + borderWidth: 0, + callbacks: { + beforeTitle: o.noop, + title: function(t, e) { + var i = "", + n = e.labels, + a = n ? n.length : 0; + if (t.length > 0) { + var o = t[0]; + o.xLabel ? i = o.xLabel : a > 0 && o.index < a && (i = n[o.index]) + } + return i + }, + afterTitle: o.noop, + beforeBody: o.noop, + beforeLabel: o.noop, + label: function(t, e) { + var i = e.datasets[t.datasetIndex].label || ""; + return i && (i += ": "), i += t.yLabel + }, + labelColor: function(t, e) { + var i = e.getDatasetMeta(t.datasetIndex).data[t.index]._view; + return { + borderColor: i.borderColor, + backgroundColor: i.backgroundColor + } + }, + labelTextColor: function() { + return this._options.bodyFontColor + }, + afterLabel: o.noop, + afterBody: o.noop, + beforeFooter: o.noop, + footer: o.noop, + afterFooter: o.noop + } + } + }), e.exports = function(t) { + function e(t, e) { + var i = o.color(t); + return i.alpha(e * i.alpha()).rgbaString() + } + + function i(t, e) { + return e && (o.isArray(e) ? Array.prototype.push.apply(t, e) : t.push(e)), t + } + + function r(t) { + var e = n.global, + i = o.valueOrDefault; + return { + xPadding: t.xPadding, + yPadding: t.yPadding, + xAlign: t.xAlign, + yAlign: t.yAlign, + bodyFontColor: t.bodyFontColor, + _bodyFontFamily: i(t.bodyFontFamily, e.defaultFontFamily), + _bodyFontStyle: i(t.bodyFontStyle, e.defaultFontStyle), + _bodyAlign: t.bodyAlign, + bodyFontSize: i(t.bodyFontSize, e.defaultFontSize), + bodySpacing: t.bodySpacing, + titleFontColor: t.titleFontColor, + _titleFontFamily: i(t.titleFontFamily, e.defaultFontFamily), + _titleFontStyle: i(t.titleFontStyle, e.defaultFontStyle), + titleFontSize: i(t.titleFontSize, e.defaultFontSize), + _titleAlign: t.titleAlign, + titleSpacing: t.titleSpacing, + titleMarginBottom: t.titleMarginBottom, + footerFontColor: t.footerFontColor, + _footerFontFamily: i(t.footerFontFamily, e.defaultFontFamily), + _footerFontStyle: i(t.footerFontStyle, e.defaultFontStyle), + footerFontSize: i(t.footerFontSize, e.defaultFontSize), + _footerAlign: t.footerAlign, + footerSpacing: t.footerSpacing, + footerMarginTop: t.footerMarginTop, + caretSize: t.caretSize, + cornerRadius: t.cornerRadius, + backgroundColor: t.backgroundColor, + opacity: 0, + legendColorBackground: t.multiKeyBackground, + displayColors: t.displayColors, + borderColor: t.borderColor, + borderWidth: t.borderWidth + } + } + t.Tooltip = a.extend({ + initialize: function() { + this._model = r(this._options), this._lastActive = [] + }, + getTitle: function() { + var t = this._options.callbacks, + e = t.beforeTitle.apply(this, arguments), + n = t.title.apply(this, arguments), + a = t.afterTitle.apply(this, arguments), + o = []; + return o = i(o = i(o = i(o, e), n), a) + }, + getBeforeBody: function() { + var t = this._options.callbacks.beforeBody.apply(this, arguments); + return o.isArray(t) ? t : void 0 !== t ? [t] : [] + }, + getBody: function(t, e) { + var n = this, + a = n._options.callbacks, + r = []; + return o.each(t, function(t) { + var o = { + before: [], + lines: [], + after: [] + }; + i(o.before, a.beforeLabel.call(n, t, e)), i(o.lines, a.label.call(n, t, e)), i(o.after, a.afterLabel.call(n, t, e)), r.push(o) + }), r + }, + getAfterBody: function() { + var t = this._options.callbacks.afterBody.apply(this, arguments); + return o.isArray(t) ? t : void 0 !== t ? [t] : [] + }, + getFooter: function() { + var t = this._options.callbacks, + e = t.beforeFooter.apply(this, arguments), + n = t.footer.apply(this, arguments), + a = t.afterFooter.apply(this, arguments), + o = []; + return o = i(o = i(o = i(o, e), n), a) + }, + update: function(e) { + var i, n, a, s, l, u, d, c, h, f, g, p, m, v, b, x, y, k, M, w, S = this, + C = S._options, + _ = S._model, + D = S._model = r(C), + I = S._active, + P = S._data, + A = { + xAlign: _.xAlign, + yAlign: _.yAlign + }, + T = { + x: _.x, + y: _.y + }, + F = { + width: _.width, + height: _.height + }, + O = { + x: _.caretX, + y: _.caretY + }; + if (I.length) { + D.opacity = 1; + var R = [], + L = []; + O = t.Tooltip.positioners[C.position].call(S, I, S._eventPosition); + var z = []; + for (i = 0, n = I.length; i < n; ++i) z.push((x = I[i], y = void 0, k = void 0, void 0, void 0, y = x._xScale, k = x._yScale || x._scale, M = x._index, w = x._datasetIndex, { + xLabel: y ? y.getLabelForIndex(M, w) : "", + yLabel: k ? k.getLabelForIndex(M, w) : "", + index: M, + datasetIndex: w, + x: x._model.x, + y: x._model.y + })); + C.filter && (z = z.filter(function(t) { + return C.filter(t, P) + })), C.itemSort && (z = z.sort(function(t, e) { + return C.itemSort(t, e, P) + })), o.each(z, function(t) { + R.push(C.callbacks.labelColor.call(S, t, S._chart)), L.push(C.callbacks.labelTextColor.call(S, t, S._chart)) + }), D.title = S.getTitle(z, P), D.beforeBody = S.getBeforeBody(z, P), D.body = S.getBody(z, P), D.afterBody = S.getAfterBody(z, P), D.footer = S.getFooter(z, P), D.x = Math.round(O.x), D.y = Math.round(O.y), D.caretPadding = C.caretPadding, D.labelColors = R, D.labelTextColors = L, D.dataPoints = z, A = function(t, e) { + var i, n, a, o, r, s = t._model, + l = t._chart, + u = t._chart.chartArea, + d = "center", + c = "center"; + s.y < e.height ? c = "top" : s.y > l.height - e.height && (c = "bottom"); + var h = (u.left + u.right) / 2, + f = (u.top + u.bottom) / 2; + "center" === c ? (i = function(t) { + return t <= h + }, n = function(t) { + return t > h + }) : (i = function(t) { + return t <= e.width / 2 + }, n = function(t) { + return t >= l.width - e.width / 2 + }), a = function(t) { + return t + e.width + s.caretSize + s.caretPadding > l.width + }, o = function(t) { + return t - e.width - s.caretSize - s.caretPadding < 0 + }, r = function(t) { + return t <= f ? "top" : "bottom" + }, i(s.x) ? (d = "left", a(s.x) && (d = "center", c = r(s.y))) : n(s.x) && (d = "right", o(s.x) && (d = "center", c = r(s.y))); + var g = t._options; + return { + xAlign: g.xAlign ? g.xAlign : d, + yAlign: g.yAlign ? g.yAlign : c + } + }(this, F = function(t, e) { + var i = t._chart.ctx, + n = 2 * e.yPadding, + a = 0, + r = e.body, + s = r.reduce(function(t, e) { + return t + e.before.length + e.lines.length + e.after.length + }, 0); + s += e.beforeBody.length + e.afterBody.length; + var l = e.title.length, + u = e.footer.length, + d = e.titleFontSize, + c = e.bodyFontSize, + h = e.footerFontSize; + n += l * d, n += l ? (l - 1) * e.titleSpacing : 0, n += l ? e.titleMarginBottom : 0, n += s * c, n += s ? (s - 1) * e.bodySpacing : 0, n += u ? e.footerMarginTop : 0, n += u * h, n += u ? (u - 1) * e.footerSpacing : 0; + var f = 0, + g = function(t) { + a = Math.max(a, i.measureText(t).width + f) + }; + return i.font = o.fontString(d, e._titleFontStyle, e._titleFontFamily), o.each(e.title, g), i.font = o.fontString(c, e._bodyFontStyle, e._bodyFontFamily), o.each(e.beforeBody.concat(e.afterBody), g), f = e.displayColors ? c + 2 : 0, o.each(r, function(t) { + o.each(t.before, g), o.each(t.lines, g), o.each(t.after, g) + }), f = 0, i.font = o.fontString(h, e._footerFontStyle, e._footerFontFamily), o.each(e.footer, g), { + width: a += 2 * e.xPadding, + height: n + } + }(this, D)), a = D, s = F, l = A, u = S._chart, d = a.x, c = a.y, h = a.caretSize, f = a.caretPadding, g = a.cornerRadius, p = l.xAlign, m = l.yAlign, v = h + f, b = g + f, "right" === p ? d -= s.width : "center" === p && ((d -= s.width / 2) + s.width > u.width && (d = u.width - s.width), d < 0 && (d = 0)), "top" === m ? c += v : c -= "bottom" === m ? s.height + v : s.height / 2, "center" === m ? "left" === p ? d += v : "right" === p && (d -= v) : "left" === p ? d -= b : "right" === p && (d += b), T = { + x: d, + y: c + } + } else D.opacity = 0; + return D.xAlign = A.xAlign, D.yAlign = A.yAlign, D.x = T.x, D.y = T.y, D.width = F.width, D.height = F.height, D.caretX = O.x, D.caretY = O.y, S._model = D, e && C.custom && C.custom.call(S, D), S + }, + drawCaret: function(t, e) { + var i = this._chart.ctx, + n = this._view, + a = this.getCaretPosition(t, e, n); + i.lineTo(a.x1, a.y1), i.lineTo(a.x2, a.y2), i.lineTo(a.x3, a.y3) + }, + getCaretPosition: function(t, e, i) { + var n, a, o, r, s, l, u = i.caretSize, + d = i.cornerRadius, + c = i.xAlign, + h = i.yAlign, + f = t.x, + g = t.y, + p = e.width, + m = e.height; + if ("center" === h) s = g + m / 2, "left" === c ? (a = (n = f) - u, o = n, r = s + u, l = s - u) : (a = (n = f + p) + u, o = n, r = s - u, l = s + u); + else if ("left" === c ? (n = (a = f + d + u) - u, o = a + u) : "right" === c ? (n = (a = f + p - d - u) - u, o = a + u) : (n = (a = i.caretX) - u, o = a + u), "top" === h) s = (r = g) - u, l = r; + else { + s = (r = g + m) + u, l = r; + var v = o; + o = n, n = v + } + return { + x1: n, + x2: a, + x3: o, + y1: r, + y2: s, + y3: l + } + }, + drawTitle: function(t, i, n, a) { + var r = i.title; + if (r.length) { + n.textAlign = i._titleAlign, n.textBaseline = "top"; + var s, l, u = i.titleFontSize, + d = i.titleSpacing; + for (n.fillStyle = e(i.titleFontColor, a), n.font = o.fontString(u, i._titleFontStyle, i._titleFontFamily), s = 0, l = r.length; s < l; ++s) n.fillText(r[s], t.x, t.y), t.y += u + d, s + 1 === r.length && (t.y += i.titleMarginBottom - d) + } + }, + drawBody: function(t, i, n, a) { + var r = i.bodyFontSize, + s = i.bodySpacing, + l = i.body; + n.textAlign = i._bodyAlign, n.textBaseline = "top", n.font = o.fontString(r, i._bodyFontStyle, i._bodyFontFamily); + var u = 0, + d = function(e) { + n.fillText(e, t.x + u, t.y), t.y += r + s + }; + n.fillStyle = e(i.bodyFontColor, a), o.each(i.beforeBody, d); + var c = i.displayColors; + u = c ? r + 2 : 0, o.each(l, function(s, l) { + var u = e(i.labelTextColors[l], a); + n.fillStyle = u, o.each(s.before, d), o.each(s.lines, function(o) { + c && (n.fillStyle = e(i.legendColorBackground, a), n.fillRect(t.x, t.y, r, r), n.lineWidth = 1, n.strokeStyle = e(i.labelColors[l].borderColor, a), n.strokeRect(t.x, t.y, r, r), n.fillStyle = e(i.labelColors[l].backgroundColor, a), n.fillRect(t.x + 1, t.y + 1, r - 2, r - 2), n.fillStyle = u), d(o) + }), o.each(s.after, d) + }), u = 0, o.each(i.afterBody, d), t.y -= s + }, + drawFooter: function(t, i, n, a) { + var r = i.footer; + r.length && (t.y += i.footerMarginTop, n.textAlign = i._footerAlign, n.textBaseline = "top", n.fillStyle = e(i.footerFontColor, a), n.font = o.fontString(i.footerFontSize, i._footerFontStyle, i._footerFontFamily), o.each(r, function(e) { + n.fillText(e, t.x, t.y), t.y += i.footerFontSize + i.footerSpacing + })) + }, + drawBackground: function(t, i, n, a, o) { + n.fillStyle = e(i.backgroundColor, o), n.strokeStyle = e(i.borderColor, o), n.lineWidth = i.borderWidth; + var r = i.xAlign, + s = i.yAlign, + l = t.x, + u = t.y, + d = a.width, + c = a.height, + h = i.cornerRadius; + n.beginPath(), n.moveTo(l + h, u), "top" === s && this.drawCaret(t, a), n.lineTo(l + d - h, u), n.quadraticCurveTo(l + d, u, l + d, u + h), "center" === s && "right" === r && this.drawCaret(t, a), n.lineTo(l + d, u + c - h), n.quadraticCurveTo(l + d, u + c, l + d - h, u + c), "bottom" === s && this.drawCaret(t, a), n.lineTo(l + h, u + c), n.quadraticCurveTo(l, u + c, l, u + c - h), "center" === s && "left" === r && this.drawCaret(t, a), n.lineTo(l, u + h), n.quadraticCurveTo(l, u, l + h, u), n.closePath(), n.fill(), i.borderWidth > 0 && n.stroke() + }, + draw: function() { + var t = this._chart.ctx, + e = this._view; + if (0 !== e.opacity) { + var i = { + width: e.width, + height: e.height + }, + n = { + x: e.x, + y: e.y + }, + a = Math.abs(e.opacity < .001) ? 0 : e.opacity, + o = e.title.length || e.beforeBody.length || e.body.length || e.afterBody.length || e.footer.length; + this._options.enabled && o && (this.drawBackground(n, e, t, i, a), n.x += e.xPadding, n.y += e.yPadding, this.drawTitle(n, e, t, a), this.drawBody(n, e, t, a), this.drawFooter(n, e, t, a)) + } + }, + handleEvent: function(t) { + var e, i = this, + n = i._options; + return i._lastActive = i._lastActive || [], "mouseout" === t.type ? i._active = [] : i._active = i._chart.getElementsAtEventForMode(t, n.mode, n), (e = !o.arrayEquals(i._active, i._lastActive)) && (i._lastActive = i._active, (n.enabled || n.custom) && (i._eventPosition = { + x: t.x, + y: t.y + }, i.update(!0), i.pivot())), e + } + }), t.Tooltip.positioners = { + average: function(t) { + if (!t.length) return !1; + var e, i, n = 0, + a = 0, + o = 0; + for (e = 0, i = t.length; e < i; ++e) { + var r = t[e]; + if (r && r.hasValue()) { + var s = r.tooltipPosition(); + n += s.x, a += s.y, ++o + } + } + return { + x: Math.round(n / o), + y: Math.round(a / o) + } + }, + nearest: function(t, e) { + var i, n, a, r = e.x, + s = e.y, + l = Number.POSITIVE_INFINITY; + for (i = 0, n = t.length; i < n; ++i) { + var u = t[i]; + if (u && u.hasValue()) { + var d = u.getCenterPoint(), + c = o.distanceBetweenPoints(e, d); + c < l && (l = c, a = u) + } + } + if (a) { + var h = a.tooltipPosition(); + r = h.x, s = h.y + } + return { + x: r, + y: s + } + } + } + } + }, { + 25: 25, + 26: 26, + 45: 45 + }], + 36: [function(t, e, i) { + "use strict"; + var n = t(25), + a = t(26), + o = t(45); + n._set("global", { + elements: { + arc: { + backgroundColor: n.global.defaultColor, + borderColor: "#fff", + borderWidth: 2 + } + } + }), e.exports = a.extend({ + inLabelRange: function(t) { + var e = this._view; + return !!e && Math.pow(t - e.x, 2) < Math.pow(e.radius + e.hoverRadius, 2) + }, + inRange: function(t, e) { + var i = this._view; + if (i) { + for (var n = o.getAngleFromPoint(i, { + x: t, + y: e + }), a = n.angle, r = n.distance, s = i.startAngle, l = i.endAngle; l < s;) l += 2 * Math.PI; + for (; a > l;) a -= 2 * Math.PI; + for (; a < s;) a += 2 * Math.PI; + var u = a >= s && a <= l, + d = r >= i.innerRadius && r <= i.outerRadius; + return u && d + } + return !1 + }, + getCenterPoint: function() { + var t = this._view, + e = (t.startAngle + t.endAngle) / 2, + i = (t.innerRadius + t.outerRadius) / 2; + return { + x: t.x + Math.cos(e) * i, + y: t.y + Math.sin(e) * i + } + }, + getArea: function() { + var t = this._view; + return Math.PI * ((t.endAngle - t.startAngle) / (2 * Math.PI)) * (Math.pow(t.outerRadius, 2) - Math.pow(t.innerRadius, 2)) + }, + tooltipPosition: function() { + var t = this._view, + e = t.startAngle + (t.endAngle - t.startAngle) / 2, + i = (t.outerRadius - t.innerRadius) / 2 + t.innerRadius; + return { + x: t.x + Math.cos(e) * i, + y: t.y + Math.sin(e) * i + } + }, + draw: function() { + var t = this._chart.ctx, + e = this._view, + i = e.startAngle, + n = e.endAngle; + t.beginPath(), t.arc(e.x, e.y, e.outerRadius, i, n), t.arc(e.x, e.y, e.innerRadius, n, i, !0), t.closePath(), t.strokeStyle = e.borderColor, t.lineWidth = e.borderWidth, t.fillStyle = e.backgroundColor, t.fill(), t.lineJoin = "bevel", e.borderWidth && t.stroke() + } + }) + }, { + 25: 25, + 26: 26, + 45: 45 + }], + 37: [function(t, e, i) { + "use strict"; + var n = t(25), + a = t(26), + o = t(45), + r = n.global; + n._set("global", { + elements: { + line: { + tension: .4, + backgroundColor: r.defaultColor, + borderWidth: 3, + borderColor: r.defaultColor, + borderCapStyle: "butt", + borderDash: [], + borderDashOffset: 0, + borderJoinStyle: "miter", + capBezierPoints: !0, + fill: !0 + } + } + }), e.exports = a.extend({ + draw: function() { + var t, e, i, n, a = this._view, + s = this._chart.ctx, + l = a.spanGaps, + u = this._children.slice(), + d = r.elements.line, + c = -1; + for (this._loop && u.length && u.push(u[0]), s.save(), s.lineCap = a.borderCapStyle || d.borderCapStyle, s.setLineDash && s.setLineDash(a.borderDash || d.borderDash), s.lineDashOffset = a.borderDashOffset || d.borderDashOffset, s.lineJoin = a.borderJoinStyle || d.borderJoinStyle, s.lineWidth = a.borderWidth || d.borderWidth, s.strokeStyle = a.borderColor || r.defaultColor, s.beginPath(), c = -1, t = 0; t < u.length; ++t) e = u[t], i = o.previousItem(u, t), n = e._view, 0 === t ? n.skip || (s.moveTo(n.x, n.y), c = t) : (i = -1 === c ? i : u[c], n.skip || (c !== t - 1 && !l || -1 === c ? s.moveTo(n.x, n.y) : o.canvas.lineTo(s, i._view, e._view), c = t)); + s.stroke(), s.restore() + } + }) + }, { + 25: 25, + 26: 26, + 45: 45 + }], + 38: [function(t, e, i) { + "use strict"; + var n = t(25), + a = t(26), + o = t(45), + r = n.global.defaultColor; + + function s(t) { + var e = this._view; + return !!e && Math.abs(t - e.x) < e.radius + e.hitRadius + } + n._set("global", { + elements: { + point: { + radius: 3, + pointStyle: "circle", + backgroundColor: r, + borderColor: r, + borderWidth: 1, + hitRadius: 1, + hoverRadius: 4, + hoverBorderWidth: 1 + } + } + }), e.exports = a.extend({ + inRange: function(t, e) { + var i = this._view; + return !!i && Math.pow(t - i.x, 2) + Math.pow(e - i.y, 2) < Math.pow(i.hitRadius + i.radius, 2) + }, + inLabelRange: s, + inXRange: s, + inYRange: function(t) { + var e = this._view; + return !!e && Math.abs(t - e.y) < e.radius + e.hitRadius + }, + getCenterPoint: function() { + var t = this._view; + return { + x: t.x, + y: t.y + } + }, + getArea: function() { + return Math.PI * Math.pow(this._view.radius, 2) + }, + tooltipPosition: function() { + var t = this._view; + return { + x: t.x, + y: t.y, + padding: t.radius + t.borderWidth + } + }, + draw: function(t) { + var e = this._view, + i = this._model, + a = this._chart.ctx, + s = e.pointStyle, + l = e.radius, + u = e.x, + d = e.y, + c = o.color, + h = 0; + e.skip || (a.strokeStyle = e.borderColor || r, a.lineWidth = o.valueOrDefault(e.borderWidth, n.global.elements.point.borderWidth), a.fillStyle = e.backgroundColor || r, void 0 !== t && (i.x < t.left || 1.01 * t.right < i.x || i.y < t.top || 1.01 * t.bottom < i.y) && (i.x < t.left ? h = (u - i.x) / (t.left - i.x) : 1.01 * t.right < i.x ? h = (i.x - u) / (i.x - t.right) : i.y < t.top ? h = (d - i.y) / (t.top - i.y) : 1.01 * t.bottom < i.y && (h = (i.y - d) / (i.y - t.bottom)), h = Math.round(100 * h) / 100, a.strokeStyle = c(a.strokeStyle).alpha(h).rgbString(), a.fillStyle = c(a.fillStyle).alpha(h).rgbString()), o.canvas.drawPoint(a, s, l, u, d)) + } + }) + }, { + 25: 25, + 26: 26, + 45: 45 + }], + 39: [function(t, e, i) { + "use strict"; + var n = t(25), + a = t(26); + + function o(t) { + return void 0 !== t._view.width + } + + function r(t) { + var e, i, n, a, r = t._view; + if (o(t)) { + var s = r.width / 2; + e = r.x - s, i = r.x + s, n = Math.min(r.y, r.base), a = Math.max(r.y, r.base) + } else { + var l = r.height / 2; + e = Math.min(r.x, r.base), i = Math.max(r.x, r.base), n = r.y - l, a = r.y + l + } + return { + left: e, + top: n, + right: i, + bottom: a + } + } + n._set("global", { + elements: { + rectangle: { + backgroundColor: n.global.defaultColor, + borderColor: n.global.defaultColor, + borderSkipped: "bottom", + borderWidth: 0 + } + } + }), e.exports = a.extend({ + draw: function() { + var t, e, i, n, a, o, r, s = this._chart.ctx, + l = this._view, + u = l.borderWidth; + if (l.horizontal ? (t = l.base, e = l.x, i = l.y - l.height / 2, n = l.y + l.height / 2, a = e > t ? 1 : -1, o = 1, r = l.borderSkipped || "left") : (t = l.x - l.width / 2, e = l.x + l.width / 2, i = l.y, a = 1, o = (n = l.base) > i ? 1 : -1, r = l.borderSkipped || "bottom"), u) { + var d = Math.min(Math.abs(t - e), Math.abs(i - n)), + c = (u = u > d ? d : u) / 2, + h = t + ("left" !== r ? c * a : 0), + f = e + ("right" !== r ? -c * a : 0), + g = i + ("top" !== r ? c * o : 0), + p = n + ("bottom" !== r ? -c * o : 0); + h !== f && (i = g, n = p), g !== p && (t = h, e = f) + } + s.beginPath(), s.fillStyle = l.backgroundColor, s.strokeStyle = l.borderColor, s.lineWidth = u; + var m = [ + [t, n], + [t, i], + [e, i], + [e, n] + ], + v = ["bottom", "left", "top", "right"].indexOf(r, 0); + + function b(t) { + return m[(v + t) % 4] + } - 1 === v && (v = 0); + var x = b(0); + s.moveTo(x[0], x[1]); + for (var y = 1; y < 4; y++) x = b(y), s.lineTo(x[0], x[1]); + s.fill(), u && s.stroke() + }, + height: function() { + var t = this._view; + return t.base - t.y + }, + inRange: function(t, e) { + var i = !1; + if (this._view) { + var n = r(this); + i = t >= n.left && t <= n.right && e >= n.top && e <= n.bottom + } + return i + }, + inLabelRange: function(t, e) { + if (!this._view) return !1; + var i = r(this); + return o(this) ? t >= i.left && t <= i.right : e >= i.top && e <= i.bottom + }, + inXRange: function(t) { + var e = r(this); + return t >= e.left && t <= e.right + }, + inYRange: function(t) { + var e = r(this); + return t >= e.top && t <= e.bottom + }, + getCenterPoint: function() { + var t, e, i = this._view; + return o(this) ? (t = i.x, e = (i.y + i.base) / 2) : (t = (i.x + i.base) / 2, e = i.y), { + x: t, + y: e + } + }, + getArea: function() { + var t = this._view; + return t.width * Math.abs(t.y - t.base) + }, + tooltipPosition: function() { + var t = this._view; + return { + x: t.x, + y: t.y + } + } + }) + }, { + 25: 25, + 26: 26 + }], + 40: [function(t, e, i) { + "use strict"; + e.exports = {}, e.exports.Arc = t(36), e.exports.Line = t(37), e.exports.Point = t(38), e.exports.Rectangle = t(39) + }, { + 36: 36, + 37: 37, + 38: 38, + 39: 39 + }], + 41: [function(t, e, i) { + "use strict"; + var n = t(42); + i = e.exports = { + clear: function(t) { + t.ctx.clearRect(0, 0, t.width, t.height) + }, + roundedRect: function(t, e, i, n, a, o) { + if (o) { + var r = Math.min(o, n / 2), + s = Math.min(o, a / 2); + t.moveTo(e + r, i), t.lineTo(e + n - r, i), t.quadraticCurveTo(e + n, i, e + n, i + s), t.lineTo(e + n, i + a - s), t.quadraticCurveTo(e + n, i + a, e + n - r, i + a), t.lineTo(e + r, i + a), t.quadraticCurveTo(e, i + a, e, i + a - s), t.lineTo(e, i + s), t.quadraticCurveTo(e, i, e + r, i) + } else t.rect(e, i, n, a) + }, + drawPoint: function(t, e, i, n, a) { + var o, r, s, l, u, d; + if (!e || "object" != typeof e || "[object HTMLImageElement]" !== (o = e.toString()) && "[object HTMLCanvasElement]" !== o) { + if (!(isNaN(i) || i <= 0)) { + switch (e) { + default: + t.beginPath(), t.arc(n, a, i, 0, 2 * Math.PI), t.closePath(), t.fill(); + break; + case "triangle": + t.beginPath(), u = (r = 3 * i / Math.sqrt(3)) * Math.sqrt(3) / 2, t.moveTo(n - r / 2, a + u / 3), t.lineTo(n + r / 2, a + u / 3), t.lineTo(n, a - 2 * u / 3), t.closePath(), t.fill(); + break; + case "rect": + d = 1 / Math.SQRT2 * i, t.beginPath(), t.fillRect(n - d, a - d, 2 * d, 2 * d), t.strokeRect(n - d, a - d, 2 * d, 2 * d); + break; + case "rectRounded": + var c = i / Math.SQRT2, + h = n - c, + f = a - c, + g = Math.SQRT2 * i; + t.beginPath(), this.roundedRect(t, h, f, g, g, i / 2), t.closePath(), t.fill(); + break; + case "rectRot": + d = 1 / Math.SQRT2 * i, t.beginPath(), t.moveTo(n - d, a), t.lineTo(n, a + d), t.lineTo(n + d, a), t.lineTo(n, a - d), t.closePath(), t.fill(); + break; + case "cross": + t.beginPath(), t.moveTo(n, a + i), t.lineTo(n, a - i), t.moveTo(n - i, a), t.lineTo(n + i, a), t.closePath(); + break; + case "crossRot": + t.beginPath(), s = Math.cos(Math.PI / 4) * i, l = Math.sin(Math.PI / 4) * i, t.moveTo(n - s, a - l), t.lineTo(n + s, a + l), t.moveTo(n - s, a + l), t.lineTo(n + s, a - l), t.closePath(); + break; + case "star": + t.beginPath(), t.moveTo(n, a + i), t.lineTo(n, a - i), t.moveTo(n - i, a), t.lineTo(n + i, a), s = Math.cos(Math.PI / 4) * i, l = Math.sin(Math.PI / 4) * i, t.moveTo(n - s, a - l), t.lineTo(n + s, a + l), t.moveTo(n - s, a + l), t.lineTo(n + s, a - l), t.closePath(); + break; + case "line": + t.beginPath(), t.moveTo(n - i, a), t.lineTo(n + i, a), t.closePath(); + break; + case "dash": + t.beginPath(), t.moveTo(n, a), t.lineTo(n + i, a), t.closePath() + } + t.stroke() + } + } else t.drawImage(e, n - e.width / 2, a - e.height / 2, e.width, e.height) + }, + clipArea: function(t, e) { + t.save(), t.beginPath(), t.rect(e.left, e.top, e.right - e.left, e.bottom - e.top), t.clip() + }, + unclipArea: function(t) { + t.restore() + }, + lineTo: function(t, e, i, n) { + if (i.steppedLine) return "after" === i.steppedLine && !n || "after" !== i.steppedLine && n ? t.lineTo(e.x, i.y) : t.lineTo(i.x, e.y), void t.lineTo(i.x, i.y); + i.tension ? t.bezierCurveTo(n ? e.controlPointPreviousX : e.controlPointNextX, n ? e.controlPointPreviousY : e.controlPointNextY, n ? i.controlPointNextX : i.controlPointPreviousX, n ? i.controlPointNextY : i.controlPointPreviousY, i.x, i.y) : t.lineTo(i.x, i.y) + } + }; + n.clear = i.clear, n.drawRoundedRectangle = function(t) { + t.beginPath(), i.roundedRect.apply(i, arguments), t.closePath() + } + }, { + 42: 42 + }], + 42: [function(t, e, i) { + "use strict"; + var n, a = { + noop: function() {}, + uid: (n = 0, function() { + return n++ + }), + isNullOrUndef: function(t) { + return null == t + }, + isArray: Array.isArray ? Array.isArray : function(t) { + return "[object Array]" === Object.prototype.toString.call(t) + }, + isObject: function(t) { + return null !== t && "[object Object]" === Object.prototype.toString.call(t) + }, + valueOrDefault: function(t, e) { + return void 0 === t ? e : t + }, + valueAtIndexOrDefault: function(t, e, i) { + return a.valueOrDefault(a.isArray(t) ? t[e] : t, i) + }, + callback: function(t, e, i) { + if (t && "function" == typeof t.call) return t.apply(i, e) + }, + each: function(t, e, i, n) { + var o, r, s; + if (a.isArray(t)) + if (r = t.length, n) + for (o = r - 1; o >= 0; o--) e.call(i, t[o], o); + else + for (o = 0; o < r; o++) e.call(i, t[o], o); + else if (a.isObject(t)) + for (r = (s = Object.keys(t)).length, o = 0; o < r; o++) e.call(i, t[s[o]], s[o]) + }, + arrayEquals: function(t, e) { + var i, n, o, r; + if (!t || !e || t.length !== e.length) return !1; + for (i = 0, n = t.length; i < n; ++i) + if (o = t[i], r = e[i], o instanceof Array && r instanceof Array) { + if (!a.arrayEquals(o, r)) return !1 + } else if (o !== r) return !1; + return !0 + }, + clone: function(t) { + if (a.isArray(t)) return t.map(a.clone); + if (a.isObject(t)) { + for (var e = {}, i = Object.keys(t), n = i.length, o = 0; o < n; ++o) e[i[o]] = a.clone(t[i[o]]); + return e + } + return t + }, + _merger: function(t, e, i, n) { + var o = e[t], + r = i[t]; + a.isObject(o) && a.isObject(r) ? a.merge(o, r, n) : e[t] = a.clone(r) + }, + _mergerIf: function(t, e, i) { + var n = e[t], + o = i[t]; + a.isObject(n) && a.isObject(o) ? a.mergeIf(n, o) : e.hasOwnProperty(t) || (e[t] = a.clone(o)) + }, + merge: function(t, e, i) { + var n, o, r, s, l, u = a.isArray(e) ? e : [e], + d = u.length; + if (!a.isObject(t)) return t; + for (n = (i = i || {}).merger || a._merger, o = 0; o < d; ++o) + if (e = u[o], a.isObject(e)) + for (l = 0, s = (r = Object.keys(e)).length; l < s; ++l) n(r[l], t, e, i); + return t + }, + mergeIf: function(t, e) { + return a.merge(t, e, { + merger: a._mergerIf + }) + }, + extend: function(t) { + for (var e = function(e, i) { + t[i] = e + }, i = 1, n = arguments.length; i < n; ++i) a.each(arguments[i], e); + return t + }, + inherits: function(t) { + var e = this, + i = t && t.hasOwnProperty("constructor") ? t.constructor : function() { + return e.apply(this, arguments) + }, + n = function() { + this.constructor = i + }; + return n.prototype = e.prototype, i.prototype = new n, i.extend = a.inherits, t && a.extend(i.prototype, t), i.__super__ = e.prototype, i + } + }; + e.exports = a, a.callCallback = a.callback, a.indexOf = function(t, e, i) { + return Array.prototype.indexOf.call(t, e, i) + }, a.getValueOrDefault = a.valueOrDefault, a.getValueAtIndexOrDefault = a.valueAtIndexOrDefault + }, {}], + 43: [function(t, e, i) { + "use strict"; + var n = t(42), + a = { + linear: function(t) { + return t + }, + easeInQuad: function(t) { + return t * t + }, + easeOutQuad: function(t) { + return -t * (t - 2) + }, + easeInOutQuad: function(t) { + return (t /= .5) < 1 ? .5 * t * t : -.5 * (--t * (t - 2) - 1) + }, + easeInCubic: function(t) { + return t * t * t + }, + easeOutCubic: function(t) { + return (t -= 1) * t * t + 1 + }, + easeInOutCubic: function(t) { + return (t /= .5) < 1 ? .5 * t * t * t : .5 * ((t -= 2) * t * t + 2) + }, + easeInQuart: function(t) { + return t * t * t * t + }, + easeOutQuart: function(t) { + return -((t -= 1) * t * t * t - 1) + }, + easeInOutQuart: function(t) { + return (t /= .5) < 1 ? .5 * t * t * t * t : -.5 * ((t -= 2) * t * t * t - 2) + }, + easeInQuint: function(t) { + return t * t * t * t * t + }, + easeOutQuint: function(t) { + return (t -= 1) * t * t * t * t + 1 + }, + easeInOutQuint: function(t) { + return (t /= .5) < 1 ? .5 * t * t * t * t * t : .5 * ((t -= 2) * t * t * t * t + 2) + }, + easeInSine: function(t) { + return 1 - Math.cos(t * (Math.PI / 2)) + }, + easeOutSine: function(t) { + return Math.sin(t * (Math.PI / 2)) + }, + easeInOutSine: function(t) { + return -.5 * (Math.cos(Math.PI * t) - 1) + }, + easeInExpo: function(t) { + return 0 === t ? 0 : Math.pow(2, 10 * (t - 1)) + }, + easeOutExpo: function(t) { + return 1 === t ? 1 : 1 - Math.pow(2, -10 * t) + }, + easeInOutExpo: function(t) { + return 0 === t ? 0 : 1 === t ? 1 : (t /= .5) < 1 ? .5 * Math.pow(2, 10 * (t - 1)) : .5 * (2 - Math.pow(2, -10 * --t)) + }, + easeInCirc: function(t) { + return t >= 1 ? t : -(Math.sqrt(1 - t * t) - 1) + }, + easeOutCirc: function(t) { + return Math.sqrt(1 - (t -= 1) * t) + }, + easeInOutCirc: function(t) { + return (t /= .5) < 1 ? -.5 * (Math.sqrt(1 - t * t) - 1) : .5 * (Math.sqrt(1 - (t -= 2) * t) + 1) + }, + easeInElastic: function(t) { + var e = 1.70158, + i = 0, + n = 1; + return 0 === t ? 0 : 1 === t ? 1 : (i || (i = .3), n < 1 ? (n = 1, e = i / 4) : e = i / (2 * Math.PI) * Math.asin(1 / n), -n * Math.pow(2, 10 * (t -= 1)) * Math.sin((t - e) * (2 * Math.PI) / i)) + }, + easeOutElastic: function(t) { + var e = 1.70158, + i = 0, + n = 1; + return 0 === t ? 0 : 1 === t ? 1 : (i || (i = .3), n < 1 ? (n = 1, e = i / 4) : e = i / (2 * Math.PI) * Math.asin(1 / n), n * Math.pow(2, -10 * t) * Math.sin((t - e) * (2 * Math.PI) / i) + 1) + }, + easeInOutElastic: function(t) { + var e = 1.70158, + i = 0, + n = 1; + return 0 === t ? 0 : 2 == (t /= .5) ? 1 : (i || (i = .45), n < 1 ? (n = 1, e = i / 4) : e = i / (2 * Math.PI) * Math.asin(1 / n), t < 1 ? n * Math.pow(2, 10 * (t -= 1)) * Math.sin((t - e) * (2 * Math.PI) / i) * -.5 : n * Math.pow(2, -10 * (t -= 1)) * Math.sin((t - e) * (2 * Math.PI) / i) * .5 + 1) + }, + easeInBack: function(t) { + return t * t * (2.70158 * t - 1.70158) + }, + easeOutBack: function(t) { + return (t -= 1) * t * (2.70158 * t + 1.70158) + 1 + }, + easeInOutBack: function(t) { + var e = 1.70158; + return (t /= .5) < 1 ? t * t * ((1 + (e *= 1.525)) * t - e) * .5 : .5 * ((t -= 2) * t * ((1 + (e *= 1.525)) * t + e) + 2) + }, + easeInBounce: function(t) { + return 1 - a.easeOutBounce(1 - t) + }, + easeOutBounce: function(t) { + return t < 1 / 2.75 ? 7.5625 * t * t : t < 2 / 2.75 ? 7.5625 * (t -= 1.5 / 2.75) * t + .75 : t < 2.5 / 2.75 ? 7.5625 * (t -= 2.25 / 2.75) * t + .9375 : 7.5625 * (t -= 2.625 / 2.75) * t + .984375 + }, + easeInOutBounce: function(t) { + return t < .5 ? .5 * a.easeInBounce(2 * t) : .5 * a.easeOutBounce(2 * t - 1) + .5 + } + }; + e.exports = { + effects: a + }, n.easingEffects = a + }, { + 42: 42 + }], + 44: [function(t, e, i) { + "use strict"; + var n = t(42); + e.exports = { + toLineHeight: function(t, e) { + var i = ("" + t).match(/^(normal|(\d+(?:\.\d+)?)(px|em|%)?)$/); + if (!i || "normal" === i[1]) return 1.2 * e; + switch (t = +i[2], i[3]) { + case "px": + return t; + case "%": + t /= 100 + } + return e * t + }, + toPadding: function(t) { + var e, i, a, o; + return n.isObject(t) ? (e = +t.top || 0, i = +t.right || 0, a = +t.bottom || 0, o = +t.left || 0) : e = i = a = o = +t || 0, { + top: e, + right: i, + bottom: a, + left: o, + height: e + a, + width: o + i + } + }, + resolve: function(t, e, i) { + var a, o, r; + for (a = 0, o = t.length; a < o; ++a) + if (void 0 !== (r = t[a]) && (void 0 !== e && "function" == typeof r && (r = r(e)), void 0 !== i && n.isArray(r) && (r = r[i]), void 0 !== r)) return r + } + } + }, { + 42: 42 + }], + 45: [function(t, e, i) { + "use strict"; + e.exports = t(42), e.exports.easing = t(43), e.exports.canvas = t(41), e.exports.options = t(44) + }, { + 41: 41, + 42: 42, + 43: 43, + 44: 44 + }], + 46: [function(t, e, i) { + e.exports = { + acquireContext: function(t) { + return t && t.canvas && (t = t.canvas), t && t.getContext("2d") || null + } + } + }, {}], + 47: [function(t, e, i) { + "use strict"; + var n = t(45), + a = "$chartjs", + o = "chartjs-", + r = o + "render-monitor", + s = o + "render-animation", + l = ["animationstart", "webkitAnimationStart"], + u = { + touchstart: "mousedown", + touchmove: "mousemove", + touchend: "mouseup", + pointerenter: "mouseenter", + pointerdown: "mousedown", + pointermove: "mousemove", + pointerup: "mouseup", + pointerleave: "mouseout", + pointerout: "mouseout" + }; + + function d(t, e) { + var i = n.getStyle(t, e), + a = i && i.match(/^(\d+)(\.\d+)?px$/); + return a ? Number(a[1]) : void 0 + } + var c = !! function() { + var t = !1; + try { + var e = Object.defineProperty({}, "passive", { + get: function() { + t = !0 + } + }); + window.addEventListener("e", null, e) + } catch (t) {} + return t + }() && { + passive: !0 + }; + + function h(t, e, i) { + t.addEventListener(e, i, c) + } + + function f(t, e, i) { + t.removeEventListener(e, i, c) + } + + function g(t, e, i, n, a) { + return { + type: t, + chart: e, + native: a || null, + x: void 0 !== i ? i : null, + y: void 0 !== n ? n : null + } + } + + function p(t, e, i) { + var u, d, c, f, p, m, v, b, x = t[a] || (t[a] = {}), + y = x.resizer = function(t) { + var e = document.createElement("div"), + i = o + "size-monitor", + n = "position:absolute;left:0;top:0;right:0;bottom:0;overflow:hidden;pointer-events:none;visibility:hidden;z-index:-1;"; + e.style.cssText = n, e.className = i, e.innerHTML = '
'; + var a = e.childNodes[0], + r = e.childNodes[1]; + e._reset = function() { + a.scrollLeft = 1e6, a.scrollTop = 1e6, r.scrollLeft = 1e6, r.scrollTop = 1e6 + }; + var s = function() { + e._reset(), t() + }; + return h(a, "scroll", s.bind(a, "expand")), h(r, "scroll", s.bind(r, "shrink")), e + }((u = function() { + if (x.resizer) return e(g("resize", i)) + }, c = !1, f = [], function() { + f = Array.prototype.slice.call(arguments), d = d || this, c || (c = !0, n.requestAnimFrame.call(window, function() { + c = !1, u.apply(d, f) + })) + })); + m = function() { + if (x.resizer) { + var e = t.parentNode; + e && e !== y.parentNode && e.insertBefore(y, e.firstChild), y._reset() + } + }, v = (p = t)[a] || (p[a] = {}), b = v.renderProxy = function(t) { + t.animationName === s && m() + }, n.each(l, function(t) { + h(p, t, b) + }), v.reflow = !!p.offsetParent, p.classList.add(r) + } + + function m(t) { + var e, i, o, s = t[a] || {}, + u = s.resizer; + delete s.resizer, i = (e = t)[a] || {}, (o = i.renderProxy) && (n.each(l, function(t) { + f(e, t, o) + }), delete i.renderProxy), e.classList.remove(r), u && u.parentNode && u.parentNode.removeChild(u) + } + e.exports = { + _enabled: "undefined" != typeof window && "undefined" != typeof document, + initialize: function() { + var t, e, i, n = "from{opacity:0.99}to{opacity:1}"; + e = "@-webkit-keyframes " + s + "{" + n + "}@keyframes " + s + "{" + n + "}." + r + "{-webkit-animation:" + s + " 0.001s;animation:" + s + " 0.001s;}", i = (t = this)._style || document.createElement("style"), t._style || (t._style = i, e = "/* Chart.js */\n" + e, i.setAttribute("type", "text/css"), document.getElementsByTagName("head")[0].appendChild(i)), i.appendChild(document.createTextNode(e)) + }, + acquireContext: function(t, e) { + "string" == typeof t ? t = document.getElementById(t) : t.length && (t = t[0]), t && t.canvas && (t = t.canvas); + var i = t && t.getContext && t.getContext("2d"); + return i && i.canvas === t ? (function(t, e) { + var i = t.style, + n = t.getAttribute("height"), + o = t.getAttribute("width"); + if (t[a] = { + initial: { + height: n, + width: o, + style: { + display: i.display, + height: i.height, + width: i.width + } + } + }, i.display = i.display || "block", null === o || "" === o) { + var r = d(t, "width"); + void 0 !== r && (t.width = r) + } + if (null === n || "" === n) + if ("" === t.style.height) t.height = t.width / (e.options.aspectRatio || 2); + else { + var s = d(t, "height"); + void 0 !== r && (t.height = s) + } + }(t, e), i) : null + }, + releaseContext: function(t) { + var e = t.canvas; + if (e[a]) { + var i = e[a].initial; + ["height", "width"].forEach(function(t) { + var a = i[t]; + n.isNullOrUndef(a) ? e.removeAttribute(t) : e.setAttribute(t, a) + }), n.each(i.style || {}, function(t, i) { + e.style[i] = t + }), e.width = e.width, delete e[a] + } + }, + addEventListener: function(t, e, i) { + var o = t.canvas; + if ("resize" !== e) { + var r = i[a] || (i[a] = {}); + h(o, e, (r.proxies || (r.proxies = {}))[t.id + "_" + e] = function(e) { + var a, o, r, s; + i((o = t, r = u[(a = e).type] || a.type, s = n.getRelativePosition(a, o), g(r, o, s.x, s.y, a))) + }) + } else p(o, i, t) + }, + removeEventListener: function(t, e, i) { + var n = t.canvas; + if ("resize" !== e) { + var o = ((i[a] || {}).proxies || {})[t.id + "_" + e]; + o && f(n, e, o) + } else m(n) + } + }, n.addEvent = h, n.removeEvent = f + }, { + 45: 45 + }], + 48: [function(t, e, i) { + "use strict"; + var n = t(45), + a = t(46), + o = t(47), + r = o._enabled ? o : a; + e.exports = n.extend({ + initialize: function() {}, + acquireContext: function() {}, + releaseContext: function() {}, + addEventListener: function() {}, + removeEventListener: function() {} + }, r) + }, { + 45: 45, + 46: 46, + 47: 47 + }], + 49: [function(t, e, i) { + "use strict"; + e.exports = {}, e.exports.filler = t(50), e.exports.legend = t(51), e.exports.title = t(52) + }, { + 50: 50, + 51: 51, + 52: 52 + }], + 50: [function(t, e, i) { + "use strict"; + var n = t(25), + a = t(40), + o = t(45); + n._set("global", { + plugins: { + filler: { + propagate: !0 + } + } + }); + var r = { + dataset: function(t) { + var e = t.fill, + i = t.chart, + n = i.getDatasetMeta(e), + a = n && i.isDatasetVisible(e) && n.dataset._children || [], + o = a.length || 0; + return o ? function(t, e) { + return e < o && a[e]._view || null + } : null + }, + boundary: function(t) { + var e = t.boundary, + i = e ? e.x : null, + n = e ? e.y : null; + return function(t) { + return { + x: null === i ? t.x : i, + y: null === n ? t.y : n + } + } + } + }; + + function s(t, e, i) { + var n, a = t._model || {}, + o = a.fill; + if (void 0 === o && (o = !!a.backgroundColor), !1 === o || null === o) return !1; + if (!0 === o) return "origin"; + if (n = parseFloat(o, 10), isFinite(n) && Math.floor(n) === n) return "-" !== o[0] && "+" !== o[0] || (n = e + n), !(n === e || n < 0 || n >= i) && n; + switch (o) { + case "bottom": + return "start"; + case "top": + return "end"; + case "zero": + return "origin"; + case "origin": + case "start": + case "end": + return o; + default: + return !1 + } + } + + function l(t) { + var e, i = t.el._model || {}, + n = t.el._scale || {}, + a = t.fill, + o = null; + if (isFinite(a)) return null; + if ("start" === a ? o = void 0 === i.scaleBottom ? n.bottom : i.scaleBottom : "end" === a ? o = void 0 === i.scaleTop ? n.top : i.scaleTop : void 0 !== i.scaleZero ? o = i.scaleZero : n.getBasePosition ? o = n.getBasePosition() : n.getBasePixel && (o = n.getBasePixel()), null != o) { + if (void 0 !== o.x && void 0 !== o.y) return o; + if ("number" == typeof o && isFinite(o)) return { + x: (e = n.isHorizontal()) ? o : null, + y: e ? null : o + } + } + return null + } + + function u(t, e, i) { + var n, a = t[e].fill, + o = [e]; + if (!i) return a; + for (; !1 !== a && -1 === o.indexOf(a);) { + if (!isFinite(a)) return a; + if (!(n = t[a])) return !1; + if (n.visible) return a; + o.push(a), a = n.fill + } + return !1 + } + + function d(t) { + return t && !t.skip + } + + function c(t, e, i, n, a) { + var r; + if (n && a) { + for (t.moveTo(e[0].x, e[0].y), r = 1; r < n; ++r) o.canvas.lineTo(t, e[r - 1], e[r]); + for (t.lineTo(i[a - 1].x, i[a - 1].y), r = a - 1; r > 0; --r) o.canvas.lineTo(t, i[r], i[r - 1], !0) + } + } + e.exports = { + id: "filler", + afterDatasetsUpdate: function(t, e) { + var i, n, o, d, c, h, f, g = (t.data.datasets || []).length, + p = e.propagate, + m = []; + for (n = 0; n < g; ++n) d = null, (o = (i = t.getDatasetMeta(n)).dataset) && o._model && o instanceof a.Line && (d = { + visible: t.isDatasetVisible(n), + fill: s(o, n, g), + chart: t, + el: o + }), i.$filler = d, m.push(d); + for (n = 0; n < g; ++n)(d = m[n]) && (d.fill = u(m, n, p), d.boundary = l(d), d.mapper = (void 0, f = void 0, h = (c = d).fill, f = "dataset", !1 === h ? null : (isFinite(h) || (f = "boundary"), r[f](c)))) + }, + beforeDatasetDraw: function(t, e) { + var i = e.meta.$filler; + if (i) { + var a = t.ctx, + r = i.el, + s = r._view, + l = r._children || [], + u = i.mapper, + h = s.backgroundColor || n.global.defaultColor; + u && h && l.length && (o.canvas.clipArea(a, t.chartArea), function(t, e, i, n, a, o) { + var r, s, l, u, h, f, g, p = e.length, + m = n.spanGaps, + v = [], + b = [], + x = 0, + y = 0; + for (t.beginPath(), r = 0, s = p + !!o; r < s; ++r) h = i(u = e[l = r % p]._view, l, n), f = d(u), g = d(h), f && g ? (x = v.push(u), y = b.push(h)) : x && y && (m ? (f && v.push(u), g && b.push(h)) : (c(t, v, b, x, y), x = y = 0, v = [], b = [])); + c(t, v, b, x, y), t.closePath(), t.fillStyle = a, t.fill() + }(a, l, u, s, h, r._loop), o.canvas.unclipArea(a)) + } + } + } + }, { + 25: 25, + 40: 40, + 45: 45 + }], + 51: [function(t, e, i) { + "use strict"; + var n = t(25), + a = t(26), + o = t(45), + r = t(30), + s = o.noop; + + function l(t, e) { + return t.usePointStyle ? e * Math.SQRT2 : t.boxWidth + } + n._set("global", { + legend: { + display: !0, + position: "top", + fullWidth: !0, + reverse: !1, + weight: 1e3, + onClick: function(t, e) { + var i = e.datasetIndex, + n = this.chart, + a = n.getDatasetMeta(i); + a.hidden = null === a.hidden ? !n.data.datasets[i].hidden : null, n.update() + }, + onHover: null, + labels: { + boxWidth: 40, + padding: 10, + generateLabels: function(t) { + var e = t.data; + return o.isArray(e.datasets) ? e.datasets.map(function(e, i) { + return { + text: e.label, + fillStyle: o.isArray(e.backgroundColor) ? e.backgroundColor[0] : e.backgroundColor, + hidden: !t.isDatasetVisible(i), + lineCap: e.borderCapStyle, + lineDash: e.borderDash, + lineDashOffset: e.borderDashOffset, + lineJoin: e.borderJoinStyle, + lineWidth: e.borderWidth, + strokeStyle: e.borderColor, + pointStyle: e.pointStyle, + datasetIndex: i + } + }, this) : [] + } + } + }, + legendCallback: function(t) { + var e = []; + e.push('
    '); + for (var i = 0; i < t.data.datasets.length; i++) e.push('
  • '), t.data.datasets[i].label && e.push(t.data.datasets[i].label), e.push("
  • "); + return e.push("
"), e.join("") + } + }); + var u = a.extend({ + initialize: function(t) { + o.extend(this, t), this.legendHitBoxes = [], this.doughnutMode = !1 + }, + beforeUpdate: s, + update: function(t, e, i) { + var n = this; + return n.beforeUpdate(), n.maxWidth = t, n.maxHeight = e, n.margins = i, n.beforeSetDimensions(), n.setDimensions(), n.afterSetDimensions(), n.beforeBuildLabels(), n.buildLabels(), n.afterBuildLabels(), n.beforeFit(), n.fit(), n.afterFit(), n.afterUpdate(), n.minSize + }, + afterUpdate: s, + beforeSetDimensions: s, + setDimensions: function() { + var t = this; + t.isHorizontal() ? (t.width = t.maxWidth, t.left = 0, t.right = t.width) : (t.height = t.maxHeight, t.top = 0, t.bottom = t.height), t.paddingLeft = 0, t.paddingTop = 0, t.paddingRight = 0, t.paddingBottom = 0, t.minSize = { + width: 0, + height: 0 + } + }, + afterSetDimensions: s, + beforeBuildLabels: s, + buildLabels: function() { + var t = this, + e = t.options.labels || {}, + i = o.callback(e.generateLabels, [t.chart], t) || []; + e.filter && (i = i.filter(function(i) { + return e.filter(i, t.chart.data) + })), t.options.reverse && i.reverse(), t.legendItems = i + }, + afterBuildLabels: s, + beforeFit: s, + fit: function() { + var t = this, + e = t.options, + i = e.labels, + a = e.display, + r = t.ctx, + s = n.global, + u = o.valueOrDefault, + d = u(i.fontSize, s.defaultFontSize), + c = u(i.fontStyle, s.defaultFontStyle), + h = u(i.fontFamily, s.defaultFontFamily), + f = o.fontString(d, c, h), + g = t.legendHitBoxes = [], + p = t.minSize, + m = t.isHorizontal(); + if (m ? (p.width = t.maxWidth, p.height = a ? 10 : 0) : (p.width = a ? 10 : 0, p.height = t.maxHeight), a) + if (r.font = f, m) { + var v = t.lineWidths = [0], + b = t.legendItems.length ? d + i.padding : 0; + r.textAlign = "left", r.textBaseline = "top", o.each(t.legendItems, function(e, n) { + var a = l(i, d) + d / 2 + r.measureText(e.text).width; + v[v.length - 1] + a + i.padding >= t.width && (b += d + i.padding, v[v.length] = t.left), g[n] = { + left: 0, + top: 0, + width: a, + height: d + }, v[v.length - 1] += a + i.padding + }), p.height += b + } else { + var x = i.padding, + y = t.columnWidths = [], + k = i.padding, + M = 0, + w = 0, + S = d + x; + o.each(t.legendItems, function(t, e) { + var n = l(i, d) + d / 2 + r.measureText(t.text).width; + w + S > p.height && (k += M + i.padding, y.push(M), M = 0, w = 0), M = Math.max(M, n), w += S, g[e] = { + left: 0, + top: 0, + width: n, + height: d + } + }), k += M, y.push(M), p.width += k + } t.width = p.width, t.height = p.height + }, + afterFit: s, + isHorizontal: function() { + return "top" === this.options.position || "bottom" === this.options.position + }, + draw: function() { + var t = this, + e = t.options, + i = e.labels, + a = n.global, + r = a.elements.line, + s = t.width, + u = t.lineWidths; + if (e.display) { + var d, c = t.ctx, + h = o.valueOrDefault, + f = h(i.fontColor, a.defaultFontColor), + g = h(i.fontSize, a.defaultFontSize), + p = h(i.fontStyle, a.defaultFontStyle), + m = h(i.fontFamily, a.defaultFontFamily), + v = o.fontString(g, p, m); + c.textAlign = "left", c.textBaseline = "middle", c.lineWidth = .5, c.strokeStyle = f, c.fillStyle = f, c.font = v; + var b = l(i, g), + x = t.legendHitBoxes, + y = t.isHorizontal(); + d = y ? { + x: t.left + (s - u[0]) / 2, + y: t.top + i.padding, + line: 0 + } : { + x: t.left + i.padding, + y: t.top + i.padding, + line: 0 + }; + var k = g + i.padding; + o.each(t.legendItems, function(n, l) { + var f, p, m, v, M, w = c.measureText(n.text).width, + S = b + g / 2 + w, + C = d.x, + _ = d.y; + y ? C + S >= s && (_ = d.y += k, d.line++, C = d.x = t.left + (s - u[d.line]) / 2) : _ + k > t.bottom && (C = d.x = C + t.columnWidths[d.line] + i.padding, _ = d.y = t.top + i.padding, d.line++), + function(t, i, n) { + if (!(isNaN(b) || b <= 0)) { + c.save(), c.fillStyle = h(n.fillStyle, a.defaultColor), c.lineCap = h(n.lineCap, r.borderCapStyle), c.lineDashOffset = h(n.lineDashOffset, r.borderDashOffset), c.lineJoin = h(n.lineJoin, r.borderJoinStyle), c.lineWidth = h(n.lineWidth, r.borderWidth), c.strokeStyle = h(n.strokeStyle, a.defaultColor); + var s = 0 === h(n.lineWidth, r.borderWidth); + if (c.setLineDash && c.setLineDash(h(n.lineDash, r.borderDash)), e.labels && e.labels.usePointStyle) { + var l = g * Math.SQRT2 / 2, + u = l / Math.SQRT2, + d = t + u, + f = i + u; + o.canvas.drawPoint(c, n.pointStyle, l, d, f) + } else s || c.strokeRect(t, i, b, g), c.fillRect(t, i, b, g); + c.restore() + } + }(C, _, n), x[l].left = C, x[l].top = _, f = n, p = w, v = b + (m = g / 2) + C, M = _ + m, c.fillText(f.text, v, M), f.hidden && (c.beginPath(), c.lineWidth = 2, c.moveTo(v, M), c.lineTo(v + p, M), c.stroke()), y ? d.x += S + i.padding : d.y += k + }) + } + }, + handleEvent: function(t) { + var e = this, + i = e.options, + n = "mouseup" === t.type ? "click" : t.type, + a = !1; + if ("mousemove" === n) { + if (!i.onHover) return + } else { + if ("click" !== n) return; + if (!i.onClick) return + } + var o = t.x, + r = t.y; + if (o >= e.left && o <= e.right && r >= e.top && r <= e.bottom) + for (var s = e.legendHitBoxes, l = 0; l < s.length; ++l) { + var u = s[l]; + if (o >= u.left && o <= u.left + u.width && r >= u.top && r <= u.top + u.height) { + if ("click" === n) { + i.onClick.call(e, t.native, e.legendItems[l]), a = !0; + break + } + if ("mousemove" === n) { + i.onHover.call(e, t.native, e.legendItems[l]), a = !0; + break + } + } + } + return a + } + }); + + function d(t, e) { + var i = new u({ + ctx: t.ctx, + options: e, + chart: t + }); + r.configure(t, i, e), r.addBox(t, i), t.legend = i + } + e.exports = { + id: "legend", + _element: u, + beforeInit: function(t) { + var e = t.options.legend; + e && d(t, e) + }, + beforeUpdate: function(t) { + var e = t.options.legend, + i = t.legend; + e ? (o.mergeIf(e, n.global.legend), i ? (r.configure(t, i, e), i.options = e) : d(t, e)) : i && (r.removeBox(t, i), delete t.legend) + }, + afterEvent: function(t, e) { + var i = t.legend; + i && i.handleEvent(e) + } + } + }, { + 25: 25, + 26: 26, + 30: 30, + 45: 45 + }], + 52: [function(t, e, i) { + "use strict"; + var n = t(25), + a = t(26), + o = t(45), + r = t(30), + s = o.noop; + n._set("global", { + title: { + display: !1, + fontStyle: "bold", + fullWidth: !0, + lineHeight: 1.2, + padding: 10, + position: "top", + text: "", + weight: 2e3 + } + }); + var l = a.extend({ + initialize: function(t) { + o.extend(this, t), this.legendHitBoxes = [] + }, + beforeUpdate: s, + update: function(t, e, i) { + var n = this; + return n.beforeUpdate(), n.maxWidth = t, n.maxHeight = e, n.margins = i, n.beforeSetDimensions(), n.setDimensions(), n.afterSetDimensions(), n.beforeBuildLabels(), n.buildLabels(), n.afterBuildLabels(), n.beforeFit(), n.fit(), n.afterFit(), n.afterUpdate(), n.minSize + }, + afterUpdate: s, + beforeSetDimensions: s, + setDimensions: function() { + var t = this; + t.isHorizontal() ? (t.width = t.maxWidth, t.left = 0, t.right = t.width) : (t.height = t.maxHeight, t.top = 0, t.bottom = t.height), t.paddingLeft = 0, t.paddingTop = 0, t.paddingRight = 0, t.paddingBottom = 0, t.minSize = { + width: 0, + height: 0 + } + }, + afterSetDimensions: s, + beforeBuildLabels: s, + buildLabels: s, + afterBuildLabels: s, + beforeFit: s, + fit: function() { + var t = this, + e = o.valueOrDefault, + i = t.options, + a = i.display, + r = e(i.fontSize, n.global.defaultFontSize), + s = t.minSize, + l = o.isArray(i.text) ? i.text.length : 1, + u = o.options.toLineHeight(i.lineHeight, r), + d = a ? l * u + 2 * i.padding : 0; + t.isHorizontal() ? (s.width = t.maxWidth, s.height = d) : (s.width = d, s.height = t.maxHeight), t.width = s.width, t.height = s.height + }, + afterFit: s, + isHorizontal: function() { + var t = this.options.position; + return "top" === t || "bottom" === t + }, + draw: function() { + var t = this, + e = t.ctx, + i = o.valueOrDefault, + a = t.options, + r = n.global; + if (a.display) { + var s, l, u, d = i(a.fontSize, r.defaultFontSize), + c = i(a.fontStyle, r.defaultFontStyle), + h = i(a.fontFamily, r.defaultFontFamily), + f = o.fontString(d, c, h), + g = o.options.toLineHeight(a.lineHeight, d), + p = g / 2 + a.padding, + m = 0, + v = t.top, + b = t.left, + x = t.bottom, + y = t.right; + e.fillStyle = i(a.fontColor, r.defaultFontColor), e.font = f, t.isHorizontal() ? (l = b + (y - b) / 2, u = v + p, s = y - b) : (l = "left" === a.position ? b + p : y - p, u = v + (x - v) / 2, s = x - v, m = Math.PI * ("left" === a.position ? -.5 : .5)), e.save(), e.translate(l, u), e.rotate(m), e.textAlign = "center", e.textBaseline = "middle"; + var k = a.text; + if (o.isArray(k)) + for (var M = 0, w = 0; w < k.length; ++w) e.fillText(k[w], 0, M, s), M += g; + else e.fillText(k, 0, 0, s); + e.restore() + } + } + }); + + function u(t, e) { + var i = new l({ + ctx: t.ctx, + options: e, + chart: t + }); + r.configure(t, i, e), r.addBox(t, i), t.titleBlock = i + } + e.exports = { + id: "title", + _element: l, + beforeInit: function(t) { + var e = t.options.title; + e && u(t, e) + }, + beforeUpdate: function(t) { + var e = t.options.title, + i = t.titleBlock; + e ? (o.mergeIf(e, n.global.title), i ? (r.configure(t, i, e), i.options = e) : u(t, e)) : i && (r.removeBox(t, i), delete t.titleBlock) + } + } + }, { + 25: 25, + 26: 26, + 30: 30, + 45: 45 + }], + 53: [function(t, e, i) { + "use strict"; + e.exports = function(t) { + var e = t.Scale.extend({ + getLabels: function() { + var t = this.chart.data; + return this.options.labels || (this.isHorizontal() ? t.xLabels : t.yLabels) || t.labels + }, + determineDataLimits: function() { + var t, e = this, + i = e.getLabels(); + e.minIndex = 0, e.maxIndex = i.length - 1, void 0 !== e.options.ticks.min && (t = i.indexOf(e.options.ticks.min), e.minIndex = -1 !== t ? t : e.minIndex), void 0 !== e.options.ticks.max && (t = i.indexOf(e.options.ticks.max), e.maxIndex = -1 !== t ? t : e.maxIndex), e.min = i[e.minIndex], e.max = i[e.maxIndex] + }, + buildTicks: function() { + var t = this, + e = t.getLabels(); + t.ticks = 0 === t.minIndex && t.maxIndex === e.length - 1 ? e : e.slice(t.minIndex, t.maxIndex + 1) + }, + getLabelForIndex: function(t, e) { + var i = this, + n = i.chart.data, + a = i.isHorizontal(); + return n.yLabels && !a ? i.getRightValue(n.datasets[e].data[t]) : i.ticks[t - i.minIndex] + }, + getPixelForValue: function(t, e) { + var i, n = this, + a = n.options.offset, + o = Math.max(n.maxIndex + 1 - n.minIndex - (a ? 0 : 1), 1); + if (null != t && (i = n.isHorizontal() ? t.x : t.y), void 0 !== i || void 0 !== t && isNaN(e)) { + t = i || t; + var r = n.getLabels().indexOf(t); + e = -1 !== r ? r : e + } + if (n.isHorizontal()) { + var s = n.width / o, + l = s * (e - n.minIndex); + return a && (l += s / 2), n.left + Math.round(l) + } + var u = n.height / o, + d = u * (e - n.minIndex); + return a && (d += u / 2), n.top + Math.round(d) + }, + getPixelForTick: function(t) { + return this.getPixelForValue(this.ticks[t], t + this.minIndex, null) + }, + getValueForPixel: function(t) { + var e = this, + i = e.options.offset, + n = Math.max(e._ticks.length - (i ? 0 : 1), 1), + a = e.isHorizontal(), + o = (a ? e.width : e.height) / n; + return t -= a ? e.left : e.top, i && (t -= o / 2), (t <= 0 ? 0 : Math.round(t / o)) + e.minIndex + }, + getBasePixel: function() { + return this.bottom + } + }); + t.scaleService.registerScaleType("category", e, { + position: "bottom" + }) + } + }, {}], + 54: [function(t, e, i) { + "use strict"; + var n = t(25), + a = t(45), + o = t(34); + e.exports = function(t) { + var e = { + position: "left", + ticks: { + callback: o.formatters.linear + } + }, + i = t.LinearScaleBase.extend({ + determineDataLimits: function() { + var t = this, + e = t.options, + i = t.chart, + n = i.data.datasets, + o = t.isHorizontal(); + + function r(e) { + return o ? e.xAxisID === t.id : e.yAxisID === t.id + } + t.min = null, t.max = null; + var s = e.stacked; + if (void 0 === s && a.each(n, function(t, e) { + if (!s) { + var n = i.getDatasetMeta(e); + i.isDatasetVisible(e) && r(n) && void 0 !== n.stack && (s = !0) + } + }), e.stacked || s) { + var l = {}; + a.each(n, function(n, o) { + var s = i.getDatasetMeta(o), + u = [s.type, void 0 === e.stacked && void 0 === s.stack ? o : "", s.stack].join("."); + void 0 === l[u] && (l[u] = { + positiveValues: [], + negativeValues: [] + }); + var d = l[u].positiveValues, + c = l[u].negativeValues; + i.isDatasetVisible(o) && r(s) && a.each(n.data, function(i, n) { + var a = +t.getRightValue(i); + isNaN(a) || s.data[n].hidden || (d[n] = d[n] || 0, c[n] = c[n] || 0, e.relativePoints ? d[n] = 100 : a < 0 ? c[n] += a : d[n] += a) + }) + }), a.each(l, function(e) { + var i = e.positiveValues.concat(e.negativeValues), + n = a.min(i), + o = a.max(i); + t.min = null === t.min ? n : Math.min(t.min, n), t.max = null === t.max ? o : Math.max(t.max, o) + }) + } else a.each(n, function(e, n) { + var o = i.getDatasetMeta(n); + i.isDatasetVisible(n) && r(o) && a.each(e.data, function(e, i) { + var n = +t.getRightValue(e); + isNaN(n) || o.data[i].hidden || (null === t.min ? t.min = n : n < t.min && (t.min = n), null === t.max ? t.max = n : n > t.max && (t.max = n)) + }) + }); + t.min = isFinite(t.min) && !isNaN(t.min) ? t.min : 0, t.max = isFinite(t.max) && !isNaN(t.max) ? t.max : 1, this.handleTickRangeOptions() + }, + getTickLimit: function() { + var t, e = this.options.ticks; + if (this.isHorizontal()) t = Math.min(e.maxTicksLimit ? e.maxTicksLimit : 11, Math.ceil(this.width / 50)); + else { + var i = a.valueOrDefault(e.fontSize, n.global.defaultFontSize); + t = Math.min(e.maxTicksLimit ? e.maxTicksLimit : 11, Math.ceil(this.height / (2 * i))) + } + return t + }, + handleDirectionalChanges: function() { + this.isHorizontal() || this.ticks.reverse() + }, + getLabelForIndex: function(t, e) { + return +this.getRightValue(this.chart.data.datasets[e].data[t]) + }, + getPixelForValue: function(t) { + var e = this, + i = e.start, + n = +e.getRightValue(t), + a = e.end - i; + return e.isHorizontal() ? e.left + e.width / a * (n - i) : e.bottom - e.height / a * (n - i) + }, + getValueForPixel: function(t) { + var e = this, + i = e.isHorizontal(), + n = i ? e.width : e.height, + a = (i ? t - e.left : e.bottom - t) / n; + return e.start + (e.end - e.start) * a + }, + getPixelForTick: function(t) { + return this.getPixelForValue(this.ticksAsNumbers[t]) + } + }); + t.scaleService.registerScaleType("linear", i, e) + } + }, { + 25: 25, + 34: 34, + 45: 45 + }], + 55: [function(t, e, i) { + "use strict"; + var n = t(45); + e.exports = function(t) { + var e = n.noop; + t.LinearScaleBase = t.Scale.extend({ + getRightValue: function(e) { + return "string" == typeof e ? +e : t.Scale.prototype.getRightValue.call(this, e) + }, + handleTickRangeOptions: function() { + var t = this, + e = t.options.ticks; + if (e.beginAtZero) { + var i = n.sign(t.min), + a = n.sign(t.max); + i < 0 && a < 0 ? t.max = 0 : i > 0 && a > 0 && (t.min = 0) + } + var o = void 0 !== e.min || void 0 !== e.suggestedMin, + r = void 0 !== e.max || void 0 !== e.suggestedMax; + void 0 !== e.min ? t.min = e.min : void 0 !== e.suggestedMin && (null === t.min ? t.min = e.suggestedMin : t.min = Math.min(t.min, e.suggestedMin)), void 0 !== e.max ? t.max = e.max : void 0 !== e.suggestedMax && (null === t.max ? t.max = e.suggestedMax : t.max = Math.max(t.max, e.suggestedMax)), o !== r && t.min >= t.max && (o ? t.max = t.min + 1 : t.min = t.max - 1), t.min === t.max && (t.max++, e.beginAtZero || t.min--) + }, + getTickLimit: e, + handleDirectionalChanges: e, + buildTicks: function() { + var t = this, + e = t.options.ticks, + i = t.getTickLimit(), + a = { + maxTicks: i = Math.max(2, i), + min: e.min, + max: e.max, + stepSize: n.valueOrDefault(e.fixedStepSize, e.stepSize) + }, + o = t.ticks = function(t, e) { + var i, a = []; + if (t.stepSize && t.stepSize > 0) i = t.stepSize; + else { + var o = n.niceNum(e.max - e.min, !1); + i = n.niceNum(o / (t.maxTicks - 1), !0) + } + var r = Math.floor(e.min / i) * i, + s = Math.ceil(e.max / i) * i; + t.min && t.max && t.stepSize && n.almostWhole((t.max - t.min) / t.stepSize, i / 1e3) && (r = t.min, s = t.max); + var l = (s - r) / i; + l = n.almostEquals(l, Math.round(l), i / 1e3) ? Math.round(l) : Math.ceil(l); + var u = 1; + i < 1 && (u = Math.pow(10, i.toString().length - 2), r = Math.round(r * u) / u, s = Math.round(s * u) / u), a.push(void 0 !== t.min ? t.min : r); + for (var d = 1; d < l; ++d) a.push(Math.round((r + d * i) * u) / u); + return a.push(void 0 !== t.max ? t.max : s), a + }(a, t); + t.handleDirectionalChanges(), t.max = n.max(o), t.min = n.min(o), e.reverse ? (o.reverse(), t.start = t.max, t.end = t.min) : (t.start = t.min, t.end = t.max) + }, + convertTicksToLabels: function() { + var e = this; + e.ticksAsNumbers = e.ticks.slice(), e.zeroLineIndex = e.ticks.indexOf(0), t.Scale.prototype.convertTicksToLabels.call(e) + } + }) + } + }, { + 45: 45 + }], + 56: [function(t, e, i) { + "use strict"; + var n = t(45), + a = t(34); + e.exports = function(t) { + var e = { + position: "left", + ticks: { + callback: a.formatters.logarithmic + } + }, + i = t.Scale.extend({ + determineDataLimits: function() { + var t = this, + e = t.options, + i = t.chart, + a = i.data.datasets, + o = t.isHorizontal(); + + function r(e) { + return o ? e.xAxisID === t.id : e.yAxisID === t.id + } + t.min = null, t.max = null, t.minNotZero = null; + var s = e.stacked; + if (void 0 === s && n.each(a, function(t, e) { + if (!s) { + var n = i.getDatasetMeta(e); + i.isDatasetVisible(e) && r(n) && void 0 !== n.stack && (s = !0) + } + }), e.stacked || s) { + var l = {}; + n.each(a, function(a, o) { + var s = i.getDatasetMeta(o), + u = [s.type, void 0 === e.stacked && void 0 === s.stack ? o : "", s.stack].join("."); + i.isDatasetVisible(o) && r(s) && (void 0 === l[u] && (l[u] = []), n.each(a.data, function(e, i) { + var n = l[u], + a = +t.getRightValue(e); + isNaN(a) || s.data[i].hidden || a < 0 || (n[i] = n[i] || 0, n[i] += a) + })) + }), n.each(l, function(e) { + if (e.length > 0) { + var i = n.min(e), + a = n.max(e); + t.min = null === t.min ? i : Math.min(t.min, i), t.max = null === t.max ? a : Math.max(t.max, a) + } + }) + } else n.each(a, function(e, a) { + var o = i.getDatasetMeta(a); + i.isDatasetVisible(a) && r(o) && n.each(e.data, function(e, i) { + var n = +t.getRightValue(e); + isNaN(n) || o.data[i].hidden || n < 0 || (null === t.min ? t.min = n : n < t.min && (t.min = n), null === t.max ? t.max = n : n > t.max && (t.max = n), 0 !== n && (null === t.minNotZero || n < t.minNotZero) && (t.minNotZero = n)) + }) + }); + this.handleTickRangeOptions() + }, + handleTickRangeOptions: function() { + var t = this, + e = t.options.ticks, + i = n.valueOrDefault; + t.min = i(e.min, t.min), t.max = i(e.max, t.max), t.min === t.max && (0 !== t.min && null !== t.min ? (t.min = Math.pow(10, Math.floor(n.log10(t.min)) - 1), t.max = Math.pow(10, Math.floor(n.log10(t.max)) + 1)) : (t.min = 1, t.max = 10)), null === t.min && (t.min = Math.pow(10, Math.floor(n.log10(t.max)) - 1)), null === t.max && (t.max = 0 !== t.min ? Math.pow(10, Math.floor(n.log10(t.min)) + 1) : 10), null === t.minNotZero && (t.min > 0 ? t.minNotZero = t.min : t.max < 1 ? t.minNotZero = Math.pow(10, Math.floor(n.log10(t.max))) : t.minNotZero = 1) + }, + buildTicks: function() { + var t = this, + e = t.options.ticks, + i = !t.isHorizontal(), + a = { + min: e.min, + max: e.max + }, + o = t.ticks = function(t, e) { + var i, a, o = [], + r = n.valueOrDefault, + s = r(t.min, Math.pow(10, Math.floor(n.log10(e.min)))), + l = Math.floor(n.log10(e.max)), + u = Math.ceil(e.max / Math.pow(10, l)); + 0 === s ? (i = Math.floor(n.log10(e.minNotZero)), a = Math.floor(e.minNotZero / Math.pow(10, i)), o.push(s), s = a * Math.pow(10, i)) : (i = Math.floor(n.log10(s)), a = Math.floor(s / Math.pow(10, i))); + for (var d = i < 0 ? Math.pow(10, Math.abs(i)) : 1; o.push(s), 10 == ++a && (a = 1, d = ++i >= 0 ? 1 : d), s = Math.round(a * Math.pow(10, i) * d) / d, i < l || i === l && a < u;); + var c = r(t.max, s); + return o.push(c), o + }(a, t); + t.max = n.max(o), t.min = n.min(o), e.reverse ? (i = !i, t.start = t.max, t.end = t.min) : (t.start = t.min, t.end = t.max), i && o.reverse() + }, + convertTicksToLabels: function() { + this.tickValues = this.ticks.slice(), t.Scale.prototype.convertTicksToLabels.call(this) + }, + getLabelForIndex: function(t, e) { + return +this.getRightValue(this.chart.data.datasets[e].data[t]) + }, + getPixelForTick: function(t) { + return this.getPixelForValue(this.tickValues[t]) + }, + _getFirstTickValue: function(t) { + var e = Math.floor(n.log10(t)); + return Math.floor(t / Math.pow(10, e)) * Math.pow(10, e) + }, + getPixelForValue: function(e) { + var i, a, o, r, s, l = this, + u = l.options.ticks.reverse, + d = n.log10, + c = l._getFirstTickValue(l.minNotZero), + h = 0; + return e = +l.getRightValue(e), u ? (o = l.end, r = l.start, s = -1) : (o = l.start, r = l.end, s = 1), l.isHorizontal() ? (i = l.width, a = u ? l.right : l.left) : (i = l.height, s *= -1, a = u ? l.top : l.bottom), e !== o && (0 === o && (i -= h = n.getValueOrDefault(l.options.ticks.fontSize, t.defaults.global.defaultFontSize), o = c), 0 !== e && (h += i / (d(r) - d(o)) * (d(e) - d(o))), a += s * h), a + }, + getValueForPixel: function(e) { + var i, a, o, r, s = this, + l = s.options.ticks.reverse, + u = n.log10, + d = s._getFirstTickValue(s.minNotZero); + if (l ? (a = s.end, o = s.start) : (a = s.start, o = s.end), s.isHorizontal() ? (i = s.width, r = l ? s.right - e : e - s.left) : (i = s.height, r = l ? e - s.top : s.bottom - e), r !== a) { + if (0 === a) { + var c = n.getValueOrDefault(s.options.ticks.fontSize, t.defaults.global.defaultFontSize); + r -= c, i -= c, a = d + } + r *= u(o) - u(a), r /= i, r = Math.pow(10, u(a) + r) + } + return r + } + }); + t.scaleService.registerScaleType("logarithmic", i, e) + } + }, { + 34: 34, + 45: 45 + }], + 57: [function(t, e, i) { + "use strict"; + var n = t(25), + a = t(45), + o = t(34); + e.exports = function(t) { + var e = n.global, + i = { + display: !0, + animate: !0, + position: "chartArea", + angleLines: { + display: !0, + color: "rgba(0, 0, 0, 0.1)", + lineWidth: 1 + }, + gridLines: { + circular: !1 + }, + ticks: { + showLabelBackdrop: !0, + backdropColor: "rgba(255,255,255,0.75)", + backdropPaddingY: 2, + backdropPaddingX: 2, + callback: o.formatters.linear + }, + pointLabels: { + display: !0, + fontSize: 10, + callback: function(t) { + return t + } + } + }; + + function r(t) { + var e = t.options; + return e.angleLines.display || e.pointLabels.display ? t.chart.data.labels.length : 0 + } + + function s(t) { + var i = t.options.pointLabels, + n = a.valueOrDefault(i.fontSize, e.defaultFontSize), + o = a.valueOrDefault(i.fontStyle, e.defaultFontStyle), + r = a.valueOrDefault(i.fontFamily, e.defaultFontFamily); + return { + size: n, + style: o, + family: r, + font: a.fontString(n, o, r) + } + } + + function l(t, e, i, n, a) { + return t === n || t === a ? { + start: e - i / 2, + end: e + i / 2 + } : t < n || t > a ? { + start: e - i - 5, + end: e + } : { + start: e, + end: e + i + 5 + } + } + + function u(t, e, i, n) { + if (a.isArray(e)) + for (var o = i.y, r = 1.5 * n, s = 0; s < e.length; ++s) t.fillText(e[s], i.x, o), o += r; + else t.fillText(e, i.x, i.y) + } + + function d(t) { + return a.isNumber(t) ? t : 0 + } + var c = t.LinearScaleBase.extend({ + setDimensions: function() { + var t = this, + i = t.options, + n = i.ticks; + t.width = t.maxWidth, t.height = t.maxHeight, t.xCenter = Math.round(t.width / 2), t.yCenter = Math.round(t.height / 2); + var o = a.min([t.height, t.width]), + r = a.valueOrDefault(n.fontSize, e.defaultFontSize); + t.drawingArea = i.display ? o / 2 - (r / 2 + n.backdropPaddingY) : o / 2 + }, + determineDataLimits: function() { + var t = this, + e = t.chart, + i = Number.POSITIVE_INFINITY, + n = Number.NEGATIVE_INFINITY; + a.each(e.data.datasets, function(o, r) { + if (e.isDatasetVisible(r)) { + var s = e.getDatasetMeta(r); + a.each(o.data, function(e, a) { + var o = +t.getRightValue(e); + isNaN(o) || s.data[a].hidden || (i = Math.min(o, i), n = Math.max(o, n)) + }) + } + }), t.min = i === Number.POSITIVE_INFINITY ? 0 : i, t.max = n === Number.NEGATIVE_INFINITY ? 0 : n, t.handleTickRangeOptions() + }, + getTickLimit: function() { + var t = this.options.ticks, + i = a.valueOrDefault(t.fontSize, e.defaultFontSize); + return Math.min(t.maxTicksLimit ? t.maxTicksLimit : 11, Math.ceil(this.drawingArea / (1.5 * i))) + }, + convertTicksToLabels: function() { + var e = this; + t.LinearScaleBase.prototype.convertTicksToLabels.call(e), e.pointLabels = e.chart.data.labels.map(e.options.pointLabels.callback, e) + }, + getLabelForIndex: function(t, e) { + return +this.getRightValue(this.chart.data.datasets[e].data[t]) + }, + fit: function() { + var t, e; + this.options.pointLabels.display ? function(t) { + var e, i, n, o = s(t), + u = Math.min(t.height / 2, t.width / 2), + d = { + r: t.width, + l: 0, + t: t.height, + b: 0 + }, + c = {}; + t.ctx.font = o.font, t._pointLabelSizes = []; + var h, f, g, p = r(t); + for (e = 0; e < p; e++) { + n = t.getPointPosition(e, u), h = t.ctx, f = o.size, g = t.pointLabels[e] || "", i = a.isArray(g) ? { + w: a.longestText(h, h.font, g), + h: g.length * f + 1.5 * (g.length - 1) * f + } : { + w: h.measureText(g).width, + h: f + }, t._pointLabelSizes[e] = i; + var m = t.getIndexAngle(e), + v = a.toDegrees(m) % 360, + b = l(v, n.x, i.w, 0, 180), + x = l(v, n.y, i.h, 90, 270); + b.start < d.l && (d.l = b.start, c.l = m), b.end > d.r && (d.r = b.end, c.r = m), x.start < d.t && (d.t = x.start, c.t = m), x.end > d.b && (d.b = x.end, c.b = m) + } + t.setReductions(u, d, c) + }(this) : (t = this, e = Math.min(t.height / 2, t.width / 2), t.drawingArea = Math.round(e), t.setCenterPoint(0, 0, 0, 0)) + }, + setReductions: function(t, e, i) { + var n = e.l / Math.sin(i.l), + a = Math.max(e.r - this.width, 0) / Math.sin(i.r), + o = -e.t / Math.cos(i.t), + r = -Math.max(e.b - this.height, 0) / Math.cos(i.b); + n = d(n), a = d(a), o = d(o), r = d(r), this.drawingArea = Math.min(Math.round(t - (n + a) / 2), Math.round(t - (o + r) / 2)), this.setCenterPoint(n, a, o, r) + }, + setCenterPoint: function(t, e, i, n) { + var a = this, + o = a.width - e - a.drawingArea, + r = t + a.drawingArea, + s = i + a.drawingArea, + l = a.height - n - a.drawingArea; + a.xCenter = Math.round((r + o) / 2 + a.left), a.yCenter = Math.round((s + l) / 2 + a.top) + }, + getIndexAngle: function(t) { + return t * (2 * Math.PI / r(this)) + (this.chart.options && this.chart.options.startAngle ? this.chart.options.startAngle : 0) * Math.PI * 2 / 360 + }, + getDistanceFromCenterForValue: function(t) { + var e = this; + if (null === t) return 0; + var i = e.drawingArea / (e.max - e.min); + return e.options.ticks.reverse ? (e.max - t) * i : (t - e.min) * i + }, + getPointPosition: function(t, e) { + var i = this.getIndexAngle(t) - Math.PI / 2; + return { + x: Math.round(Math.cos(i) * e) + this.xCenter, + y: Math.round(Math.sin(i) * e) + this.yCenter + } + }, + getPointPositionForValue: function(t, e) { + return this.getPointPosition(t, this.getDistanceFromCenterForValue(e)) + }, + getBasePosition: function() { + var t = this.min, + e = this.max; + return this.getPointPositionForValue(0, this.beginAtZero ? 0 : t < 0 && e < 0 ? e : t > 0 && e > 0 ? t : 0) + }, + draw: function() { + var t = this, + i = t.options, + n = i.gridLines, + o = i.ticks, + l = a.valueOrDefault; + if (i.display) { + var d = t.ctx, + c = this.getIndexAngle(0), + h = l(o.fontSize, e.defaultFontSize), + f = l(o.fontStyle, e.defaultFontStyle), + g = l(o.fontFamily, e.defaultFontFamily), + p = a.fontString(h, f, g); + a.each(t.ticks, function(i, s) { + if (s > 0 || o.reverse) { + var u = t.getDistanceFromCenterForValue(t.ticksAsNumbers[s]); + if (n.display && 0 !== s && function(t, e, i, n) { + var o = t.ctx; + if (o.strokeStyle = a.valueAtIndexOrDefault(e.color, n - 1), o.lineWidth = a.valueAtIndexOrDefault(e.lineWidth, n - 1), t.options.gridLines.circular) o.beginPath(), o.arc(t.xCenter, t.yCenter, i, 0, 2 * Math.PI), o.closePath(), o.stroke(); + else { + var s = r(t); + if (0 === s) return; + o.beginPath(); + var l = t.getPointPosition(0, i); + o.moveTo(l.x, l.y); + for (var u = 1; u < s; u++) l = t.getPointPosition(u, i), o.lineTo(l.x, l.y); + o.closePath(), o.stroke() + } + }(t, n, u, s), o.display) { + var f = l(o.fontColor, e.defaultFontColor); + if (d.font = p, d.save(), d.translate(t.xCenter, t.yCenter), d.rotate(c), o.showLabelBackdrop) { + var g = d.measureText(i).width; + d.fillStyle = o.backdropColor, d.fillRect(-g / 2 - o.backdropPaddingX, -u - h / 2 - o.backdropPaddingY, g + 2 * o.backdropPaddingX, h + 2 * o.backdropPaddingY) + } + d.textAlign = "center", d.textBaseline = "middle", d.fillStyle = f, d.fillText(i, 0, -u), d.restore() + } + } + }), (i.angleLines.display || i.pointLabels.display) && function(t) { + var i = t.ctx, + n = t.options, + o = n.angleLines, + l = n.pointLabels; + i.lineWidth = o.lineWidth, i.strokeStyle = o.color; + var d, c, h, f, g = t.getDistanceFromCenterForValue(n.ticks.reverse ? t.min : t.max), + p = s(t); + i.textBaseline = "top"; + for (var m = r(t) - 1; m >= 0; m--) { + if (o.display) { + var v = t.getPointPosition(m, g); + i.beginPath(), i.moveTo(t.xCenter, t.yCenter), i.lineTo(v.x, v.y), i.stroke(), i.closePath() + } + if (l.display) { + var b = t.getPointPosition(m, g + 5), + x = a.valueAtIndexOrDefault(l.fontColor, m, e.defaultFontColor); + i.font = p.font, i.fillStyle = x; + var y = t.getIndexAngle(m), + k = a.toDegrees(y); + i.textAlign = 0 === (f = k) || 180 === f ? "center" : f < 180 ? "left" : "right", d = k, c = t._pointLabelSizes[m], h = b, 90 === d || 270 === d ? h.y -= c.h / 2 : (d > 270 || d < 90) && (h.y -= c.h), u(i, t.pointLabels[m] || "", b, p.size) + } + } + }(t) + } + } + }); + t.scaleService.registerScaleType("radialLinear", c, i) + } + }, { + 25: 25, + 34: 34, + 45: 45 + }], + 58: [function(t, e, i) { + "use strict"; + var n = t(1); + n = "function" == typeof n ? n : window.moment; + var a = t(25), + o = t(45), + r = Number.MIN_SAFE_INTEGER || -9007199254740991, + s = Number.MAX_SAFE_INTEGER || 9007199254740991, + l = { + millisecond: { + common: !0, + size: 1, + steps: [1, 2, 5, 10, 20, 50, 100, 250, 500] + }, + second: { + common: !0, + size: 1e3, + steps: [1, 2, 5, 10, 30] + }, + minute: { + common: !0, + size: 6e4, + steps: [1, 2, 5, 10, 30] + }, + hour: { + common: !0, + size: 36e5, + steps: [1, 2, 3, 6, 12] + }, + day: { + common: !0, + size: 864e5, + steps: [1, 2, 5] + }, + week: { + common: !1, + size: 6048e5, + steps: [1, 2, 3, 4] + }, + month: { + common: !0, + size: 2628e6, + steps: [1, 2, 3] + }, + quarter: { + common: !1, + size: 7884e6, + steps: [1, 2, 3, 4] + }, + year: { + common: !0, + size: 3154e7 + } + }, + u = Object.keys(l); + + function d(t, e) { + return t - e + } + + function c(t) { + var e, i, n, a = {}, + o = []; + for (e = 0, i = t.length; e < i; ++e) a[n = t[e]] || (a[n] = !0, o.push(n)); + return o + } + + function h(t, e, i, n) { + var a = function(t, e, i) { + for (var n, a, o, r = 0, s = t.length - 1; r >= 0 && r <= s;) { + if (a = t[(n = r + s >> 1) - 1] || null, o = t[n], !a) return { + lo: null, + hi: o + }; + if (o[e] < i) r = n + 1; + else { + if (!(a[e] > i)) return { + lo: a, + hi: o + }; + s = n - 1 + } + } + return { + lo: o, + hi: null + } + }(t, e, i), + o = a.lo ? a.hi ? a.lo : t[t.length - 2] : t[0], + r = a.lo ? a.hi ? a.hi : t[t.length - 1] : t[1], + s = r[e] - o[e], + l = s ? (i - o[e]) / s : 0, + u = (r[n] - o[n]) * l; + return o[n] + u + } + + function f(t, e) { + var i = e.parser, + a = e.parser || e.format; + return "function" == typeof i ? i(t) : "string" == typeof t && "string" == typeof a ? n(t, a) : (t instanceof n || (t = n(t)), t.isValid() ? t : "function" == typeof a ? a(t) : t) + } + + function g(t, e) { + if (o.isNullOrUndef(t)) return null; + var i = e.options.time, + n = f(e.getRightValue(t), i); + return n.isValid() ? (i.round && n.startOf(i.round), n.valueOf()) : null + } + + function p(t) { + for (var e = u.indexOf(t) + 1, i = u.length; e < i; ++e) + if (l[u[e]].common) return u[e] + } + + function m(t, e, i, a) { + var r, d = a.time, + c = d.unit || function(t, e, i, n) { + var a, o, r, d = u.length; + for (a = u.indexOf(t); a < d - 1; ++a) + if (r = (o = l[u[a]]).steps ? o.steps[o.steps.length - 1] : s, o.common && Math.ceil((i - e) / (r * o.size)) <= n) return u[a]; + return u[d - 1] + }(d.minUnit, t, e, i), + h = p(c), + f = o.valueOrDefault(d.stepSize, d.unitStepSize), + g = "week" === c && d.isoWeekday, + m = a.ticks.major.enabled, + v = l[c], + b = n(t), + x = n(e), + y = []; + for (f || (f = function(t, e, i, n) { + var a, o, r, s = e - t, + u = l[i], + d = u.size, + c = u.steps; + if (!c) return Math.ceil(s / (n * d)); + for (a = 0, o = c.length; a < o && (r = c[a], !(Math.ceil(s / (d * r)) <= n)); ++a); + return r + }(t, e, c, i)), g && (b = b.isoWeekday(g), x = x.isoWeekday(g)), b = b.startOf(g ? "day" : c), (x = x.startOf(g ? "day" : c)) < e && x.add(1, c), r = n(b), m && h && !g && !d.round && (r.startOf(h), r.add(~~((b - r) / (v.size * f)) * f, c)); r < x; r.add(f, c)) y.push(+r); + return y.push(+r), y + } + e.exports = function(t) { + var e = t.Scale.extend({ + initialize: function() { + if (!n) throw new Error("Chart.js - Moment.js could not be found! You must include it before Chart.js to use the time scale. Download at https://momentjs.com"); + this.mergeTicksOptions(), t.Scale.prototype.initialize.call(this) + }, + update: function() { + var e = this.options; + return e.time && e.time.format && console.warn("options.time.format is deprecated and replaced by options.time.parser."), t.Scale.prototype.update.apply(this, arguments) + }, + getRightValue: function(e) { + return e && void 0 !== e.t && (e = e.t), t.Scale.prototype.getRightValue.call(this, e) + }, + determineDataLimits: function() { + var t, e, i, a, l, u, h = this, + f = h.chart, + p = h.options.time, + m = p.unit || "day", + v = s, + b = r, + x = [], + y = [], + k = []; + for (t = 0, i = f.data.labels.length; t < i; ++t) k.push(g(f.data.labels[t], h)); + for (t = 0, i = (f.data.datasets || []).length; t < i; ++t) + if (f.isDatasetVisible(t)) + if (l = f.data.datasets[t].data, o.isObject(l[0])) + for (y[t] = [], e = 0, a = l.length; e < a; ++e) u = g(l[e], h), x.push(u), y[t][e] = u; + else x.push.apply(x, k), y[t] = k.slice(0); + else y[t] = []; + k.length && (k = c(k).sort(d), v = Math.min(v, k[0]), b = Math.max(b, k[k.length - 1])), x.length && (x = c(x).sort(d), v = Math.min(v, x[0]), b = Math.max(b, x[x.length - 1])), v = g(p.min, h) || v, b = g(p.max, h) || b, v = v === s ? +n().startOf(m) : v, b = b === r ? +n().endOf(m) + 1 : b, h.min = Math.min(v, b), h.max = Math.max(v + 1, b), h._horizontal = h.isHorizontal(), h._table = [], h._timestamps = { + data: x, + datasets: y, + labels: k + } + }, + buildTicks: function() { + var t, e, i, a, o, r, s, d, c, v, b, x, y = this, + k = y.min, + M = y.max, + w = y.options, + S = w.time, + C = [], + _ = []; + switch (w.ticks.source) { + case "data": + C = y._timestamps.data; + break; + case "labels": + C = y._timestamps.labels; + break; + case "auto": + default: + C = m(k, M, y.getLabelCapacity(k), w) + } + for ("ticks" === w.bounds && C.length && (k = C[0], M = C[C.length - 1]), k = g(S.min, y) || k, M = g(S.max, y) || M, t = 0, e = C.length; t < e; ++t)(i = C[t]) >= k && i <= M && _.push(i); + return y.min = k, y.max = M, y._unit = S.unit || function(t, e, i, a) { + var o, r, s = n.duration(n(a).diff(n(i))); + for (o = u.length - 1; o >= u.indexOf(e); o--) + if (r = u[o], l[r].common && s.as(r) >= t.length) return r; + return u[e ? u.indexOf(e) : 0] + }(_, S.minUnit, y.min, y.max), y._majorUnit = p(y._unit), y._table = function(t, e, i, n) { + if ("linear" === n || !t.length) return [{ + time: e, + pos: 0 + }, { + time: i, + pos: 1 + }]; + var a, o, r, s, l, u = [], + d = [e]; + for (a = 0, o = t.length; a < o; ++a)(s = t[a]) > e && s < i && d.push(s); + for (d.push(i), a = 0, o = d.length; a < o; ++a) l = d[a + 1], r = d[a - 1], s = d[a], void 0 !== r && void 0 !== l && Math.round((l + r) / 2) === s || u.push({ + time: s, + pos: a / (o - 1) + }); + return u + }(y._timestamps.data, k, M, w.distribution), y._offsets = (a = y._table, o = _, r = k, s = M, b = 0, x = 0, (d = w).offset && o.length && (d.time.min || (c = o.length > 1 ? o[1] : s, v = o[0], b = (h(a, "time", c, "pos") - h(a, "time", v, "pos")) / 2), d.time.max || (c = o[o.length - 1], v = o.length > 1 ? o[o.length - 2] : r, x = (h(a, "time", c, "pos") - h(a, "time", v, "pos")) / 2)), { + left: b, + right: x + }), y._labelFormat = function(t, e) { + var i, n, a, o = t.length; + for (i = 0; i < o; i++) { + if (0 !== (n = f(t[i], e)).millisecond()) return "MMM D, YYYY h:mm:ss.SSS a"; + 0 === n.second() && 0 === n.minute() && 0 === n.hour() || (a = !0) + } + return a ? "MMM D, YYYY h:mm:ss a" : "MMM D, YYYY" + }(y._timestamps.data, S), + function(t, e) { + var i, a, o, r, s = []; + for (i = 0, a = t.length; i < a; ++i) o = t[i], r = !!e && o === +n(o).startOf(e), s.push({ + value: o, + major: r + }); + return s + }(_, y._majorUnit) + }, + getLabelForIndex: function(t, e) { + var i = this.chart.data, + n = this.options.time, + a = i.labels && t < i.labels.length ? i.labels[t] : "", + r = i.datasets[e].data[t]; + return o.isObject(r) && (a = this.getRightValue(r)), n.tooltipFormat ? f(a, n).format(n.tooltipFormat) : "string" == typeof a ? a : f(a, n).format(this._labelFormat) + }, + tickFormatFunction: function(t, e, i, n) { + var a = this.options, + r = t.valueOf(), + s = a.time.displayFormats, + l = s[this._unit], + u = this._majorUnit, + d = s[u], + c = t.clone().startOf(u).valueOf(), + h = a.ticks.major, + f = h.enabled && u && d && r === c, + g = t.format(n || (f ? d : l)), + p = f ? h : a.ticks.minor, + m = o.valueOrDefault(p.callback, p.userCallback); + return m ? m(g, e, i) : g + }, + convertTicksToLabels: function(t) { + var e, i, a = []; + for (e = 0, i = t.length; e < i; ++e) a.push(this.tickFormatFunction(n(t[e].value), e, t)); + return a + }, + getPixelForOffset: function(t) { + var e = this, + i = e._horizontal ? e.width : e.height, + n = e._horizontal ? e.left : e.top, + a = h(e._table, "time", t, "pos"); + return n + i * (e._offsets.left + a) / (e._offsets.left + 1 + e._offsets.right) + }, + getPixelForValue: function(t, e, i) { + var n = null; + if (void 0 !== e && void 0 !== i && (n = this._timestamps.datasets[i][e]), null === n && (n = g(t, this)), null !== n) return this.getPixelForOffset(n) + }, + getPixelForTick: function(t) { + var e = this.getTicks(); + return t >= 0 && t < e.length ? this.getPixelForOffset(e[t].value) : null + }, + getValueForPixel: function(t) { + var e = this, + i = e._horizontal ? e.width : e.height, + a = e._horizontal ? e.left : e.top, + o = (i ? (t - a) / i : 0) * (e._offsets.left + 1 + e._offsets.left) - e._offsets.right, + r = h(e._table, "pos", o, "time"); + return n(r) + }, + getLabelWidth: function(t) { + var e = this.options.ticks, + i = this.ctx.measureText(t).width, + n = o.toRadians(e.maxRotation), + r = Math.cos(n), + s = Math.sin(n); + return i * r + o.valueOrDefault(e.fontSize, a.global.defaultFontSize) * s + }, + getLabelCapacity: function(t) { + var e = this, + i = e.options.time.displayFormats.millisecond, + a = e.tickFormatFunction(n(t), 0, [], i), + o = e.getLabelWidth(a), + r = e.isHorizontal() ? e.width : e.height, + s = Math.floor(r / o); + return s > 0 ? s : 1 + } + }); + t.scaleService.registerScaleType("time", e, { + position: "bottom", + distribution: "linear", + bounds: "data", + time: { + parser: !1, + format: !1, + unit: !1, + round: !1, + displayFormat: !1, + isoWeekday: !1, + minUnit: "millisecond", + displayFormats: { + millisecond: "h:mm:ss.SSS a", + second: "h:mm:ss a", + minute: "h:mm a", + hour: "hA", + day: "MMM D", + week: "ll", + month: "MMM YYYY", + quarter: "[Q]Q - YYYY", + year: "YYYY" + } + }, + ticks: { + autoSkip: !1, + source: "auto", + major: { + enabled: !1 + } + } + }) + } + }, { + 1: 1, + 25: 25, + 45: 45 + }] + }, {}, [7])(7) +}); \ No newline at end of file diff --git a/webroot/rsrc/js/application/timetracker/chart/jquery.min.js b/webroot/rsrc/js/application/timetracker/chart/jquery.min.js new file mode 100644 index 0000000000..a89655456a --- /dev/null +++ b/webroot/rsrc/js/application/timetracker/chart/jquery.min.js @@ -0,0 +1,7 @@ +/** + * @provides jquery.min + */ +!function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=c.slice,e=c.concat,f=c.push,g=c.indexOf,h={},i=h.toString,j=h.hasOwnProperty,k={},l="1.11.3",m=function(a,b){return new m.fn.init(a,b)},n=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,o=/^-ms-/,p=/-([\da-z])/gi,q=function(a,b){return b.toUpperCase()};m.fn=m.prototype={jquery:l,constructor:m,selector:"",length:0,toArray:function(){return d.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:d.call(this)},pushStack:function(a){var b=m.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a,b){return m.each(this,a,b)},map:function(a){return this.pushStack(m.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(d.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor(null)},push:f,sort:c.sort,splice:c.splice},m.extend=m.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||m.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(e=arguments[h]))for(d in e)a=g[d],c=e[d],g!==c&&(j&&c&&(m.isPlainObject(c)||(b=m.isArray(c)))?(b?(b=!1,f=a&&m.isArray(a)?a:[]):f=a&&m.isPlainObject(a)?a:{},g[d]=m.extend(j,f,c)):void 0!==c&&(g[d]=c));return g},m.extend({expando:"jQuery"+(l+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===m.type(a)},isArray:Array.isArray||function(a){return"array"===m.type(a)},isWindow:function(a){return null!=a&&a==a.window},isNumeric:function(a){return!m.isArray(a)&&a-parseFloat(a)+1>=0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},isPlainObject:function(a){var b;if(!a||"object"!==m.type(a)||a.nodeType||m.isWindow(a))return!1;try{if(a.constructor&&!j.call(a,"constructor")&&!j.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}if(k.ownLast)for(b in a)return j.call(a,b);for(b in a);return void 0===b||j.call(a,b)},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?h[i.call(a)]||"object":typeof a},globalEval:function(b){b&&m.trim(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(o,"ms-").replace(p,q)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b,c){var d,e=0,f=a.length,g=r(a);if(c){if(g){for(;f>e;e++)if(d=b.apply(a[e],c),d===!1)break}else for(e in a)if(d=b.apply(a[e],c),d===!1)break}else if(g){for(;f>e;e++)if(d=b.call(a[e],e,a[e]),d===!1)break}else for(e in a)if(d=b.call(a[e],e,a[e]),d===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(n,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(r(Object(a))?m.merge(c,"string"==typeof a?[a]:a):f.call(c,a)),c},inArray:function(a,b,c){var d;if(b){if(g)return g.call(b,a,c);for(d=b.length,c=c?0>c?Math.max(0,d+c):c:0;d>c;c++)if(c in b&&b[c]===a)return c}return-1},merge:function(a,b){var c=+b.length,d=0,e=a.length;while(c>d)a[e++]=b[d++];if(c!==c)while(void 0!==b[d])a[e++]=b[d++];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,f=0,g=a.length,h=r(a),i=[];if(h)for(;g>f;f++)d=b(a[f],f,c),null!=d&&i.push(d);else for(f in a)d=b(a[f],f,c),null!=d&&i.push(d);return e.apply([],i)},guid:1,proxy:function(a,b){var c,e,f;return"string"==typeof b&&(f=a[b],b=a,a=f),m.isFunction(a)?(c=d.call(arguments,2),e=function(){return a.apply(b||this,c.concat(d.call(arguments)))},e.guid=a.guid=a.guid||m.guid++,e):void 0},now:function(){return+new Date},support:k}),m.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(a,b){h["[object "+b+"]"]=b.toLowerCase()});function r(a){var b="length"in a&&a.length,c=m.type(a);return"function"===c||m.isWindow(a)?!1:1===a.nodeType&&b?!0:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var s=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ha(),z=ha(),A=ha(),B=function(a,b){return a===b&&(l=!0),0},C=1<<31,D={}.hasOwnProperty,E=[],F=E.pop,G=E.push,H=E.push,I=E.slice,J=function(a,b){for(var c=0,d=a.length;d>c;c++)if(a[c]===b)return c;return-1},K="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",L="[\\x20\\t\\r\\n\\f]",M="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",N=M.replace("w","w#"),O="\\["+L+"*("+M+")(?:"+L+"*([*^$|!~]?=)"+L+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+N+"))|)"+L+"*\\]",P=":("+M+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+O+")*)|.*)\\)|)",Q=new RegExp(L+"+","g"),R=new RegExp("^"+L+"+|((?:^|[^\\\\])(?:\\\\.)*)"+L+"+$","g"),S=new RegExp("^"+L+"*,"+L+"*"),T=new RegExp("^"+L+"*([>+~]|"+L+")"+L+"*"),U=new RegExp("="+L+"*([^\\]'\"]*?)"+L+"*\\]","g"),V=new RegExp(P),W=new RegExp("^"+N+"$"),X={ID:new RegExp("^#("+M+")"),CLASS:new RegExp("^\\.("+M+")"),TAG:new RegExp("^("+M.replace("w","w*")+")"),ATTR:new RegExp("^"+O),PSEUDO:new RegExp("^"+P),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+L+"*(even|odd|(([+-]|)(\\d*)n|)"+L+"*(?:([+-]|)"+L+"*(\\d+)|))"+L+"*\\)|)","i"),bool:new RegExp("^(?:"+K+")$","i"),needsContext:new RegExp("^"+L+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+L+"*((?:-\\d)?\\d*)"+L+"*\\)|)(?=[^-]|$)","i")},Y=/^(?:input|select|textarea|button)$/i,Z=/^h\d$/i,$=/^[^{]+\{\s*\[native \w/,_=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,aa=/[+~]/,ba=/'|\\/g,ca=new RegExp("\\\\([\\da-f]{1,6}"+L+"?|("+L+")|.)","ig"),da=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},ea=function(){m()};try{H.apply(E=I.call(v.childNodes),v.childNodes),E[v.childNodes.length].nodeType}catch(fa){H={apply:E.length?function(a,b){G.apply(a,I.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function ga(a,b,d,e){var f,h,j,k,l,o,r,s,w,x;if((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,d=d||[],k=b.nodeType,"string"!=typeof a||!a||1!==k&&9!==k&&11!==k)return d;if(!e&&p){if(11!==k&&(f=_.exec(a)))if(j=f[1]){if(9===k){if(h=b.getElementById(j),!h||!h.parentNode)return d;if(h.id===j)return d.push(h),d}else if(b.ownerDocument&&(h=b.ownerDocument.getElementById(j))&&t(b,h)&&h.id===j)return d.push(h),d}else{if(f[2])return H.apply(d,b.getElementsByTagName(a)),d;if((j=f[3])&&c.getElementsByClassName)return H.apply(d,b.getElementsByClassName(j)),d}if(c.qsa&&(!q||!q.test(a))){if(s=r=u,w=b,x=1!==k&&a,1===k&&"object"!==b.nodeName.toLowerCase()){o=g(a),(r=b.getAttribute("id"))?s=r.replace(ba,"\\$&"):b.setAttribute("id",s),s="[id='"+s+"'] ",l=o.length;while(l--)o[l]=s+ra(o[l]);w=aa.test(a)&&pa(b.parentNode)||b,x=o.join(",")}if(x)try{return H.apply(d,w.querySelectorAll(x)),d}catch(y){}finally{r||b.removeAttribute("id")}}}return i(a.replace(R,"$1"),b,d,e)}function ha(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ia(a){return a[u]=!0,a}function ja(a){var b=n.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ka(a,b){var c=a.split("|"),e=a.length;while(e--)d.attrHandle[c[e]]=b}function la(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||C)-(~a.sourceIndex||C);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function na(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function oa(a){return ia(function(b){return b=+b,ia(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function pa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=ga.support={},f=ga.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},m=ga.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=g.documentElement,e=g.defaultView,e&&e!==e.top&&(e.addEventListener?e.addEventListener("unload",ea,!1):e.attachEvent&&e.attachEvent("onunload",ea)),p=!f(g),c.attributes=ja(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ja(function(a){return a.appendChild(g.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=$.test(g.getElementsByClassName),c.getById=ja(function(a){return o.appendChild(a).id=u,!g.getElementsByName||!g.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c&&c.parentNode?[c]:[]}},d.filter.ID=function(a){var b=a.replace(ca,da);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(ca,da);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=$.test(g.querySelectorAll))&&(ja(function(a){o.appendChild(a).innerHTML="",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+L+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+L+"*(?:value|"+K+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ja(function(a){var b=g.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+L+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=$.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ja(function(a){c.disconnectedMatch=s.call(a,"div"),s.call(a,"[s!='']:x"),r.push("!=",P)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=$.test(o.compareDocumentPosition),t=b||$.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===g||a.ownerDocument===v&&t(v,a)?-1:b===g||b.ownerDocument===v&&t(v,b)?1:k?J(k,a)-J(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,h=[a],i=[b];if(!e||!f)return a===g?-1:b===g?1:e?-1:f?1:k?J(k,a)-J(k,b):0;if(e===f)return la(a,b);c=a;while(c=c.parentNode)h.unshift(c);c=b;while(c=c.parentNode)i.unshift(c);while(h[d]===i[d])d++;return d?la(h[d],i[d]):h[d]===v?-1:i[d]===v?1:0},g):n},ga.matches=function(a,b){return ga(a,null,null,b)},ga.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(U,"='$1']"),!(!c.matchesSelector||!p||r&&r.test(b)||q&&q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return ga(b,n,null,[a]).length>0},ga.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},ga.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&D.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},ga.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},ga.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=ga.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=ga.selectors={cacheLength:50,createPseudo:ia,match:X,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(ca,da),a[3]=(a[3]||a[4]||a[5]||"").replace(ca,da),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||ga.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&ga.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return X.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&V.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(ca,da).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+L+")"+a+"("+L+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=ga.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(Q," ")+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h;if(q){if(f){while(p){l=b;while(l=l[p])if(h?l.nodeName.toLowerCase()===r:1===l.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){k=q[u]||(q[u]={}),j=k[a]||[],n=j[0]===w&&j[1],m=j[0]===w&&j[2],l=n&&q.childNodes[n];while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if(1===l.nodeType&&++m&&l===b){k[a]=[w,n,m];break}}else if(s&&(j=(b[u]||(b[u]={}))[a])&&j[0]===w)m=j[1];else while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if((h?l.nodeName.toLowerCase()===r:1===l.nodeType)&&++m&&(s&&((l[u]||(l[u]={}))[a]=[w,m]),l===b))break;return m-=e,m===d||m%d===0&&m/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||ga.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ia(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=J(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ia(function(a){var b=[],c=[],d=h(a.replace(R,"$1"));return d[u]?ia(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ia(function(a){return function(b){return ga(a,b).length>0}}),contains:ia(function(a){return a=a.replace(ca,da),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ia(function(a){return W.test(a||"")||ga.error("unsupported lang: "+a),a=a.replace(ca,da).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Z.test(a.nodeName)},input:function(a){return Y.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:oa(function(){return[0]}),last:oa(function(a,b){return[b-1]}),eq:oa(function(a,b,c){return[0>c?c+b:c]}),even:oa(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:oa(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:oa(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:oa(function(a,b,c){for(var d=0>c?c+b:c;++db;b++)d+=a[b].value;return d}function sa(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(i=b[u]||(b[u]={}),(h=i[d])&&h[0]===w&&h[1]===f)return j[2]=h[2];if(i[d]=j,j[2]=a(b,c,g))return!0}}}function ta(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function ua(a,b,c){for(var d=0,e=b.length;e>d;d++)ga(a,b[d],c);return c}function va(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(!c||c(f,d,e))&&(g.push(f),j&&b.push(h));return g}function wa(a,b,c,d,e,f){return d&&!d[u]&&(d=wa(d)),e&&!e[u]&&(e=wa(e,f)),ia(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||ua(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:va(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=va(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?J(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=va(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):H.apply(g,r)})}function xa(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=sa(function(a){return a===b},h,!0),l=sa(function(a){return J(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];f>i;i++)if(c=d.relative[a[i].type])m=[sa(ta(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return wa(i>1&&ta(m),i>1&&ra(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(R,"$1"),c,e>i&&xa(a.slice(i,e)),f>e&&xa(a=a.slice(e)),f>e&&ra(a))}m.push(c)}return ta(m)}function ya(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,m,o,p=0,q="0",r=f&&[],s=[],t=j,u=f||e&&d.find.TAG("*",k),v=w+=null==t?1:Math.random()||.1,x=u.length;for(k&&(j=g!==n&&g);q!==x&&null!=(l=u[q]);q++){if(e&&l){m=0;while(o=a[m++])if(o(l,g,h)){i.push(l);break}k&&(w=v)}c&&((l=!o&&l)&&p--,f&&r.push(l))}if(p+=q,c&&q!==p){m=0;while(o=b[m++])o(r,s,g,h);if(f){if(p>0)while(q--)r[q]||s[q]||(s[q]=F.call(i));s=va(s)}H.apply(i,s),k&&!f&&s.length>0&&p+b.length>1&&ga.uniqueSort(i)}return k&&(w=v,j=t),r};return c?ia(f):f}return h=ga.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=xa(b[c]),f[u]?d.push(f):e.push(f);f=A(a,ya(e,d)),f.selector=a}return f},i=ga.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(ca,da),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=X.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(ca,da),aa.test(j[0].type)&&pa(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&ra(j),!a)return H.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,aa.test(a)&&pa(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ja(function(a){return 1&a.compareDocumentPosition(n.createElement("div"))}),ja(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||ka("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ja(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ka("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),ja(function(a){return null==a.getAttribute("disabled")})||ka(K,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),ga}(a);m.find=s,m.expr=s.selectors,m.expr[":"]=m.expr.pseudos,m.unique=s.uniqueSort,m.text=s.getText,m.isXMLDoc=s.isXML,m.contains=s.contains;var t=m.expr.match.needsContext,u=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,v=/^.[^:#\[\.,]*$/;function w(a,b,c){if(m.isFunction(b))return m.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return m.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(v.test(b))return m.filter(b,a,c);b=m.filter(b,a)}return m.grep(a,function(a){return m.inArray(a,b)>=0!==c})}m.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?m.find.matchesSelector(d,a)?[d]:[]:m.find.matches(a,m.grep(b,function(a){return 1===a.nodeType}))},m.fn.extend({find:function(a){var b,c=[],d=this,e=d.length;if("string"!=typeof a)return this.pushStack(m(a).filter(function(){for(b=0;e>b;b++)if(m.contains(d[b],this))return!0}));for(b=0;e>b;b++)m.find(a,d[b],c);return c=this.pushStack(e>1?m.unique(c):c),c.selector=this.selector?this.selector+" "+a:a,c},filter:function(a){return this.pushStack(w(this,a||[],!1))},not:function(a){return this.pushStack(w(this,a||[],!0))},is:function(a){return!!w(this,"string"==typeof a&&t.test(a)?m(a):a||[],!1).length}});var x,y=a.document,z=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,A=m.fn.init=function(a,b){var c,d;if(!a)return this;if("string"==typeof a){if(c="<"===a.charAt(0)&&">"===a.charAt(a.length-1)&&a.length>=3?[null,a,null]:z.exec(a),!c||!c[1]&&b)return!b||b.jquery?(b||x).find(a):this.constructor(b).find(a);if(c[1]){if(b=b instanceof m?b[0]:b,m.merge(this,m.parseHTML(c[1],b&&b.nodeType?b.ownerDocument||b:y,!0)),u.test(c[1])&&m.isPlainObject(b))for(c in b)m.isFunction(this[c])?this[c](b[c]):this.attr(c,b[c]);return this}if(d=y.getElementById(c[2]),d&&d.parentNode){if(d.id!==c[2])return x.find(a);this.length=1,this[0]=d}return this.context=y,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):m.isFunction(a)?"undefined"!=typeof x.ready?x.ready(a):a(m):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),m.makeArray(a,this))};A.prototype=m.fn,x=m(y);var B=/^(?:parents|prev(?:Until|All))/,C={children:!0,contents:!0,next:!0,prev:!0};m.extend({dir:function(a,b,c){var d=[],e=a[b];while(e&&9!==e.nodeType&&(void 0===c||1!==e.nodeType||!m(e).is(c)))1===e.nodeType&&d.push(e),e=e[b];return d},sibling:function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c}}),m.fn.extend({has:function(a){var b,c=m(a,this),d=c.length;return this.filter(function(){for(b=0;d>b;b++)if(m.contains(this,c[b]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=t.test(a)||"string"!=typeof a?m(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&m.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?m.unique(f):f)},index:function(a){return a?"string"==typeof a?m.inArray(this[0],m(a)):m.inArray(a.jquery?a[0]:a,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(m.unique(m.merge(this.get(),m(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function D(a,b){do a=a[b];while(a&&1!==a.nodeType);return a}m.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return m.dir(a,"parentNode")},parentsUntil:function(a,b,c){return m.dir(a,"parentNode",c)},next:function(a){return D(a,"nextSibling")},prev:function(a){return D(a,"previousSibling")},nextAll:function(a){return m.dir(a,"nextSibling")},prevAll:function(a){return m.dir(a,"previousSibling")},nextUntil:function(a,b,c){return m.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return m.dir(a,"previousSibling",c)},siblings:function(a){return m.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return m.sibling(a.firstChild)},contents:function(a){return m.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:m.merge([],a.childNodes)}},function(a,b){m.fn[a]=function(c,d){var e=m.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=m.filter(d,e)),this.length>1&&(C[a]||(e=m.unique(e)),B.test(a)&&(e=e.reverse())),this.pushStack(e)}});var E=/\S+/g,F={};function G(a){var b=F[a]={};return m.each(a.match(E)||[],function(a,c){b[c]=!0}),b}m.Callbacks=function(a){a="string"==typeof a?F[a]||G(a):m.extend({},a);var b,c,d,e,f,g,h=[],i=!a.once&&[],j=function(l){for(c=a.memory&&l,d=!0,f=g||0,g=0,e=h.length,b=!0;h&&e>f;f++)if(h[f].apply(l[0],l[1])===!1&&a.stopOnFalse){c=!1;break}b=!1,h&&(i?i.length&&j(i.shift()):c?h=[]:k.disable())},k={add:function(){if(h){var d=h.length;!function f(b){m.each(b,function(b,c){var d=m.type(c);"function"===d?a.unique&&k.has(c)||h.push(c):c&&c.length&&"string"!==d&&f(c)})}(arguments),b?e=h.length:c&&(g=d,j(c))}return this},remove:function(){return h&&m.each(arguments,function(a,c){var d;while((d=m.inArray(c,h,d))>-1)h.splice(d,1),b&&(e>=d&&e--,f>=d&&f--)}),this},has:function(a){return a?m.inArray(a,h)>-1:!(!h||!h.length)},empty:function(){return h=[],e=0,this},disable:function(){return h=i=c=void 0,this},disabled:function(){return!h},lock:function(){return i=void 0,c||k.disable(),this},locked:function(){return!i},fireWith:function(a,c){return!h||d&&!i||(c=c||[],c=[a,c.slice?c.slice():c],b?i.push(c):j(c)),this},fire:function(){return k.fireWith(this,arguments),this},fired:function(){return!!d}};return k},m.extend({Deferred:function(a){var b=[["resolve","done",m.Callbacks("once memory"),"resolved"],["reject","fail",m.Callbacks("once memory"),"rejected"],["notify","progress",m.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return m.Deferred(function(c){m.each(b,function(b,f){var g=m.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&m.isFunction(a.promise)?a.promise().done(c.resolve).fail(c.reject).progress(c.notify):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?m.extend(a,d):d}},e={};return d.pipe=d.then,m.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=d.call(arguments),e=c.length,f=1!==e||a&&m.isFunction(a.promise)?e:0,g=1===f?a:m.Deferred(),h=function(a,b,c){return function(e){b[a]=this,c[a]=arguments.length>1?d.call(arguments):e,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(e>1)for(i=new Array(e),j=new Array(e),k=new Array(e);e>b;b++)c[b]&&m.isFunction(c[b].promise)?c[b].promise().done(h(b,k,c)).fail(g.reject).progress(h(b,j,i)):--f;return f||g.resolveWith(k,c),g.promise()}});var H;m.fn.ready=function(a){return m.ready.promise().done(a),this},m.extend({isReady:!1,readyWait:1,holdReady:function(a){a?m.readyWait++:m.ready(!0)},ready:function(a){if(a===!0?!--m.readyWait:!m.isReady){if(!y.body)return setTimeout(m.ready);m.isReady=!0,a!==!0&&--m.readyWait>0||(H.resolveWith(y,[m]),m.fn.triggerHandler&&(m(y).triggerHandler("ready"),m(y).off("ready")))}}});function I(){y.addEventListener?(y.removeEventListener("DOMContentLoaded",J,!1),a.removeEventListener("load",J,!1)):(y.detachEvent("onreadystatechange",J),a.detachEvent("onload",J))}function J(){(y.addEventListener||"load"===event.type||"complete"===y.readyState)&&(I(),m.ready())}m.ready.promise=function(b){if(!H)if(H=m.Deferred(),"complete"===y.readyState)setTimeout(m.ready);else if(y.addEventListener)y.addEventListener("DOMContentLoaded",J,!1),a.addEventListener("load",J,!1);else{y.attachEvent("onreadystatechange",J),a.attachEvent("onload",J);var c=!1;try{c=null==a.frameElement&&y.documentElement}catch(d){}c&&c.doScroll&&!function e(){if(!m.isReady){try{c.doScroll("left")}catch(a){return setTimeout(e,50)}I(),m.ready()}}()}return H.promise(b)};var K="undefined",L;for(L in m(k))break;k.ownLast="0"!==L,k.inlineBlockNeedsLayout=!1,m(function(){var a,b,c,d;c=y.getElementsByTagName("body")[0],c&&c.style&&(b=y.createElement("div"),d=y.createElement("div"),d.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",c.appendChild(d).appendChild(b),typeof b.style.zoom!==K&&(b.style.cssText="display:inline;margin:0;border:0;padding:1px;width:1px;zoom:1",k.inlineBlockNeedsLayout=a=3===b.offsetWidth,a&&(c.style.zoom=1)),c.removeChild(d))}),function(){var a=y.createElement("div");if(null==k.deleteExpando){k.deleteExpando=!0;try{delete a.test}catch(b){k.deleteExpando=!1}}a=null}(),m.acceptData=function(a){var b=m.noData[(a.nodeName+" ").toLowerCase()],c=+a.nodeType||1;return 1!==c&&9!==c?!1:!b||b!==!0&&a.getAttribute("classid")===b};var M=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,N=/([A-Z])/g;function O(a,b,c){if(void 0===c&&1===a.nodeType){var d="data-"+b.replace(N,"-$1").toLowerCase();if(c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:M.test(c)?m.parseJSON(c):c}catch(e){}m.data(a,b,c)}else c=void 0}return c}function P(a){var b;for(b in a)if(("data"!==b||!m.isEmptyObject(a[b]))&&"toJSON"!==b)return!1; + +return!0}function Q(a,b,d,e){if(m.acceptData(a)){var f,g,h=m.expando,i=a.nodeType,j=i?m.cache:a,k=i?a[h]:a[h]&&h;if(k&&j[k]&&(e||j[k].data)||void 0!==d||"string"!=typeof b)return k||(k=i?a[h]=c.pop()||m.guid++:h),j[k]||(j[k]=i?{}:{toJSON:m.noop}),("object"==typeof b||"function"==typeof b)&&(e?j[k]=m.extend(j[k],b):j[k].data=m.extend(j[k].data,b)),g=j[k],e||(g.data||(g.data={}),g=g.data),void 0!==d&&(g[m.camelCase(b)]=d),"string"==typeof b?(f=g[b],null==f&&(f=g[m.camelCase(b)])):f=g,f}}function R(a,b,c){if(m.acceptData(a)){var d,e,f=a.nodeType,g=f?m.cache:a,h=f?a[m.expando]:m.expando;if(g[h]){if(b&&(d=c?g[h]:g[h].data)){m.isArray(b)?b=b.concat(m.map(b,m.camelCase)):b in d?b=[b]:(b=m.camelCase(b),b=b in d?[b]:b.split(" ")),e=b.length;while(e--)delete d[b[e]];if(c?!P(d):!m.isEmptyObject(d))return}(c||(delete g[h].data,P(g[h])))&&(f?m.cleanData([a],!0):k.deleteExpando||g!=g.window?delete g[h]:g[h]=null)}}}m.extend({cache:{},noData:{"applet ":!0,"embed ":!0,"object ":"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(a){return a=a.nodeType?m.cache[a[m.expando]]:a[m.expando],!!a&&!P(a)},data:function(a,b,c){return Q(a,b,c)},removeData:function(a,b){return R(a,b)},_data:function(a,b,c){return Q(a,b,c,!0)},_removeData:function(a,b){return R(a,b,!0)}}),m.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=m.data(f),1===f.nodeType&&!m._data(f,"parsedAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=m.camelCase(d.slice(5)),O(f,d,e[d])));m._data(f,"parsedAttrs",!0)}return e}return"object"==typeof a?this.each(function(){m.data(this,a)}):arguments.length>1?this.each(function(){m.data(this,a,b)}):f?O(f,a,m.data(f,a)):void 0},removeData:function(a){return this.each(function(){m.removeData(this,a)})}}),m.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=m._data(a,b),c&&(!d||m.isArray(c)?d=m._data(a,b,m.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=m.queue(a,b),d=c.length,e=c.shift(),f=m._queueHooks(a,b),g=function(){m.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return m._data(a,c)||m._data(a,c,{empty:m.Callbacks("once memory").add(function(){m._removeData(a,b+"queue"),m._removeData(a,c)})})}}),m.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.lengthh;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f},W=/^(?:checkbox|radio)$/i;!function(){var a=y.createElement("input"),b=y.createElement("div"),c=y.createDocumentFragment();if(b.innerHTML="
a",k.leadingWhitespace=3===b.firstChild.nodeType,k.tbody=!b.getElementsByTagName("tbody").length,k.htmlSerialize=!!b.getElementsByTagName("link").length,k.html5Clone="<:nav>"!==y.createElement("nav").cloneNode(!0).outerHTML,a.type="checkbox",a.checked=!0,c.appendChild(a),k.appendChecked=a.checked,b.innerHTML="",k.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue,c.appendChild(b),b.innerHTML="",k.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,k.noCloneEvent=!0,b.attachEvent&&(b.attachEvent("onclick",function(){k.noCloneEvent=!1}),b.cloneNode(!0).click()),null==k.deleteExpando){k.deleteExpando=!0;try{delete b.test}catch(d){k.deleteExpando=!1}}}(),function(){var b,c,d=y.createElement("div");for(b in{submit:!0,change:!0,focusin:!0})c="on"+b,(k[b+"Bubbles"]=c in a)||(d.setAttribute(c,"t"),k[b+"Bubbles"]=d.attributes[c].expando===!1);d=null}();var X=/^(?:input|select|textarea)$/i,Y=/^key/,Z=/^(?:mouse|pointer|contextmenu)|click/,$=/^(?:focusinfocus|focusoutblur)$/,_=/^([^.]*)(?:\.(.+)|)$/;function aa(){return!0}function ba(){return!1}function ca(){try{return y.activeElement}catch(a){}}m.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,n,o,p,q,r=m._data(a);if(r){c.handler&&(i=c,c=i.handler,e=i.selector),c.guid||(c.guid=m.guid++),(g=r.events)||(g=r.events={}),(k=r.handle)||(k=r.handle=function(a){return typeof m===K||a&&m.event.triggered===a.type?void 0:m.event.dispatch.apply(k.elem,arguments)},k.elem=a),b=(b||"").match(E)||[""],h=b.length;while(h--)f=_.exec(b[h])||[],o=q=f[1],p=(f[2]||"").split(".").sort(),o&&(j=m.event.special[o]||{},o=(e?j.delegateType:j.bindType)||o,j=m.event.special[o]||{},l=m.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&m.expr.match.needsContext.test(e),namespace:p.join(".")},i),(n=g[o])||(n=g[o]=[],n.delegateCount=0,j.setup&&j.setup.call(a,d,p,k)!==!1||(a.addEventListener?a.addEventListener(o,k,!1):a.attachEvent&&a.attachEvent("on"+o,k))),j.add&&(j.add.call(a,l),l.handler.guid||(l.handler.guid=c.guid)),e?n.splice(n.delegateCount++,0,l):n.push(l),m.event.global[o]=!0);a=null}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,n,o,p,q,r=m.hasData(a)&&m._data(a);if(r&&(k=r.events)){b=(b||"").match(E)||[""],j=b.length;while(j--)if(h=_.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=m.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,n=k[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),i=f=n.length;while(f--)g=n[f],!e&&q!==g.origType||c&&c.guid!==g.guid||h&&!h.test(g.namespace)||d&&d!==g.selector&&("**"!==d||!g.selector)||(n.splice(f,1),g.selector&&n.delegateCount--,l.remove&&l.remove.call(a,g));i&&!n.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||m.removeEvent(a,o,r.handle),delete k[o])}else for(o in k)m.event.remove(a,o+b[j],c,d,!0);m.isEmptyObject(k)&&(delete r.handle,m._removeData(a,"events"))}},trigger:function(b,c,d,e){var f,g,h,i,k,l,n,o=[d||y],p=j.call(b,"type")?b.type:b,q=j.call(b,"namespace")?b.namespace.split("."):[];if(h=l=d=d||y,3!==d.nodeType&&8!==d.nodeType&&!$.test(p+m.event.triggered)&&(p.indexOf(".")>=0&&(q=p.split("."),p=q.shift(),q.sort()),g=p.indexOf(":")<0&&"on"+p,b=b[m.expando]?b:new m.Event(p,"object"==typeof b&&b),b.isTrigger=e?2:3,b.namespace=q.join("."),b.namespace_re=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=d),c=null==c?[b]:m.makeArray(c,[b]),k=m.event.special[p]||{},e||!k.trigger||k.trigger.apply(d,c)!==!1)){if(!e&&!k.noBubble&&!m.isWindow(d)){for(i=k.delegateType||p,$.test(i+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),l=h;l===(d.ownerDocument||y)&&o.push(l.defaultView||l.parentWindow||a)}n=0;while((h=o[n++])&&!b.isPropagationStopped())b.type=n>1?i:k.bindType||p,f=(m._data(h,"events")||{})[b.type]&&m._data(h,"handle"),f&&f.apply(h,c),f=g&&h[g],f&&f.apply&&m.acceptData(h)&&(b.result=f.apply(h,c),b.result===!1&&b.preventDefault());if(b.type=p,!e&&!b.isDefaultPrevented()&&(!k._default||k._default.apply(o.pop(),c)===!1)&&m.acceptData(d)&&g&&d[p]&&!m.isWindow(d)){l=d[g],l&&(d[g]=null),m.event.triggered=p;try{d[p]()}catch(r){}m.event.triggered=void 0,l&&(d[g]=l)}return b.result}},dispatch:function(a){a=m.event.fix(a);var b,c,e,f,g,h=[],i=d.call(arguments),j=(m._data(this,"events")||{})[a.type]||[],k=m.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=m.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,g=0;while((e=f.handlers[g++])&&!a.isImmediatePropagationStopped())(!a.namespace_re||a.namespace_re.test(e.namespace))&&(a.handleObj=e,a.data=e.data,c=((m.event.special[e.origType]||{}).handle||e.handler).apply(f.elem,i),void 0!==c&&(a.result=c)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&(!a.button||"click"!==a.type))for(;i!=this;i=i.parentNode||this)if(1===i.nodeType&&(i.disabled!==!0||"click"!==a.type)){for(e=[],f=0;h>f;f++)d=b[f],c=d.selector+" ",void 0===e[c]&&(e[c]=d.needsContext?m(c,this).index(i)>=0:m.find(c,this,null,[i]).length),e[c]&&e.push(d);e.length&&g.push({elem:i,handlers:e})}return h]","i"),ha=/^\s+/,ia=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,ja=/<([\w:]+)/,ka=/\s*$/g,ra={option:[1,""],legend:[1,"
","
"],area:[1,"",""],param:[1,"",""],thead:[1,"","
"],tr:[2,"","
"],col:[2,"","
"],td:[3,"","
"],_default:k.htmlSerialize?[0,"",""]:[1,"X
","
"]},sa=da(y),ta=sa.appendChild(y.createElement("div"));ra.optgroup=ra.option,ra.tbody=ra.tfoot=ra.colgroup=ra.caption=ra.thead,ra.th=ra.td;function ua(a,b){var c,d,e=0,f=typeof a.getElementsByTagName!==K?a.getElementsByTagName(b||"*"):typeof a.querySelectorAll!==K?a.querySelectorAll(b||"*"):void 0;if(!f)for(f=[],c=a.childNodes||a;null!=(d=c[e]);e++)!b||m.nodeName(d,b)?f.push(d):m.merge(f,ua(d,b));return void 0===b||b&&m.nodeName(a,b)?m.merge([a],f):f}function va(a){W.test(a.type)&&(a.defaultChecked=a.checked)}function wa(a,b){return m.nodeName(a,"table")&&m.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function xa(a){return a.type=(null!==m.find.attr(a,"type"))+"/"+a.type,a}function ya(a){var b=pa.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function za(a,b){for(var c,d=0;null!=(c=a[d]);d++)m._data(c,"globalEval",!b||m._data(b[d],"globalEval"))}function Aa(a,b){if(1===b.nodeType&&m.hasData(a)){var c,d,e,f=m._data(a),g=m._data(b,f),h=f.events;if(h){delete g.handle,g.events={};for(c in h)for(d=0,e=h[c].length;e>d;d++)m.event.add(b,c,h[c][d])}g.data&&(g.data=m.extend({},g.data))}}function Ba(a,b){var c,d,e;if(1===b.nodeType){if(c=b.nodeName.toLowerCase(),!k.noCloneEvent&&b[m.expando]){e=m._data(b);for(d in e.events)m.removeEvent(b,d,e.handle);b.removeAttribute(m.expando)}"script"===c&&b.text!==a.text?(xa(b).text=a.text,ya(b)):"object"===c?(b.parentNode&&(b.outerHTML=a.outerHTML),k.html5Clone&&a.innerHTML&&!m.trim(b.innerHTML)&&(b.innerHTML=a.innerHTML)):"input"===c&&W.test(a.type)?(b.defaultChecked=b.checked=a.checked,b.value!==a.value&&(b.value=a.value)):"option"===c?b.defaultSelected=b.selected=a.defaultSelected:("input"===c||"textarea"===c)&&(b.defaultValue=a.defaultValue)}}m.extend({clone:function(a,b,c){var d,e,f,g,h,i=m.contains(a.ownerDocument,a);if(k.html5Clone||m.isXMLDoc(a)||!ga.test("<"+a.nodeName+">")?f=a.cloneNode(!0):(ta.innerHTML=a.outerHTML,ta.removeChild(f=ta.firstChild)),!(k.noCloneEvent&&k.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||m.isXMLDoc(a)))for(d=ua(f),h=ua(a),g=0;null!=(e=h[g]);++g)d[g]&&Ba(e,d[g]);if(b)if(c)for(h=h||ua(a),d=d||ua(f),g=0;null!=(e=h[g]);g++)Aa(e,d[g]);else Aa(a,f);return d=ua(f,"script"),d.length>0&&za(d,!i&&ua(a,"script")),d=h=e=null,f},buildFragment:function(a,b,c,d){for(var e,f,g,h,i,j,l,n=a.length,o=da(b),p=[],q=0;n>q;q++)if(f=a[q],f||0===f)if("object"===m.type(f))m.merge(p,f.nodeType?[f]:f);else if(la.test(f)){h=h||o.appendChild(b.createElement("div")),i=(ja.exec(f)||["",""])[1].toLowerCase(),l=ra[i]||ra._default,h.innerHTML=l[1]+f.replace(ia,"<$1>")+l[2],e=l[0];while(e--)h=h.lastChild;if(!k.leadingWhitespace&&ha.test(f)&&p.push(b.createTextNode(ha.exec(f)[0])),!k.tbody){f="table"!==i||ka.test(f)?""!==l[1]||ka.test(f)?0:h:h.firstChild,e=f&&f.childNodes.length;while(e--)m.nodeName(j=f.childNodes[e],"tbody")&&!j.childNodes.length&&f.removeChild(j)}m.merge(p,h.childNodes),h.textContent="";while(h.firstChild)h.removeChild(h.firstChild);h=o.lastChild}else p.push(b.createTextNode(f));h&&o.removeChild(h),k.appendChecked||m.grep(ua(p,"input"),va),q=0;while(f=p[q++])if((!d||-1===m.inArray(f,d))&&(g=m.contains(f.ownerDocument,f),h=ua(o.appendChild(f),"script"),g&&za(h),c)){e=0;while(f=h[e++])oa.test(f.type||"")&&c.push(f)}return h=null,o},cleanData:function(a,b){for(var d,e,f,g,h=0,i=m.expando,j=m.cache,l=k.deleteExpando,n=m.event.special;null!=(d=a[h]);h++)if((b||m.acceptData(d))&&(f=d[i],g=f&&j[f])){if(g.events)for(e in g.events)n[e]?m.event.remove(d,e):m.removeEvent(d,e,g.handle);j[f]&&(delete j[f],l?delete d[i]:typeof d.removeAttribute!==K?d.removeAttribute(i):d[i]=null,c.push(f))}}}),m.fn.extend({text:function(a){return V(this,function(a){return void 0===a?m.text(this):this.empty().append((this[0]&&this[0].ownerDocument||y).createTextNode(a))},null,a,arguments.length)},append:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=wa(this,a);b.appendChild(a)}})},prepend:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=wa(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},remove:function(a,b){for(var c,d=a?m.filter(a,this):this,e=0;null!=(c=d[e]);e++)b||1!==c.nodeType||m.cleanData(ua(c)),c.parentNode&&(b&&m.contains(c.ownerDocument,c)&&za(ua(c,"script")),c.parentNode.removeChild(c));return this},empty:function(){for(var a,b=0;null!=(a=this[b]);b++){1===a.nodeType&&m.cleanData(ua(a,!1));while(a.firstChild)a.removeChild(a.firstChild);a.options&&m.nodeName(a,"select")&&(a.options.length=0)}return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return m.clone(this,a,b)})},html:function(a){return V(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a)return 1===b.nodeType?b.innerHTML.replace(fa,""):void 0;if(!("string"!=typeof a||ma.test(a)||!k.htmlSerialize&&ga.test(a)||!k.leadingWhitespace&&ha.test(a)||ra[(ja.exec(a)||["",""])[1].toLowerCase()])){a=a.replace(ia,"<$1>");try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(m.cleanData(ua(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=arguments[0];return this.domManip(arguments,function(b){a=this.parentNode,m.cleanData(ua(this)),a&&a.replaceChild(b,this)}),a&&(a.length||a.nodeType)?this:this.remove()},detach:function(a){return this.remove(a,!0)},domManip:function(a,b){a=e.apply([],a);var c,d,f,g,h,i,j=0,l=this.length,n=this,o=l-1,p=a[0],q=m.isFunction(p);if(q||l>1&&"string"==typeof p&&!k.checkClone&&na.test(p))return this.each(function(c){var d=n.eq(c);q&&(a[0]=p.call(this,c,d.html())),d.domManip(a,b)});if(l&&(i=m.buildFragment(a,this[0].ownerDocument,!1,this),c=i.firstChild,1===i.childNodes.length&&(i=c),c)){for(g=m.map(ua(i,"script"),xa),f=g.length;l>j;j++)d=i,j!==o&&(d=m.clone(d,!0,!0),f&&m.merge(g,ua(d,"script"))),b.call(this[j],d,j);if(f)for(h=g[g.length-1].ownerDocument,m.map(g,ya),j=0;f>j;j++)d=g[j],oa.test(d.type||"")&&!m._data(d,"globalEval")&&m.contains(h,d)&&(d.src?m._evalUrl&&m._evalUrl(d.src):m.globalEval((d.text||d.textContent||d.innerHTML||"").replace(qa,"")));i=c=null}return this}}),m.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){m.fn[a]=function(a){for(var c,d=0,e=[],g=m(a),h=g.length-1;h>=d;d++)c=d===h?this:this.clone(!0),m(g[d])[b](c),f.apply(e,c.get());return this.pushStack(e)}});var Ca,Da={};function Ea(b,c){var d,e=m(c.createElement(b)).appendTo(c.body),f=a.getDefaultComputedStyle&&(d=a.getDefaultComputedStyle(e[0]))?d.display:m.css(e[0],"display");return e.detach(),f}function Fa(a){var b=y,c=Da[a];return c||(c=Ea(a,b),"none"!==c&&c||(Ca=(Ca||m("