From a5971b465d1252eafe74b1e0a652bb57293f0a3d Mon Sep 17 00:00:00 2001 From: Shkiv Date: Tue, 24 Aug 2021 11:57:39 +0300 Subject: [PATCH 01/62] Add Docker support --- Dockerfile | 126 ++++++++++++++++++++++++++++++++++++++++++ Dockerfile.aphlict | 133 +++++++++++++++++++++++++++++++++++++++++++++ Dockerfile.daemon | 119 ++++++++++++++++++++++++++++++++++++++++ docker-compose.yml | 34 ++++++++++++ 4 files changed, 412 insertions(+) create mode 100644 Dockerfile create mode 100644 Dockerfile.aphlict create mode 100644 Dockerfile.daemon create mode 100644 docker-compose.yml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000..9629abdfdf --- /dev/null +++ b/Dockerfile @@ -0,0 +1,126 @@ +##### Start Phabricator +FROM php:7.3-apache-buster +##### End Phabricator + +LABEL org.opencontainers.image.source https://github.com/phabricator-docker/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 \ + git \ + mercurial \ + subversion \ + ca-certificates \ + # @see https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=944908 + python3-pkg-resources \ + python3-pygments \ + imagemagick \ + # @see https://secure.phabricator.com/w/guides/dependencies/ + # provides ssh-keygen and ssh, these are needed to sync ssh repositories + openssh-client \ + procps \ + && 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 \ + libjpeg62-turbo-dev \ + libpng-dev \ + libfreetype6-dev \ + libzip-dev \ + ; \ + \ + docker-php-ext-configure gd \ + --with-jpeg-dir=/usr \ + --with-png-dir=/usr \ + --with-freetype-dir=/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 + +##### Start Phabricator +RUN { \ + echo ''; \ + echo ' RewriteEngine on'; \ + echo ' RewriteRule ^(.*)$ /index.php?__path__=$1 [B,L,QSA]'; \ + echo ''; \ + } > /etc/apache2/sites-available/000-default.conf +##### End Phabricator + +# COPY ./ /opt/phabricator +RUN git clone https://github.com/moonmana-games/phabricator /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/Dockerfile.aphlict b/Dockerfile.aphlict new file mode 100644 index 0000000000..736b7231f8 --- /dev/null +++ b/Dockerfile.aphlict @@ -0,0 +1,133 @@ +##### Start Aphlict +FROM php:7.3-cli-buster +##### End Aphlict + +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 \ + git \ + mercurial \ + subversion \ + ca-certificates \ + # @see https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=944908 + python3-pkg-resources \ + python3-pygments \ + imagemagick \ + # @see https://secure.phabricator.com/w/guides/dependencies/ + # provides ssh-keygen and ssh, these are needed to sync ssh repositories + openssh-client \ + procps \ + && rm -rf /var/lib/apt/lists/* + +# install the PHP extensions we need +RUN set -ex; \ + \ + if command -v a2enmod; then \ + a2enmod rewrite; \ + fi; \ + \ + savedAptMark="$(apt-mark showmanual)"; \ + \ + apt-get update; \ + apt-get install -y --no-install-recommends \ + libcurl4-gnutls-dev \ + libjpeg62-turbo-dev \ + libpng-dev \ + libfreetype6-dev \ + libzip-dev \ + ; \ + \ + docker-php-ext-configure gd \ + --with-jpeg-dir=/usr \ + --with-png-dir=/usr \ + --with-freetype-dir=/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 + +##### Start Aphlict +RUN mkdir -p /var/log \ + && touch /var/log/aphlict.log \ + && chown www-data:www-data /var/log/aphlict.log + +EXPOSE 22280 +EXPOSE 22281 + +COPY --from=node:lts-buster /usr/local/bin/node /usr/local/bin/node + +COPY --from=node:lts-buster /usr/local/lib/node_modules /usr/local/lib/node_modules + +RUN ln -s ../lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm; \ + ln -s ../lib/node_modules/npm/bin/npx-cli.js /usr/local/bin/npx; +##### End Aphlict + +COPY ./ /opt + +WORKDIR /opt/phabricator + +ENV PATH "$PATH:/opt/phabricator/bin" + +##### Start Aphlict +RUN npm install --prefix /opt/phabricator/support/aphlict/server ws + +USER www-data + +CMD aphlict debug +##### End Aphlict diff --git a/Dockerfile.daemon b/Dockerfile.daemon new file mode 100644 index 0000000000..0a8477b71d --- /dev/null +++ b/Dockerfile.daemon @@ -0,0 +1,119 @@ +##### Start Daemon +FROM php:7.3-cli-buster +##### End Daemon + +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 \ + git \ + mercurial \ + subversion \ + ca-certificates \ + # @see https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=944908 + python3-pkg-resources \ + python3-pygments \ + imagemagick \ + # @see https://secure.phabricator.com/w/guides/dependencies/ + # provides ssh-keygen and ssh, these are needed to sync ssh repositories + openssh-client \ + procps \ + && rm -rf /var/lib/apt/lists/* + +# install the PHP extensions we need +RUN set -ex; \ + \ + if command -v a2enmod; then \ + a2enmod rewrite; \ + fi; \ + \ + savedAptMark="$(apt-mark showmanual)"; \ + \ + apt-get update; \ + apt-get install -y --no-install-recommends \ + libcurl4-gnutls-dev \ + libjpeg62-turbo-dev \ + libpng-dev \ + libfreetype6-dev \ + libzip-dev \ + ; \ + \ + docker-php-ext-configure gd \ + --with-jpeg-dir=/usr \ + --with-png-dir=/usr \ + --with-freetype-dir=/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 + +##### Start Daemon +RUN mkdir -p /var/tmp/phd/log/ \ + && touch /var/tmp/phd/log/daemons.log +##### End Daemon + +COPY ./ /opt + +WORKDIR /opt/phabricator + +ENV PATH "$PATH:/opt/phabricator/bin" + +##### Start Daemon +CMD phd start \ + && tail -f /var/tmp/phd/log/daemons.log +##### End Daemon diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000000..527315ca9d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,34 @@ +version: '3' +services: + phabricator: + build: ./ + image: phabricator/phabricator + volumes: + - config:/opt/phabricator/conf/local + - repo:/var/repo + ports: + - 8888:80 + links: + - database + daemon: + build: + context: ./ + dockerfile: Dockerfile.daemon + image: phabricator/daemon + volumes: + - config:/opt/phabricator/conf/local + - repo:/var/repo + links: + - database + database: + image: mariadb:10.2 + volumes: + - db-data:/var/lib/mysql + ports: + - 3306:3306 + environment: + MYSQL_ROOT_PASSWORD: CHANGEME +volumes: + config: + repo: + db-data: From 6b377a2df404d752a05c871b4c0828ceef5880e7 Mon Sep 17 00:00:00 2001 From: Shkiv Date: Tue, 24 Aug 2021 12:50:30 +0300 Subject: [PATCH 02/62] +1 --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 9629abdfdf..833dbccff5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ FROM php:7.3-apache-buster ##### End Phabricator -LABEL org.opencontainers.image.source https://github.com/phabricator-docker/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 From 0729b32f9302bf5b42d2b7676704149cac1f652f Mon Sep 17 00:00:00 2001 From: Shkiv Date: Tue, 24 Aug 2021 13:56:15 +0300 Subject: [PATCH 03/62] Fix paths --- Dockerfile | 4 ++-- Dockerfile.aphlict | 4 +++- Dockerfile.daemon | 4 +++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index 833dbccff5..65af73a830 100644 --- a/Dockerfile +++ b/Dockerfile @@ -112,9 +112,9 @@ RUN { \ } > /etc/apache2/sites-available/000-default.conf ##### End Phabricator -# COPY ./ /opt/phabricator -RUN git clone https://github.com/moonmana-games/phabricator /opt/phabricator +# Clone phabricator RUN git clone https://github.com/phacility/arcanist /opt/arcanist +RUN git clone https://github.com/moonmana-games/phabricator /opt/phabricator WORKDIR /opt/phabricator diff --git a/Dockerfile.aphlict b/Dockerfile.aphlict index 736b7231f8..7300bcd19e 100644 --- a/Dockerfile.aphlict +++ b/Dockerfile.aphlict @@ -118,7 +118,9 @@ RUN ln -s ../lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm; \ ln -s ../lib/node_modules/npm/bin/npx-cli.js /usr/local/bin/npx; ##### End Aphlict -COPY ./ /opt +# Clone phabricator +RUN git clone https://github.com/phacility/arcanist /opt/arcanist +RUN git clone https://github.com/moonmana-games/phabricator /opt/phabricator WORKDIR /opt/phabricator diff --git a/Dockerfile.daemon b/Dockerfile.daemon index 0a8477b71d..81a643502a 100644 --- a/Dockerfile.daemon +++ b/Dockerfile.daemon @@ -107,7 +107,9 @@ RUN mkdir -p /var/tmp/phd/log/ \ && touch /var/tmp/phd/log/daemons.log ##### End Daemon -COPY ./ /opt +# Clone phabricator +RUN git clone https://github.com/phacility/arcanist /opt/arcanist +RUN git clone https://github.com/moonmana-games/phabricator /opt/phabricator WORKDIR /opt/phabricator From 480a07ff2004d22542c16e603b05a91dd3727038 Mon Sep 17 00:00:00 2001 From: Shkiv Date: Tue, 24 Aug 2021 20:47:32 +0300 Subject: [PATCH 04/62] Use local sources for dev --- .gitattributes | 2 ++ Dockerfile | 3 ++- Dockerfile.aphlict | 3 ++- Dockerfile.daemon | 3 ++- 4 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..1312090792 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +* text=auto +*.sh text eol=lf \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 65af73a830..0585649727 100644 --- a/Dockerfile +++ b/Dockerfile @@ -114,7 +114,8 @@ RUN { \ # Clone phabricator RUN git clone https://github.com/phacility/arcanist /opt/arcanist -RUN git clone https://github.com/moonmana-games/phabricator /opt/phabricator +# RUN git clone https://github.com/moonmana-games/phabricator /opt/phabricator +COPY ./ /opt/phabricator WORKDIR /opt/phabricator diff --git a/Dockerfile.aphlict b/Dockerfile.aphlict index 7300bcd19e..ba4db7c411 100644 --- a/Dockerfile.aphlict +++ b/Dockerfile.aphlict @@ -120,7 +120,8 @@ RUN ln -s ../lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm; \ # Clone phabricator RUN git clone https://github.com/phacility/arcanist /opt/arcanist -RUN git clone https://github.com/moonmana-games/phabricator /opt/phabricator +# RUN git clone https://github.com/moonmana-games/phabricator /opt/phabricator +COPY ./ /opt/phabricator WORKDIR /opt/phabricator diff --git a/Dockerfile.daemon b/Dockerfile.daemon index 81a643502a..a7e634d550 100644 --- a/Dockerfile.daemon +++ b/Dockerfile.daemon @@ -109,7 +109,8 @@ RUN mkdir -p /var/tmp/phd/log/ \ # Clone phabricator RUN git clone https://github.com/phacility/arcanist /opt/arcanist -RUN git clone https://github.com/moonmana-games/phabricator /opt/phabricator +# RUN git clone https://github.com/moonmana-games/phabricator /opt/phabricator +COPY ./ /opt/phabricator WORKDIR /opt/phabricator From 72ae91e600e19bc467cfa7776e2edf6f8aacaceb Mon Sep 17 00:00:00 2001 From: Shkiv Date: Thu, 26 Aug 2021 15:50:13 +0300 Subject: [PATCH 05/62] Move local sources to compose --- .gitattributes | 2 +- Dockerfile | 2 -- Dockerfile.aphlict | 2 -- Dockerfile.daemon | 2 -- docker-compose.yml | 9 +++++++++ 5 files changed, 10 insertions(+), 7 deletions(-) diff --git a/.gitattributes b/.gitattributes index 1312090792..7e51847ac3 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,2 @@ * text=auto -*.sh text eol=lf \ No newline at end of file +*.php text eol=lf \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 0585649727..7014b252d5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -114,8 +114,6 @@ RUN { \ # Clone phabricator RUN git clone https://github.com/phacility/arcanist /opt/arcanist -# RUN git clone https://github.com/moonmana-games/phabricator /opt/phabricator -COPY ./ /opt/phabricator WORKDIR /opt/phabricator diff --git a/Dockerfile.aphlict b/Dockerfile.aphlict index ba4db7c411..513266e0e6 100644 --- a/Dockerfile.aphlict +++ b/Dockerfile.aphlict @@ -120,8 +120,6 @@ RUN ln -s ../lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm; \ # Clone phabricator RUN git clone https://github.com/phacility/arcanist /opt/arcanist -# RUN git clone https://github.com/moonmana-games/phabricator /opt/phabricator -COPY ./ /opt/phabricator WORKDIR /opt/phabricator diff --git a/Dockerfile.daemon b/Dockerfile.daemon index a7e634d550..8b3b5d29a6 100644 --- a/Dockerfile.daemon +++ b/Dockerfile.daemon @@ -109,8 +109,6 @@ RUN mkdir -p /var/tmp/phd/log/ \ # Clone phabricator RUN git clone https://github.com/phacility/arcanist /opt/arcanist -# RUN git clone https://github.com/moonmana-games/phabricator /opt/phabricator -COPY ./ /opt/phabricator WORKDIR /opt/phabricator diff --git a/docker-compose.yml b/docker-compose.yml index 527315ca9d..3dc58364bc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,6 +6,7 @@ services: volumes: - config:/opt/phabricator/conf/local - repo:/var/repo + - .:/opt/phabricator ports: - 8888:80 links: @@ -18,6 +19,7 @@ services: volumes: - config:/opt/phabricator/conf/local - repo:/var/repo + - .:/opt/phabricator links: - database database: @@ -28,6 +30,13 @@ services: - 3306:3306 environment: MYSQL_ROOT_PASSWORD: CHANGEME + phpmyadmin: + image: phpmyadmin + restart: always + ports: + - 8080:80 + environment: + - PMA_ARBITRARY=1 volumes: config: repo: From cfa161934d0da7dc301133b32e319efe05a46245 Mon Sep 17 00:00:00 2001 From: Aleksandr Maydaniuk Date: Sun, 31 Oct 2021 23:13:43 +0200 Subject: [PATCH 06/62] Change: Roles, Documents Removed the check for the presence of text in the description when creating Documents. Added "Roles" application. Added display of role to user profile. --- resources/builtin/roles/fa-android.png | Bin 0 -> 1663 bytes resources/builtin/roles/fa-apple.png | Bin 0 -> 1492 bytes resources/builtin/roles/fa-beer.png | Bin 0 -> 719 bytes resources/builtin/roles/fa-bomb.png | Bin 0 -> 1876 bytes resources/builtin/roles/fa-book.png | Bin 0 -> 1774 bytes resources/builtin/roles/fa-briefcase.png | Bin 0 -> 721 bytes resources/builtin/roles/fa-bug.png | Bin 0 -> 1577 bytes resources/builtin/roles/fa-building.png | Bin 0 -> 1088 bytes resources/builtin/roles/fa-calendar.png | Bin 0 -> 954 bytes resources/builtin/roles/fa-camera-retro.png | Bin 0 -> 1477 bytes resources/builtin/roles/fa-chrome.png | Bin 0 -> 2667 bytes resources/builtin/roles/fa-cloud.png | Bin 0 -> 1083 bytes resources/builtin/roles/fa-coffee.png | Bin 0 -> 958 bytes resources/builtin/roles/fa-comments.png | Bin 0 -> 1739 bytes resources/builtin/roles/fa-credit-card.png | Bin 0 -> 643 bytes resources/builtin/roles/fa-database.png | Bin 0 -> 2146 bytes resources/builtin/roles/fa-desktop.png | Bin 0 -> 760 bytes resources/builtin/roles/fa-diamond.png | Bin 0 -> 2065 bytes resources/builtin/roles/fa-empire.png | Bin 0 -> 4598 bytes resources/builtin/roles/fa-envelope.png | Bin 0 -> 1446 bytes resources/builtin/roles/fa-facebook.png | Bin 0 -> 1109 bytes resources/builtin/roles/fa-fax.png | Bin 0 -> 1304 bytes resources/builtin/roles/fa-film.png | Bin 0 -> 1276 bytes resources/builtin/roles/fa-firefox.png | Bin 0 -> 2300 bytes resources/builtin/roles/fa-flag-checkered.png | Bin 0 -> 1896 bytes resources/builtin/roles/fa-flask.png | Bin 0 -> 1223 bytes resources/builtin/roles/fa-folder.png | Bin 0 -> 752 bytes resources/builtin/roles/fa-gamepad.png | Bin 0 -> 1375 bytes resources/builtin/roles/fa-gears.png | Bin 0 -> 2691 bytes resources/builtin/roles/fa-google.png | Bin 0 -> 1851 bytes resources/builtin/roles/fa-hand-peace-o.png | Bin 0 -> 2680 bytes resources/builtin/roles/fa-hashtag.png | Bin 0 -> 1195 bytes resources/builtin/roles/fa-heart.png | Bin 0 -> 1343 bytes .../builtin/roles/fa-internet-explorer.png | Bin 0 -> 2479 bytes resources/builtin/roles/fa-key.png | Bin 0 -> 1792 bytes resources/builtin/roles/fa-legal.png | Bin 0 -> 1133 bytes resources/builtin/roles/fa-linux.png | Bin 0 -> 3424 bytes resources/builtin/roles/fa-lock.png | Bin 0 -> 1128 bytes resources/builtin/roles/fa-map-marker.png | Bin 0 -> 3115 bytes resources/builtin/roles/fa-microphone.png | Bin 0 -> 1649 bytes resources/builtin/roles/fa-mobile.png | Bin 0 -> 840 bytes resources/builtin/roles/fa-money.png | Bin 0 -> 1279 bytes resources/builtin/roles/fa-phone.png | Bin 0 -> 1499 bytes resources/builtin/roles/fa-pie-chart.png | Bin 0 -> 1587 bytes resources/builtin/roles/fa-rebel.png | Bin 0 -> 2394 bytes resources/builtin/roles/fa-reddit-alien.png | Bin 0 -> 2238 bytes resources/builtin/roles/fa-safari.png | Bin 0 -> 3632 bytes resources/builtin/roles/fa-search.png | Bin 0 -> 1773 bytes resources/builtin/roles/fa-server.png | Bin 0 -> 714 bytes resources/builtin/roles/fa-shopping-cart.png | Bin 0 -> 1212 bytes resources/builtin/roles/fa-sitemap.png | Bin 0 -> 912 bytes resources/builtin/roles/fa-star.png | Bin 0 -> 1574 bytes resources/builtin/roles/fa-tablet.png | Bin 0 -> 761 bytes resources/builtin/roles/fa-tag.png | Bin 0 -> 1070 bytes resources/builtin/roles/fa-tags.png | Bin 0 -> 1302 bytes resources/builtin/roles/fa-trash-o.png | Bin 0 -> 1714 bytes resources/builtin/roles/fa-truck.png | Bin 0 -> 1326 bytes resources/builtin/roles/fa-twitter.png | Bin 0 -> 1634 bytes resources/builtin/roles/fa-umbrella.png | Bin 0 -> 1584 bytes resources/builtin/roles/fa-university.png | Bin 0 -> 698 bytes resources/builtin/roles/fa-user-secret.png | Bin 0 -> 2108 bytes resources/builtin/roles/fa-user.png | Bin 0 -> 1432 bytes resources/builtin/roles/fa-users.png | Bin 0 -> 2411 bytes resources/builtin/roles/fa-warning.png | Bin 0 -> 1415 bytes resources/builtin/roles/fa-wheelchair.png | Bin 0 -> 1905 bytes resources/builtin/roles/fa-windows.png | Bin 0 -> 854 bytes resources/builtin/roles/v3/archive.png | Bin 0 -> 4491 bytes resources/builtin/roles/v3/basic-book.png | Bin 0 -> 3221 bytes resources/builtin/roles/v3/book.png | Bin 0 -> 3411 bytes resources/builtin/roles/v3/briefcase.png | Bin 0 -> 4350 bytes resources/builtin/roles/v3/bug.png | Bin 0 -> 8151 bytes resources/builtin/roles/v3/calendar.png | Bin 0 -> 4330 bytes resources/builtin/roles/v3/clipboard.png | Bin 0 -> 4737 bytes resources/builtin/roles/v3/cloud.png | Bin 0 -> 7539 bytes resources/builtin/roles/v3/code.png | Bin 0 -> 6685 bytes resources/builtin/roles/v3/contact.png | Bin 0 -> 6819 bytes resources/builtin/roles/v3/creditcard.png | Bin 0 -> 2306 bytes resources/builtin/roles/v3/database.png | Bin 0 -> 11418 bytes resources/builtin/roles/v3/desktop.png | Bin 0 -> 2581 bytes resources/builtin/roles/v3/discussion.png | Bin 0 -> 10059 bytes resources/builtin/roles/v3/download.png | Bin 0 -> 9392 bytes resources/builtin/roles/v3/experimental.png | Bin 0 -> 7579 bytes resources/builtin/roles/v3/flag.png | Bin 0 -> 6751 bytes resources/builtin/roles/v3/folder.png | Bin 0 -> 2237 bytes resources/builtin/roles/v3/gears.png | Bin 0 -> 15975 bytes resources/builtin/roles/v3/gold.png | Bin 0 -> 7077 bytes resources/builtin/roles/v3/home.png | Bin 0 -> 8787 bytes resources/builtin/roles/v3/library.png | Bin 0 -> 5783 bytes resources/builtin/roles/v3/lightbulb.png | Bin 0 -> 6435 bytes resources/builtin/roles/v3/lock.png | Bin 0 -> 7824 bytes resources/builtin/roles/v3/mail.png | Bin 0 -> 5777 bytes resources/builtin/roles/v3/manage.png | Bin 0 -> 6277 bytes resources/builtin/roles/v3/marker.png | Bin 0 -> 8125 bytes resources/builtin/roles/v3/mobile.png | Bin 0 -> 4209 bytes resources/builtin/roles/v3/one-server.png | Bin 0 -> 4128 bytes resources/builtin/roles/v3/organization.png | Bin 0 -> 4252 bytes resources/builtin/roles/v3/people.png | Bin 0 -> 13619 bytes resources/builtin/roles/v3/piechart.png | Bin 0 -> 11337 bytes resources/builtin/roles/v3/police-badge.png | Bin 0 -> 12753 bytes resources/builtin/roles/v3/purchase-order.png | Bin 0 -> 5189 bytes resources/builtin/roles/v3/robot.png | Bin 0 -> 9119 bytes resources/builtin/roles/v3/rocket.png | Bin 0 -> 11037 bytes .../builtin/roles/v3/server-documentation.png | Bin 0 -> 5590 bytes resources/builtin/roles/v3/servers.png | Bin 0 -> 6447 bytes resources/builtin/roles/v3/shield.png | Bin 0 -> 9232 bytes resources/builtin/roles/v3/silver.png | Bin 0 -> 6059 bytes resources/builtin/roles/v3/sitemap.png | Bin 0 -> 4336 bytes resources/builtin/roles/v3/support.png | Bin 0 -> 4726 bytes resources/builtin/roles/v3/sword.png | Bin 0 -> 6791 bytes resources/builtin/roles/v3/tag.png | Bin 0 -> 4161 bytes resources/builtin/roles/v3/three-servers.png | Bin 0 -> 6623 bytes resources/builtin/roles/v3/trash.png | Bin 0 -> 9154 bytes resources/builtin/roles/v3/truck.png | Bin 0 -> 6294 bytes resources/builtin/roles/v3/two-servers.png | Bin 0 -> 5460 bytes resources/builtin/roles/v3/umbrella.png | Bin 0 -> 9812 bytes resources/builtin/roles/v3/upload.png | Bin 0 -> 9463 bytes resources/builtin/roles/v3/wand.png | Bin 0 -> 8717 bytes .../autopatches/090.forceuniquerolenames.php | 114 ++ .../autopatches/20140521.roleslug.2.mig.php | 36 + .../sql/autopatches/20140711.rnames.2.php | 11 + .../autopatches/20140805.rolboardcol.2.php | 53 + .../autopatches/20140808.rolboardprop.3.php | 24 + .../20141107.role.phriction.policy.2.php | 61 + .../autopatches/20141222.maniphestroltxn.php | 61 + .../autopatches/20150515.role.mailkey.2.php | 18 + .../20151219.role.06.defaultpolicy.php | 28 + .../20151223.rol.05.updatekeys.php | 24 + .../sql/autopatches/20151231.rol.01.icon.php | 34 + .../20160122.role.1.boarddefault.php | 60 + .../20181031.rolboard.01.queryreset.php | 50 + .../autopatches/20190129.role.01.spaces.php | 18 + .../sql/patches/090.forceuniquerolesnames.php | 114 ++ .../20130716.archivememberlessroles.php | 38 + .../sql/patches/20131020.rxactionmig.php | 91 + resources/sql/patches/migrate-role-edges.php | 35 + resources/sql/quickstart.sql | 409 ++++ src/__phutil_library_map__.php | 204 ++ ...PhabricatorPeopleProfileViewController.php | 56 + .../PhrictionDocumentContentTransaction.php | 8 +- .../PhrictionDocumentDraftTransaction.php | 8 +- .../__tests__/PhabricatorRoleCoreTestCase.php | 1694 +++++++++++++++++ .../PhabricatorRoleApplication.php | 177 ++ .../capability/RoleCanLockRolesCapability.php | 16 + .../capability/RoleCreateRolesCapability.php | 16 + .../capability/RoleDefaultEditCapability.php | 12 + .../capability/RoleDefaultJoinCapability.php | 12 + .../capability/RoleDefaultViewCapability.php | 15 + .../PhabricatorRoleActivityChartEngine.php | 135 ++ .../PhabricatorRoleBurndownChartEngine.php | 111 ++ .../command/ProjectAddRolesEmailCommand.php | 70 + .../RoleColumnSearchConduitAPIMethod.php | 18 + .../roles/conduit/RoleConduitAPIMethod.php | 48 + .../conduit/RoleCreateConduitAPIMethod.php | 94 + .../conduit/RoleEditConduitAPIMethod.php | 19 + .../conduit/RoleQueryConduitAPIMethod.php | 137 ++ .../conduit/RoleSearchConduitAPIMethod.php | 24 + .../PhabricatorRoleColorsConfigType.php | 14 + .../config/PhabricatorRoleConfigOptions.php | 141 ++ .../config/PhabricatorRoleIconsConfigType.php | 14 + .../PhabricatorRoleSubtypesConfigType.php | 14 + .../roles/constants/PhabricatorRoleStatus.php | 24 + ...habricatorRoleWorkboardBackgroundColor.php | 124 ++ .../PhabricatorRoleArchiveController.php | 69 + ...abricatorRoleBoardBackgroundController.php | 172 ++ .../PhabricatorRoleBoardController.php | 36 + .../PhabricatorRoleBoardDefaultController.php | 81 + .../PhabricatorRoleBoardDisableController.php | 62 + .../PhabricatorRoleBoardFilterController.php | 56 + .../PhabricatorRoleBoardImportController.php | 113 ++ .../PhabricatorRoleBoardManageController.php | 144 ++ .../PhabricatorRoleBoardReloadController.php | 73 + .../PhabricatorRoleBoardReorderController.php | 145 ++ .../PhabricatorRoleBoardViewController.php | 1040 ++++++++++ ...habricatorRoleColumnBulkEditController.php | 72 + ...habricatorRoleColumnBulkMoveController.php | 301 +++ .../PhabricatorRoleColumnDetailController.php | 113 ++ .../PhabricatorRoleColumnEditController.php | 143 ++ .../PhabricatorRoleColumnHideController.php | 149 ++ ...catorRoleColumnRemoveTriggerController.php | 60 + ...abricatorRoleColumnViewQueryController.php | 72 + .../controller/PhabricatorRoleController.php | 231 +++ .../PhabricatorRoleCoverController.php | 53 + .../PhabricatorRoleEditController.php | 112 ++ .../PhabricatorRoleEditPictureController.php | 354 ++++ .../PhabricatorRoleListController.php | 26 + .../PhabricatorRoleLockController.php | 86 + .../PhabricatorRoleManageController.php | 157 ++ .../PhabricatorRoleMembersAddController.php | 77 + ...PhabricatorRoleMembersRemoveController.php | 95 + .../PhabricatorRoleMembersViewController.php | 229 +++ .../PhabricatorRoleMenuItemController.php | 24 + .../PhabricatorRoleMoveController.php | 147 ++ .../PhabricatorRoleProfileController.php | 334 ++++ .../PhabricatorRoleReportsController.php | 74 + .../PhabricatorRoleSilenceController.php | 87 + ...habricatorRoleSubroleWarningController.php | 51 + .../PhabricatorRoleSubrolesController.php | 227 +++ .../PhabricatorRoleUpdateController.php | 120 ++ .../PhabricatorRoleViewController.php | 48 + .../PhabricatorRoleWatchController.php | 115 ++ .../PhabricatorRoleTriggerController.php | 16 + .../PhabricatorRoleTriggerEditController.php | 299 +++ .../PhabricatorRoleTriggerListController.php | 16 + .../PhabricatorRoleTriggerViewController.php | 232 +++ .../PhabricatorRoleConfiguredCustomField.php | 17 + .../PhabricatorRoleCustomField.php | 18 + .../PhabricatorRoleDescriptionField.php | 20 + .../PhabricatorRoleStandardCustomField.php | 23 + ...bricatorRoleMaterializedMemberEdgeType.php | 8 + .../PhabricatorRoleMemberOfRoleEdgeType.php | 103 + .../PhabricatorRoleObjectHasRoleEdgeType.php | 108 ++ .../PhabricatorRoleRoleHasMemberEdgeType.php | 103 + .../PhabricatorRoleRoleHasObjectEdgeType.php | 12 + .../edge/PhabricatorRoleSilencedEdgeType.php | 8 + ...PhabricatorRoleColumnTransactionEditor.php | 22 + .../PhabricatorRoleTransactionEditor.php | 457 +++++ .../editor/PhabricatorRoleTriggerEditor.php | 34 + .../engine/PhabricatorBoardLayoutEngine.php | 601 ++++++ .../PhabricatorBoardRenderingEngine.php | 147 ++ .../engine/PhabricatorBoardResponseEngine.php | 279 +++ .../engine/PhabricatorRoleEditEngine.php | 326 ++++ .../PhabricatorRoleProfileMenuEngine.php | 61 + ...atorBoardColumnsSearchEngineAttachment.php | 80 + ...habricatorRoleHovercardEngineExtension.php | 55 + ...orRoleTriggerUsageIndexEngineExtension.php | 69 + ...orRolesAncestorsSearchEngineAttachment.php | 30 + .../PhabricatorRolesCurtainExtension.php | 91 + .../PhabricatorRolesEditEngineExtension.php | 96 + ...habricatorRolesFulltextEngineExtension.php | 37 + .../PhabricatorRolesMailEngineExtension.php | 32 + ...atorRolesMembersSearchEngineAttachment.php | 31 + ...torRolesMembershipIndexEngineExtension.php | 176 ++ ...PhabricatorRolesSearchEngineAttachment.php | 43 + .../PhabricatorRolesSearchEngineExtension.php | 60 + ...torRolesWatchersSearchEngineAttachment.php | 31 + .../RoleDatasourceEngineExtension.php | 57 + .../events/PhabricatorRoleUIEventListener.php | 115 ++ ...bricatorRoleTriggerCorruptionException.php | 4 + .../roles/herald/HeraldExactRolesField.php | 31 + .../roles/herald/HeraldRolesField.php | 18 + .../herald/PhabricatorRoleAddHeraldAction.php | 28 + .../herald/PhabricatorRoleHeraldAction.php | 125 ++ .../herald/PhabricatorRoleHeraldAdapter.php | 59 + .../PhabricatorRoleHeraldFieldGroup.php | 15 + .../PhabricatorRoleRemoveHeraldAction.php | 28 + .../herald/PhabricatorRoleTagsAddedField.php | 23 + .../roles/herald/PhabricatorRoleTagsField.php | 27 + .../PhabricatorRoleTagsRemovedField.php | 23 + .../roles/icon/PhabricatorRoleDropEffect.php | 83 + .../roles/icon/PhabricatorRoleIconSet.php | 509 +++++ .../PhabricatorColumnProxyInterface.php | 7 + .../interface/PhabricatorRoleInterface.php | 3 + .../PhabricatorWorkboardInterface.php | 3 + .../PhabricatorRoleNameContextFreeGrammar.php | 75 + .../PhabricatorRoleTestDataGenerator.php | 72 + .../roles/mail/RoleReplyHandler.php | 21 + .../PhabricatorRoleDetailsProfileMenuItem.php | 73 + .../PhabricatorRoleManageProfileMenuItem.php | 73 + .../PhabricatorRoleMembersProfileMenuItem.php | 63 + .../PhabricatorRolePictureProfileMenuItem.php | 51 + .../PhabricatorRolePointsProfileMenuItem.php | 177 ++ .../PhabricatorRoleReportsProfileMenuItem.php | 85 + ...PhabricatorRoleSubrolesProfileMenuItem.php | 70 + ...habricatorRoleWorkboardProfileMenuItem.php | 77 + .../PhabricatorRoleColumnAuthorOrder.php | 139 ++ .../PhabricatorRoleColumnCreatedOrder.php | 35 + .../order/PhabricatorRoleColumnHeader.php | 110 ++ .../PhabricatorRoleColumnNaturalOrder.php | 24 + .../order/PhabricatorRoleColumnOrder.php | 211 ++ .../order/PhabricatorRoleColumnOwnerOrder.php | 199 ++ .../PhabricatorRoleColumnPointsOrder.php | 50 + .../PhabricatorRoleColumnPriorityOrder.php | 113 ++ .../PhabricatorRoleColumnStatusOrder.php | 116 ++ .../order/PhabricatorRoleColumnTitleOrder.php | 34 + .../phid/PhabricatorRoleColumnPHIDType.php | 48 + .../phid/PhabricatorRoleRolePHIDType.php | 121 ++ .../phid/PhabricatorRoleTriggerPHIDType.php | 45 + .../PhabricatorRoleMembersPolicyRule.php | 97 + .../PhabricatorRolesAllPolicyRule.php | 29 + .../PhabricatorRolesBasePolicyRule.php | 64 + .../policyrule/PhabricatorRolesPolicyRule.php | 29 + .../PhabricatorRoleColumnPositionQuery.php | 77 + .../query/PhabricatorRoleColumnQuery.php | 235 +++ .../PhabricatorRoleColumnSearchEngine.php | 78 + .../PhabricatorRoleColumnTransactionQuery.php | 10 + .../roles/query/PhabricatorRoleQuery.php | 901 +++++++++ .../query/PhabricatorRoleSearchEngine.php | 359 ++++ .../query/PhabricatorRoleTransactionQuery.php | 10 + .../query/PhabricatorRoleTriggerQuery.php | 135 ++ .../PhabricatorRoleTriggerSearchEngine.php | 155 ++ ...PhabricatorRoleTriggerTransactionQuery.php | 10 + .../roles/remarkup/RoleRemarkupRule.php | 76 + .../__tests__/RoleRemarkupRuleTestCase.php | 147 ++ .../search/PhabricatorRoleFerretEngine.php | 18 + .../search/PhabricatorRoleFulltextEngine.php | 41 + .../PhabricatorRoleSearchField.php | 54 + .../state/PhabricatorWorkboardViewState.php | 291 +++ .../roles/storage/PhabricatorRole.php | 918 +++++++++ .../roles/storage/PhabricatorRoleColumn.php | 362 ++++ .../storage/PhabricatorRoleColumnPosition.php | 89 + .../PhabricatorRoleColumnTransaction.php | 18 + ...PhabricatorRoleCustomFieldNumericIndex.php | 10 + .../PhabricatorRoleCustomFieldStorage.php | 10 + .../PhabricatorRoleCustomFieldStringIndex.php | 10 + .../roles/storage/PhabricatorRoleDAO.php | 9 + .../storage/PhabricatorRoleSchemaSpec.php | 33 + .../roles/storage/PhabricatorRoleSlug.php | 25 + .../storage/PhabricatorRoleTransaction.php | 164 ++ .../roles/storage/PhabricatorRoleTrigger.php | 352 ++++ .../PhabricatorRoleTriggerTransaction.php | 18 + .../storage/PhabricatorRoleTriggerUsage.php | 28 + .../PhabricatorRoleTriggerAddRolesRule.php | 111 ++ .../PhabricatorRoleTriggerInvalidRule.php | 93 + ...abricatorRoleTriggerManiphestOwnerRule.php | 148 ++ ...icatorRoleTriggerManiphestPriorityRule.php | 98 + ...bricatorRoleTriggerManiphestStatusRule.php | 97 + .../PhabricatorRoleTriggerPlaySoundRule.php | 123 ++ ...abricatorRoleTriggerRemoveProjectsRule.php | 111 ++ .../trigger/PhabricatorRoleTriggerRule.php | 172 ++ .../PhabricatorRoleTriggerRuleRecord.php | 27 + .../PhabricatorRoleTriggerUnknownRule.php | 71 + .../typeahead/PhabricatorRoleDatasource.php | 157 ++ ...abricatorRoleLogicalAncestorDatasource.php | 95 + .../PhabricatorRoleLogicalDatasource.php | 29 + .../PhabricatorRoleLogicalOnlyDatasource.php | 76 + .../PhabricatorRoleLogicalOrNotDatasource.php | 169 ++ .../PhabricatorRoleLogicalUserDatasource.php | 138 ++ ...PhabricatorRoleLogicalViewerDatasource.php | 103 + .../PhabricatorRoleMembersDatasource.php | 104 + .../PhabricatorRoleNoRolesDatasource.php | 75 + .../PhabricatorRoleOrUserDatasource.php | 21 + ...habricatorRoleOrUserFunctionDatasource.php | 25 + .../PhabricatorRoleSubtypeDatasource.php | 45 + .../PhabricatorRoleUserFunctionDatasource.php | 36 + .../roles/view/PhabricatorRoleCardView.php | 84 + .../roles/view/PhabricatorRoleListView.php | 107 ++ .../view/PhabricatorRoleMemberListView.php | 65 + .../view/PhabricatorRoleUserListView.php | 183 ++ .../view/PhabricatorRoleWatcherListView.php | 43 + .../roles/view/ProjectBoardTaskCard.php | 186 ++ .../PhabricatorRoleColorTransaction.php | 33 + .../PhabricatorRoleFilterTransaction.php | 26 + .../PhabricatorRoleIconTransaction.php | 42 + .../PhabricatorRoleImageTransaction.php | 136 ++ .../PhabricatorRoleLockTransaction.php | 64 + .../PhabricatorRoleMilestoneTransaction.php | 31 + .../PhabricatorRoleNameTransaction.php | 112 ++ .../PhabricatorRoleParentTransaction.php | 29 + .../PhabricatorRoleSlugsTransaction.php | 174 ++ .../PhabricatorRoleSortTransaction.php | 26 + .../PhabricatorRoleStatusTransaction.php | 66 + .../PhabricatorRoleTransactionType.php | 4 + .../PhabricatorRoleTypeTransaction.php | 71 + ...atorRoleWorkboardBackgroundTransaction.php | 26 + .../PhabricatorRoleWorkboardTransaction.php | 38 + .../PhabricatorRoleColumnLimitTransaction.php | 63 + .../PhabricatorRoleColumnNameTransaction.php | 69 + ...PhabricatorRoleColumnStatusTransaction.php | 64 + .../PhabricatorRoleColumnTransactionType.php | 4 + ...habricatorRoleColumnTriggerTransaction.php | 97 + .../PhabricatorRoleTriggerNameTransaction.php | 58 + ...abricatorRoleTriggerRulesetTransaction.php | 81 + .../PhabricatorRoleTriggerTransactionType.php | 4 + .../js/application/roles/WorkboardBoard.js | 795 ++++++++ .../js/application/roles/WorkboardCard.js | 79 + .../roles/WorkboardCardTemplate.js | 69 + .../js/application/roles/WorkboardColumn.js | 486 +++++ .../application/roles/WorkboardController.js | 201 ++ .../application/roles/WorkboardDropEffect.js | 73 + .../js/application/roles/WorkboardHeader.js | 48 + .../roles/WorkboardHeaderTemplate.js | 40 + .../roles/WorkboardOrderTemplate.js | 27 + .../roles/behavior-reorder-columns.js | 58 + .../application/roles/behavior-role-boards.js | 182 ++ .../application/roles/behavior-role-create.js | 26 + 375 files changed, 28061 insertions(+), 8 deletions(-) create mode 100644 resources/builtin/roles/fa-android.png create mode 100644 resources/builtin/roles/fa-apple.png create mode 100644 resources/builtin/roles/fa-beer.png create mode 100644 resources/builtin/roles/fa-bomb.png create mode 100644 resources/builtin/roles/fa-book.png create mode 100644 resources/builtin/roles/fa-briefcase.png create mode 100644 resources/builtin/roles/fa-bug.png create mode 100644 resources/builtin/roles/fa-building.png create mode 100644 resources/builtin/roles/fa-calendar.png create mode 100644 resources/builtin/roles/fa-camera-retro.png create mode 100644 resources/builtin/roles/fa-chrome.png create mode 100644 resources/builtin/roles/fa-cloud.png create mode 100644 resources/builtin/roles/fa-coffee.png create mode 100644 resources/builtin/roles/fa-comments.png create mode 100644 resources/builtin/roles/fa-credit-card.png create mode 100644 resources/builtin/roles/fa-database.png create mode 100644 resources/builtin/roles/fa-desktop.png create mode 100644 resources/builtin/roles/fa-diamond.png create mode 100644 resources/builtin/roles/fa-empire.png create mode 100644 resources/builtin/roles/fa-envelope.png create mode 100644 resources/builtin/roles/fa-facebook.png create mode 100644 resources/builtin/roles/fa-fax.png create mode 100644 resources/builtin/roles/fa-film.png create mode 100644 resources/builtin/roles/fa-firefox.png create mode 100644 resources/builtin/roles/fa-flag-checkered.png create mode 100644 resources/builtin/roles/fa-flask.png create mode 100644 resources/builtin/roles/fa-folder.png create mode 100644 resources/builtin/roles/fa-gamepad.png create mode 100644 resources/builtin/roles/fa-gears.png create mode 100644 resources/builtin/roles/fa-google.png create mode 100644 resources/builtin/roles/fa-hand-peace-o.png create mode 100644 resources/builtin/roles/fa-hashtag.png create mode 100644 resources/builtin/roles/fa-heart.png create mode 100644 resources/builtin/roles/fa-internet-explorer.png create mode 100644 resources/builtin/roles/fa-key.png create mode 100644 resources/builtin/roles/fa-legal.png create mode 100644 resources/builtin/roles/fa-linux.png create mode 100644 resources/builtin/roles/fa-lock.png create mode 100644 resources/builtin/roles/fa-map-marker.png create mode 100644 resources/builtin/roles/fa-microphone.png create mode 100644 resources/builtin/roles/fa-mobile.png create mode 100644 resources/builtin/roles/fa-money.png create mode 100644 resources/builtin/roles/fa-phone.png create mode 100644 resources/builtin/roles/fa-pie-chart.png create mode 100644 resources/builtin/roles/fa-rebel.png create mode 100644 resources/builtin/roles/fa-reddit-alien.png create mode 100644 resources/builtin/roles/fa-safari.png create mode 100644 resources/builtin/roles/fa-search.png create mode 100644 resources/builtin/roles/fa-server.png create mode 100644 resources/builtin/roles/fa-shopping-cart.png create mode 100644 resources/builtin/roles/fa-sitemap.png create mode 100644 resources/builtin/roles/fa-star.png create mode 100644 resources/builtin/roles/fa-tablet.png create mode 100644 resources/builtin/roles/fa-tag.png create mode 100644 resources/builtin/roles/fa-tags.png create mode 100644 resources/builtin/roles/fa-trash-o.png create mode 100644 resources/builtin/roles/fa-truck.png create mode 100644 resources/builtin/roles/fa-twitter.png create mode 100644 resources/builtin/roles/fa-umbrella.png create mode 100644 resources/builtin/roles/fa-university.png create mode 100644 resources/builtin/roles/fa-user-secret.png create mode 100644 resources/builtin/roles/fa-user.png create mode 100644 resources/builtin/roles/fa-users.png create mode 100644 resources/builtin/roles/fa-warning.png create mode 100644 resources/builtin/roles/fa-wheelchair.png create mode 100644 resources/builtin/roles/fa-windows.png create mode 100644 resources/builtin/roles/v3/archive.png create mode 100644 resources/builtin/roles/v3/basic-book.png create mode 100644 resources/builtin/roles/v3/book.png create mode 100644 resources/builtin/roles/v3/briefcase.png create mode 100644 resources/builtin/roles/v3/bug.png create mode 100644 resources/builtin/roles/v3/calendar.png create mode 100644 resources/builtin/roles/v3/clipboard.png create mode 100644 resources/builtin/roles/v3/cloud.png create mode 100644 resources/builtin/roles/v3/code.png create mode 100644 resources/builtin/roles/v3/contact.png create mode 100644 resources/builtin/roles/v3/creditcard.png create mode 100644 resources/builtin/roles/v3/database.png create mode 100644 resources/builtin/roles/v3/desktop.png create mode 100644 resources/builtin/roles/v3/discussion.png create mode 100644 resources/builtin/roles/v3/download.png create mode 100644 resources/builtin/roles/v3/experimental.png create mode 100644 resources/builtin/roles/v3/flag.png create mode 100644 resources/builtin/roles/v3/folder.png create mode 100644 resources/builtin/roles/v3/gears.png create mode 100644 resources/builtin/roles/v3/gold.png create mode 100644 resources/builtin/roles/v3/home.png create mode 100644 resources/builtin/roles/v3/library.png create mode 100644 resources/builtin/roles/v3/lightbulb.png create mode 100644 resources/builtin/roles/v3/lock.png create mode 100644 resources/builtin/roles/v3/mail.png create mode 100644 resources/builtin/roles/v3/manage.png create mode 100644 resources/builtin/roles/v3/marker.png create mode 100644 resources/builtin/roles/v3/mobile.png create mode 100644 resources/builtin/roles/v3/one-server.png create mode 100644 resources/builtin/roles/v3/organization.png create mode 100644 resources/builtin/roles/v3/people.png create mode 100644 resources/builtin/roles/v3/piechart.png create mode 100644 resources/builtin/roles/v3/police-badge.png create mode 100644 resources/builtin/roles/v3/purchase-order.png create mode 100644 resources/builtin/roles/v3/robot.png create mode 100644 resources/builtin/roles/v3/rocket.png create mode 100644 resources/builtin/roles/v3/server-documentation.png create mode 100644 resources/builtin/roles/v3/servers.png create mode 100644 resources/builtin/roles/v3/shield.png create mode 100644 resources/builtin/roles/v3/silver.png create mode 100644 resources/builtin/roles/v3/sitemap.png create mode 100644 resources/builtin/roles/v3/support.png create mode 100644 resources/builtin/roles/v3/sword.png create mode 100644 resources/builtin/roles/v3/tag.png create mode 100644 resources/builtin/roles/v3/three-servers.png create mode 100644 resources/builtin/roles/v3/trash.png create mode 100644 resources/builtin/roles/v3/truck.png create mode 100644 resources/builtin/roles/v3/two-servers.png create mode 100644 resources/builtin/roles/v3/umbrella.png create mode 100644 resources/builtin/roles/v3/upload.png create mode 100644 resources/builtin/roles/v3/wand.png create mode 100644 resources/sql/autopatches/090.forceuniquerolenames.php create mode 100644 resources/sql/autopatches/20140521.roleslug.2.mig.php create mode 100644 resources/sql/autopatches/20140711.rnames.2.php create mode 100644 resources/sql/autopatches/20140805.rolboardcol.2.php create mode 100644 resources/sql/autopatches/20140808.rolboardprop.3.php create mode 100644 resources/sql/autopatches/20141107.role.phriction.policy.2.php create mode 100644 resources/sql/autopatches/20141222.maniphestroltxn.php create mode 100644 resources/sql/autopatches/20150515.role.mailkey.2.php create mode 100644 resources/sql/autopatches/20151219.role.06.defaultpolicy.php create mode 100644 resources/sql/autopatches/20151223.rol.05.updatekeys.php create mode 100644 resources/sql/autopatches/20151231.rol.01.icon.php create mode 100644 resources/sql/autopatches/20160122.role.1.boarddefault.php create mode 100644 resources/sql/autopatches/20181031.rolboard.01.queryreset.php create mode 100644 resources/sql/autopatches/20190129.role.01.spaces.php create mode 100644 resources/sql/patches/090.forceuniquerolesnames.php create mode 100644 resources/sql/patches/20130716.archivememberlessroles.php create mode 100644 resources/sql/patches/20131020.rxactionmig.php create mode 100644 resources/sql/patches/migrate-role-edges.php create mode 100644 src/extensions/roles/__tests__/PhabricatorRoleCoreTestCase.php create mode 100644 src/extensions/roles/application/PhabricatorRoleApplication.php create mode 100644 src/extensions/roles/capability/RoleCanLockRolesCapability.php create mode 100644 src/extensions/roles/capability/RoleCreateRolesCapability.php create mode 100644 src/extensions/roles/capability/RoleDefaultEditCapability.php create mode 100644 src/extensions/roles/capability/RoleDefaultJoinCapability.php create mode 100644 src/extensions/roles/capability/RoleDefaultViewCapability.php create mode 100644 src/extensions/roles/chart/PhabricatorRoleActivityChartEngine.php create mode 100644 src/extensions/roles/chart/PhabricatorRoleBurndownChartEngine.php create mode 100644 src/extensions/roles/command/ProjectAddRolesEmailCommand.php create mode 100644 src/extensions/roles/conduit/RoleColumnSearchConduitAPIMethod.php create mode 100644 src/extensions/roles/conduit/RoleConduitAPIMethod.php create mode 100644 src/extensions/roles/conduit/RoleCreateConduitAPIMethod.php create mode 100644 src/extensions/roles/conduit/RoleEditConduitAPIMethod.php create mode 100644 src/extensions/roles/conduit/RoleQueryConduitAPIMethod.php create mode 100644 src/extensions/roles/conduit/RoleSearchConduitAPIMethod.php create mode 100644 src/extensions/roles/config/PhabricatorRoleColorsConfigType.php create mode 100644 src/extensions/roles/config/PhabricatorRoleConfigOptions.php create mode 100644 src/extensions/roles/config/PhabricatorRoleIconsConfigType.php create mode 100644 src/extensions/roles/config/PhabricatorRoleSubtypesConfigType.php create mode 100644 src/extensions/roles/constants/PhabricatorRoleStatus.php create mode 100644 src/extensions/roles/constants/PhabricatorRoleWorkboardBackgroundColor.php create mode 100644 src/extensions/roles/controller/PhabricatorRoleArchiveController.php create mode 100644 src/extensions/roles/controller/PhabricatorRoleBoardBackgroundController.php create mode 100644 src/extensions/roles/controller/PhabricatorRoleBoardController.php create mode 100644 src/extensions/roles/controller/PhabricatorRoleBoardDefaultController.php create mode 100644 src/extensions/roles/controller/PhabricatorRoleBoardDisableController.php create mode 100644 src/extensions/roles/controller/PhabricatorRoleBoardFilterController.php create mode 100644 src/extensions/roles/controller/PhabricatorRoleBoardImportController.php create mode 100644 src/extensions/roles/controller/PhabricatorRoleBoardManageController.php create mode 100644 src/extensions/roles/controller/PhabricatorRoleBoardReloadController.php create mode 100644 src/extensions/roles/controller/PhabricatorRoleBoardReorderController.php create mode 100644 src/extensions/roles/controller/PhabricatorRoleBoardViewController.php create mode 100644 src/extensions/roles/controller/PhabricatorRoleColumnBulkEditController.php create mode 100644 src/extensions/roles/controller/PhabricatorRoleColumnBulkMoveController.php create mode 100644 src/extensions/roles/controller/PhabricatorRoleColumnDetailController.php create mode 100644 src/extensions/roles/controller/PhabricatorRoleColumnEditController.php create mode 100644 src/extensions/roles/controller/PhabricatorRoleColumnHideController.php create mode 100644 src/extensions/roles/controller/PhabricatorRoleColumnRemoveTriggerController.php create mode 100644 src/extensions/roles/controller/PhabricatorRoleColumnViewQueryController.php create mode 100644 src/extensions/roles/controller/PhabricatorRoleController.php create mode 100644 src/extensions/roles/controller/PhabricatorRoleCoverController.php create mode 100644 src/extensions/roles/controller/PhabricatorRoleEditController.php create mode 100644 src/extensions/roles/controller/PhabricatorRoleEditPictureController.php create mode 100644 src/extensions/roles/controller/PhabricatorRoleListController.php create mode 100644 src/extensions/roles/controller/PhabricatorRoleLockController.php create mode 100644 src/extensions/roles/controller/PhabricatorRoleManageController.php create mode 100644 src/extensions/roles/controller/PhabricatorRoleMembersAddController.php create mode 100644 src/extensions/roles/controller/PhabricatorRoleMembersRemoveController.php create mode 100644 src/extensions/roles/controller/PhabricatorRoleMembersViewController.php create mode 100644 src/extensions/roles/controller/PhabricatorRoleMenuItemController.php create mode 100644 src/extensions/roles/controller/PhabricatorRoleMoveController.php create mode 100644 src/extensions/roles/controller/PhabricatorRoleProfileController.php create mode 100644 src/extensions/roles/controller/PhabricatorRoleReportsController.php create mode 100644 src/extensions/roles/controller/PhabricatorRoleSilenceController.php create mode 100644 src/extensions/roles/controller/PhabricatorRoleSubroleWarningController.php create mode 100644 src/extensions/roles/controller/PhabricatorRoleSubrolesController.php create mode 100644 src/extensions/roles/controller/PhabricatorRoleUpdateController.php create mode 100644 src/extensions/roles/controller/PhabricatorRoleViewController.php create mode 100644 src/extensions/roles/controller/PhabricatorRoleWatchController.php create mode 100644 src/extensions/roles/controller/trigger/PhabricatorRoleTriggerController.php create mode 100644 src/extensions/roles/controller/trigger/PhabricatorRoleTriggerEditController.php create mode 100644 src/extensions/roles/controller/trigger/PhabricatorRoleTriggerListController.php create mode 100644 src/extensions/roles/controller/trigger/PhabricatorRoleTriggerViewController.php create mode 100644 src/extensions/roles/customfield/PhabricatorRoleConfiguredCustomField.php create mode 100644 src/extensions/roles/customfield/PhabricatorRoleCustomField.php create mode 100644 src/extensions/roles/customfield/PhabricatorRoleDescriptionField.php create mode 100644 src/extensions/roles/customfield/PhabricatorRoleStandardCustomField.php create mode 100644 src/extensions/roles/edge/PhabricatorRoleMaterializedMemberEdgeType.php create mode 100644 src/extensions/roles/edge/PhabricatorRoleMemberOfRoleEdgeType.php create mode 100644 src/extensions/roles/edge/PhabricatorRoleObjectHasRoleEdgeType.php create mode 100644 src/extensions/roles/edge/PhabricatorRoleRoleHasMemberEdgeType.php create mode 100644 src/extensions/roles/edge/PhabricatorRoleRoleHasObjectEdgeType.php create mode 100644 src/extensions/roles/edge/PhabricatorRoleSilencedEdgeType.php create mode 100644 src/extensions/roles/editor/PhabricatorRoleColumnTransactionEditor.php create mode 100644 src/extensions/roles/editor/PhabricatorRoleTransactionEditor.php create mode 100644 src/extensions/roles/editor/PhabricatorRoleTriggerEditor.php create mode 100644 src/extensions/roles/engine/PhabricatorBoardLayoutEngine.php create mode 100644 src/extensions/roles/engine/PhabricatorBoardRenderingEngine.php create mode 100644 src/extensions/roles/engine/PhabricatorBoardResponseEngine.php create mode 100644 src/extensions/roles/engine/PhabricatorRoleEditEngine.php create mode 100644 src/extensions/roles/engine/PhabricatorRoleProfileMenuEngine.php create mode 100644 src/extensions/roles/engineextension/PhabricatorBoardColumnsSearchEngineAttachment.php create mode 100644 src/extensions/roles/engineextension/PhabricatorRoleHovercardEngineExtension.php create mode 100644 src/extensions/roles/engineextension/PhabricatorRoleTriggerUsageIndexEngineExtension.php create mode 100644 src/extensions/roles/engineextension/PhabricatorRolesAncestorsSearchEngineAttachment.php create mode 100644 src/extensions/roles/engineextension/PhabricatorRolesCurtainExtension.php create mode 100644 src/extensions/roles/engineextension/PhabricatorRolesEditEngineExtension.php create mode 100644 src/extensions/roles/engineextension/PhabricatorRolesFulltextEngineExtension.php create mode 100644 src/extensions/roles/engineextension/PhabricatorRolesMailEngineExtension.php create mode 100644 src/extensions/roles/engineextension/PhabricatorRolesMembersSearchEngineAttachment.php create mode 100644 src/extensions/roles/engineextension/PhabricatorRolesMembershipIndexEngineExtension.php create mode 100644 src/extensions/roles/engineextension/PhabricatorRolesSearchEngineAttachment.php create mode 100644 src/extensions/roles/engineextension/PhabricatorRolesSearchEngineExtension.php create mode 100644 src/extensions/roles/engineextension/PhabricatorRolesWatchersSearchEngineAttachment.php create mode 100644 src/extensions/roles/engineextension/RoleDatasourceEngineExtension.php create mode 100644 src/extensions/roles/events/PhabricatorRoleUIEventListener.php create mode 100644 src/extensions/roles/exception/PhabricatorRoleTriggerCorruptionException.php create mode 100644 src/extensions/roles/herald/HeraldExactRolesField.php create mode 100644 src/extensions/roles/herald/HeraldRolesField.php create mode 100644 src/extensions/roles/herald/PhabricatorRoleAddHeraldAction.php create mode 100644 src/extensions/roles/herald/PhabricatorRoleHeraldAction.php create mode 100644 src/extensions/roles/herald/PhabricatorRoleHeraldAdapter.php create mode 100644 src/extensions/roles/herald/PhabricatorRoleHeraldFieldGroup.php create mode 100644 src/extensions/roles/herald/PhabricatorRoleRemoveHeraldAction.php create mode 100644 src/extensions/roles/herald/PhabricatorRoleTagsAddedField.php create mode 100644 src/extensions/roles/herald/PhabricatorRoleTagsField.php create mode 100644 src/extensions/roles/herald/PhabricatorRoleTagsRemovedField.php create mode 100644 src/extensions/roles/icon/PhabricatorRoleDropEffect.php create mode 100644 src/extensions/roles/icon/PhabricatorRoleIconSet.php create mode 100644 src/extensions/roles/interface/PhabricatorColumnProxyInterface.php create mode 100644 src/extensions/roles/interface/PhabricatorRoleInterface.php create mode 100644 src/extensions/roles/interface/PhabricatorWorkboardInterface.php create mode 100644 src/extensions/roles/lipsum/PhabricatorRoleNameContextFreeGrammar.php create mode 100644 src/extensions/roles/lipsum/PhabricatorRoleTestDataGenerator.php create mode 100644 src/extensions/roles/mail/RoleReplyHandler.php create mode 100644 src/extensions/roles/menuitem/PhabricatorRoleDetailsProfileMenuItem.php create mode 100644 src/extensions/roles/menuitem/PhabricatorRoleManageProfileMenuItem.php create mode 100644 src/extensions/roles/menuitem/PhabricatorRoleMembersProfileMenuItem.php create mode 100644 src/extensions/roles/menuitem/PhabricatorRolePictureProfileMenuItem.php create mode 100644 src/extensions/roles/menuitem/PhabricatorRolePointsProfileMenuItem.php create mode 100644 src/extensions/roles/menuitem/PhabricatorRoleReportsProfileMenuItem.php create mode 100644 src/extensions/roles/menuitem/PhabricatorRoleSubrolesProfileMenuItem.php create mode 100644 src/extensions/roles/menuitem/PhabricatorRoleWorkboardProfileMenuItem.php create mode 100644 src/extensions/roles/order/PhabricatorRoleColumnAuthorOrder.php create mode 100644 src/extensions/roles/order/PhabricatorRoleColumnCreatedOrder.php create mode 100644 src/extensions/roles/order/PhabricatorRoleColumnHeader.php create mode 100644 src/extensions/roles/order/PhabricatorRoleColumnNaturalOrder.php create mode 100644 src/extensions/roles/order/PhabricatorRoleColumnOrder.php create mode 100644 src/extensions/roles/order/PhabricatorRoleColumnOwnerOrder.php create mode 100644 src/extensions/roles/order/PhabricatorRoleColumnPointsOrder.php create mode 100644 src/extensions/roles/order/PhabricatorRoleColumnPriorityOrder.php create mode 100644 src/extensions/roles/order/PhabricatorRoleColumnStatusOrder.php create mode 100644 src/extensions/roles/order/PhabricatorRoleColumnTitleOrder.php create mode 100644 src/extensions/roles/phid/PhabricatorRoleColumnPHIDType.php create mode 100644 src/extensions/roles/phid/PhabricatorRoleRolePHIDType.php create mode 100644 src/extensions/roles/phid/PhabricatorRoleTriggerPHIDType.php create mode 100644 src/extensions/roles/policyrule/PhabricatorRoleMembersPolicyRule.php create mode 100644 src/extensions/roles/policyrule/PhabricatorRolesAllPolicyRule.php create mode 100644 src/extensions/roles/policyrule/PhabricatorRolesBasePolicyRule.php create mode 100644 src/extensions/roles/policyrule/PhabricatorRolesPolicyRule.php create mode 100644 src/extensions/roles/query/PhabricatorRoleColumnPositionQuery.php create mode 100644 src/extensions/roles/query/PhabricatorRoleColumnQuery.php create mode 100644 src/extensions/roles/query/PhabricatorRoleColumnSearchEngine.php create mode 100644 src/extensions/roles/query/PhabricatorRoleColumnTransactionQuery.php create mode 100644 src/extensions/roles/query/PhabricatorRoleQuery.php create mode 100644 src/extensions/roles/query/PhabricatorRoleSearchEngine.php create mode 100644 src/extensions/roles/query/PhabricatorRoleTransactionQuery.php create mode 100644 src/extensions/roles/query/PhabricatorRoleTriggerQuery.php create mode 100644 src/extensions/roles/query/PhabricatorRoleTriggerSearchEngine.php create mode 100644 src/extensions/roles/query/PhabricatorRoleTriggerTransactionQuery.php create mode 100644 src/extensions/roles/remarkup/RoleRemarkupRule.php create mode 100644 src/extensions/roles/remarkup/__tests__/RoleRemarkupRuleTestCase.php create mode 100644 src/extensions/roles/search/PhabricatorRoleFerretEngine.php create mode 100644 src/extensions/roles/search/PhabricatorRoleFulltextEngine.php create mode 100644 src/extensions/roles/searchfield/PhabricatorRoleSearchField.php create mode 100644 src/extensions/roles/state/PhabricatorWorkboardViewState.php create mode 100644 src/extensions/roles/storage/PhabricatorRole.php create mode 100644 src/extensions/roles/storage/PhabricatorRoleColumn.php create mode 100644 src/extensions/roles/storage/PhabricatorRoleColumnPosition.php create mode 100644 src/extensions/roles/storage/PhabricatorRoleColumnTransaction.php create mode 100644 src/extensions/roles/storage/PhabricatorRoleCustomFieldNumericIndex.php create mode 100644 src/extensions/roles/storage/PhabricatorRoleCustomFieldStorage.php create mode 100644 src/extensions/roles/storage/PhabricatorRoleCustomFieldStringIndex.php create mode 100644 src/extensions/roles/storage/PhabricatorRoleDAO.php create mode 100644 src/extensions/roles/storage/PhabricatorRoleSchemaSpec.php create mode 100644 src/extensions/roles/storage/PhabricatorRoleSlug.php create mode 100644 src/extensions/roles/storage/PhabricatorRoleTransaction.php create mode 100644 src/extensions/roles/storage/PhabricatorRoleTrigger.php create mode 100644 src/extensions/roles/storage/PhabricatorRoleTriggerTransaction.php create mode 100644 src/extensions/roles/storage/PhabricatorRoleTriggerUsage.php create mode 100644 src/extensions/roles/trigger/PhabricatorRoleTriggerAddRolesRule.php create mode 100644 src/extensions/roles/trigger/PhabricatorRoleTriggerInvalidRule.php create mode 100644 src/extensions/roles/trigger/PhabricatorRoleTriggerManiphestOwnerRule.php create mode 100644 src/extensions/roles/trigger/PhabricatorRoleTriggerManiphestPriorityRule.php create mode 100644 src/extensions/roles/trigger/PhabricatorRoleTriggerManiphestStatusRule.php create mode 100644 src/extensions/roles/trigger/PhabricatorRoleTriggerPlaySoundRule.php create mode 100644 src/extensions/roles/trigger/PhabricatorRoleTriggerRemoveProjectsRule.php create mode 100644 src/extensions/roles/trigger/PhabricatorRoleTriggerRule.php create mode 100644 src/extensions/roles/trigger/PhabricatorRoleTriggerRuleRecord.php create mode 100644 src/extensions/roles/trigger/PhabricatorRoleTriggerUnknownRule.php create mode 100644 src/extensions/roles/typeahead/PhabricatorRoleDatasource.php create mode 100644 src/extensions/roles/typeahead/PhabricatorRoleLogicalAncestorDatasource.php create mode 100644 src/extensions/roles/typeahead/PhabricatorRoleLogicalDatasource.php create mode 100644 src/extensions/roles/typeahead/PhabricatorRoleLogicalOnlyDatasource.php create mode 100644 src/extensions/roles/typeahead/PhabricatorRoleLogicalOrNotDatasource.php create mode 100644 src/extensions/roles/typeahead/PhabricatorRoleLogicalUserDatasource.php create mode 100644 src/extensions/roles/typeahead/PhabricatorRoleLogicalViewerDatasource.php create mode 100644 src/extensions/roles/typeahead/PhabricatorRoleMembersDatasource.php create mode 100644 src/extensions/roles/typeahead/PhabricatorRoleNoRolesDatasource.php create mode 100644 src/extensions/roles/typeahead/PhabricatorRoleOrUserDatasource.php create mode 100644 src/extensions/roles/typeahead/PhabricatorRoleOrUserFunctionDatasource.php create mode 100644 src/extensions/roles/typeahead/PhabricatorRoleSubtypeDatasource.php create mode 100644 src/extensions/roles/typeahead/PhabricatorRoleUserFunctionDatasource.php create mode 100644 src/extensions/roles/view/PhabricatorRoleCardView.php create mode 100644 src/extensions/roles/view/PhabricatorRoleListView.php create mode 100644 src/extensions/roles/view/PhabricatorRoleMemberListView.php create mode 100644 src/extensions/roles/view/PhabricatorRoleUserListView.php create mode 100644 src/extensions/roles/view/PhabricatorRoleWatcherListView.php create mode 100644 src/extensions/roles/view/ProjectBoardTaskCard.php create mode 100644 src/extensions/roles/xaction/PhabricatorRoleColorTransaction.php create mode 100644 src/extensions/roles/xaction/PhabricatorRoleFilterTransaction.php create mode 100644 src/extensions/roles/xaction/PhabricatorRoleIconTransaction.php create mode 100644 src/extensions/roles/xaction/PhabricatorRoleImageTransaction.php create mode 100644 src/extensions/roles/xaction/PhabricatorRoleLockTransaction.php create mode 100644 src/extensions/roles/xaction/PhabricatorRoleMilestoneTransaction.php create mode 100644 src/extensions/roles/xaction/PhabricatorRoleNameTransaction.php create mode 100644 src/extensions/roles/xaction/PhabricatorRoleParentTransaction.php create mode 100644 src/extensions/roles/xaction/PhabricatorRoleSlugsTransaction.php create mode 100644 src/extensions/roles/xaction/PhabricatorRoleSortTransaction.php create mode 100644 src/extensions/roles/xaction/PhabricatorRoleStatusTransaction.php create mode 100644 src/extensions/roles/xaction/PhabricatorRoleTransactionType.php create mode 100644 src/extensions/roles/xaction/PhabricatorRoleTypeTransaction.php create mode 100644 src/extensions/roles/xaction/PhabricatorRoleWorkboardBackgroundTransaction.php create mode 100644 src/extensions/roles/xaction/PhabricatorRoleWorkboardTransaction.php create mode 100644 src/extensions/roles/xaction/column/PhabricatorRoleColumnLimitTransaction.php create mode 100644 src/extensions/roles/xaction/column/PhabricatorRoleColumnNameTransaction.php create mode 100644 src/extensions/roles/xaction/column/PhabricatorRoleColumnStatusTransaction.php create mode 100644 src/extensions/roles/xaction/column/PhabricatorRoleColumnTransactionType.php create mode 100644 src/extensions/roles/xaction/column/PhabricatorRoleColumnTriggerTransaction.php create mode 100644 src/extensions/roles/xaction/trigger/PhabricatorRoleTriggerNameTransaction.php create mode 100644 src/extensions/roles/xaction/trigger/PhabricatorRoleTriggerRulesetTransaction.php create mode 100644 src/extensions/roles/xaction/trigger/PhabricatorRoleTriggerTransactionType.php create mode 100644 webroot/rsrc/js/application/roles/WorkboardBoard.js create mode 100644 webroot/rsrc/js/application/roles/WorkboardCard.js create mode 100644 webroot/rsrc/js/application/roles/WorkboardCardTemplate.js create mode 100644 webroot/rsrc/js/application/roles/WorkboardColumn.js create mode 100644 webroot/rsrc/js/application/roles/WorkboardController.js create mode 100644 webroot/rsrc/js/application/roles/WorkboardDropEffect.js create mode 100644 webroot/rsrc/js/application/roles/WorkboardHeader.js create mode 100644 webroot/rsrc/js/application/roles/WorkboardHeaderTemplate.js create mode 100644 webroot/rsrc/js/application/roles/WorkboardOrderTemplate.js create mode 100644 webroot/rsrc/js/application/roles/behavior-reorder-columns.js create mode 100644 webroot/rsrc/js/application/roles/behavior-role-boards.js create mode 100644 webroot/rsrc/js/application/roles/behavior-role-create.js diff --git a/resources/builtin/roles/fa-android.png b/resources/builtin/roles/fa-android.png new file mode 100644 index 0000000000000000000000000000000000000000..db56162683c6ea53fc08e8a6f064dc714d493701 GIT binary patch literal 1663 zcmb7FX;c!37DjQw9UYWhQcUc*jJZIX;YNcJ4NAD1MJ||GrV~sGGFpLS(u5(V4Te@e zCr!;sa7v#`S{SIwL?)~}wx$ON>STkcg zV*mhPhTDsY{;1MV1nGU8hQuS*0DwUR4ug)R=`8i1{dfbMxwd%|dQ7(Xq z()-?f%c5xCkm1IJm*Ho&G1FNz;I3iLzCa-Dfx&3d958~@{~gh?SK65A6vo}AOO6Wy z2^K6*$XsN)j_n@Rgl=tP8*S^9;K#sM{4N)-d6ZWB{ziU%B;D}z_{{ntP&}sb^|kV- zZ0z%?k|-p_aE#KMGxcb|L@}*>dnx3fKhGlGT~bK87-unx6BCHmFZ(W}(8Z!RvuIQW zo0hLiBiWFR3am1HmeuZuS2<=G+lnB``z6)!v0UL>-a(_5^bZ^(B+641xOueI?)73u zuhZ5(t;Y2B)t)c-5OK#vKK=RH`ulA0>#yq^%wF`l#4kWaLHC6GPUfYNMP<~rlA`vf zFBj#qUgx8hsh)uL-IBM9Zu|YcCK!~U=Fz95Ut;Z>U|vegD^z$T#l|9uh^`)CP(MRN z<3fd3%%1wyCw-Gzn-Sr0Jib;xS`SknLsH3Dj9f&yd2!c0(P=pPEHSz-9(!YTeCo#ta=kGc{ z5^0;}h|(XFJ%I?}2;LI@;+BD~lXeTJkuKl!SV5#vzp(Y-j~Sh6(@nR91lhckN=4f} zYG)?sPrwhBKicaaDvzg|3&8>4YOke+4Vlo~If(H^6pQAh`bSVReNteM>UzIDk;Gbx z_&)DZ26%a>y*P{Lt@HtjtXi`)Tgd49I};pPpNDtY45o7jj3(`^cwpk%!>r9@u^%l20+FmB^mBw3K35@GO|f2>H<&o`N)Qg(~{6u^UN z52S9(GIaJ3EtJ|_Si^_qINZO3KL+oTU%oI;@Ugz$)V9-B0xb=8`Vb-$rTN>C8pO&o zx_94gkjuQqk-mW`CbF-G!@!5Hp2AUB>#&$^=wvzhl)q6(Y5trZ&w7_2JCIjO#xgGV zv2L5{?@O9NmD?)VKe@Z6@gK9Y!>XT2%aZnrNH5VU=aE>lB(|RK`34qKek0)7Xa#b- z*b!cn6&mpe8iR&lKGlEB-`e4c#(t&92hy_HdRs^V*9jLBo)=rL(mrN&?=KPpHZj$U zB1dI@vkM{{92zqmVYEfe3C+gS=pUO0Y6p4++cF6U~A1T##bQ@iybSkD{t?CTtY` ztB%<|akc*Dc9PdkJlf+cLg`HDIx34)KLAl0?bA*hSPmO#5I;;ez`TykXyTr&C+@0^ zem`5N?=CruCbTH`OPO4bV|hn; zUye(z9p>IF31Nm;!Y0k#?+hu*>;1pyJkR(0Ip_KRlYZ6JQBGD>76O6D;jh@aANAY+ z?>PJ@rQL3#A&_Gwc)Ls2Xwdap7*qX$k|Q`%N~zzSYZYZ?tnm4K=57q)G_>C$$8gjD zWBXgy_?yD84+_T_^1~wMSiup5BSq-ALYtG7!z`UY~`qfZnyY1;JY z?UYTVA<3JK@ZaWda%{WGn~p$kFHhs;r0o^P)vWR1{<#tIR;mQDyAsNchp8E9>Dq{2LG@9=I0&+U&mJ&SI5;m;Q#4&uYQIlUN@ z;oD)K2RLaY3BY=Lvo#mbJ0i=q#9p3`6{`3IzSSjg_6*hwq$X1oQZ&5w9rS}nHq6^opQ<>(@(Q( z2Pv&3`)6y2k+80Q(iDLXC^~D9b&D3K)w!h6sEzXaHDw`nuZjxd%}iV^Mi!Q1Kj9Vq znTbP>DE%f|xD9hoF;8-ShHm7~POqaIR@7&S7pSziouylNBQ14apCI7Ej87N#R2#OC zjPY?opyt#`WT~vtUZN?Fi_u6>=Z*8Pe>`2jI7zE=9gBzpV*zIwhq_2bRy~iW(HV;& zOln}znp`GMZSP!WCU~PXxK}7tqE!tWhekz7v#PLK7?pPLn(y_eQD|0`x8nO$7rw;6 zECsYnVA|G&td55zJ?&BFj&I+}kU0qXp3UVof~LM9Ab1hL+YB4=dE+t3TwXIRFNa8u z2Q&#|(fY8?lrtfG;{Xg{blQ}fqrJRd05zfIZM2?vjV6p$P`Z%--l_m<*vq_DQEbFw ziwab*zk)W<1E&-F6tGjczY`T80SD;}+s?oCiz&Rgn0nJ{Kh z0QKiGw}C+hN`(L3OzF-9O?SxgVH3YU@>*~JT%Hpej0%xnxZXIxjnE?2E$SPONf~2{ z_G#=E&lnDz!1ok*hJ8%0T%mxZT*(a=@{N32Ln7fH>8zcVA(`EFykJ(QMeHQVvbVp4 zAWW*HH0AkpSaQp-I{fO!_u5l*6Hk>ul>|W_trNdjDaL7!z z5z6fUZO>h&^}f_UC%Y>g_+B-U5gV%3s5-jjxY;Uxtxn)eW#&c*@o~>6-EbCLvQ0*E ze5ua_vR0?8y#AWskGtomw;idB`cPRzJl{>{Ed9tIvy>(_ZK<}Fgs&&p`S+4rTfb{t3gknc! z@^(Z=-j^tdf9!~9VDwI-Ojod8f`_~oPCeU*2J<}_@~nnMMu%jL zZTihzimuU!I3RLLS9M7Voo&x^hxg?Zdtj2E>$U@*fs&CZ2G;SLb;QCYtZvxQ)%u6j rsh((>M%_H<iP(Fv)}uMDkdF1RG#F8^GZch>bK=cPmX?Ni+*u(N8a?tLS?WQvWhhvv(t3lx^7Xm}V#s(5Jb)Jk^r zI;q?wGWk6VP|X^FB~xN%$Tm%xjnaP&!}W7o;jNL8R2 z&pyrpD&_f9)yEm+RaL37npIOR{Mph!N*;zUpB}TBvHf$cSZeUMR2j!tr+c3^_dE;k zi`$kGR3|sf-tpc^m9Nhu^fs+qqx*W*$6H|f)tjC*+S#iQ?Ao+#&1yDdh}=Pl+$u1= zw(Q!}t*Nn*(L41gOU_E38}+At5j5mElsp)lLXKJ7?oL=Ys{J#I+@A*Gpp6?GK3<1?pHByz6lhc3&`i1W| z_n)XJ?OPdnNpIiAFuyaA8S?9ImQoky^@}#puaUC|=V-ix>wn3v|A+C(I=I)$ovgfv zhEwy^4K~i=_~w_J|6=}i99Cob%m7{H{J$kKb7|o2nb_Hwvb6T@=o4UyLE^8mHs#4D zh#rsY!^X8+)WLe__d(lL)|2y3X9OM31pl*4cV++e1&WMODl3vN9u6j zwsI-bO`=Gpqc9@xDyTs z6#Z=e8zU5>Kf|vgReRY_C9>4YuUFS)(JSi1@3N=oOOhns969i%-~*0H2hPvhlq8Sw z0YmC7w%{GG50QPM_%z&gxtL-))o0-Ur7#RWvs{dLn5w{oFz_KsVmqoRJEhM&bLF*l zIRza&W^$Y}cw5V5&7drB;>?GYKF^i8Z$&`p&gsIJrGu^AmzE7+3|p4gRK3+%Ri` zHJ2a6dKmMN+u*}4XS&~6FnczWR_3E~iOEc?*?^009LK>7WqiIWZ^^)i@N`&=YmEA` z+v8od&BYqoPgAz7y~6QB)ANX47pO&V0cpa(Z*6vEYeF063-AO{(7CTa-uMy1O|=3u zKjcoSAKKw{n;!jjw5Bot`%Sx6jkO6=*@B}zRWx&!g7&DPt=r~gc0^jd3uO|j>%iVZ1?JVwx+)m~P8GPfGX;;(q#16Yf)Dd~Z%4=z z=UwzJ^kkc=3ueYMboVKo88lEy?pEQoZ$A#@m?}oacYh|KuquMBzqLE=8nGT z!5|%HLMEnUhukB(>m#&sj9ALZlM9J@Q0m0eD8HhX2^;aosICFcr-{#S$v=szBR}`c zzP~0+AcN%}P4nH6anRUbe_G4cDMcLQ@p1QA#LWQd<}Ggfu3+qX^4P^>9R|QI(4omc zVi75H!3ChRB9pMXl$~My%ivQ&Pw+vIVy9v`;2hZ>ofVse^lKEm)jAc-)ZwZOwrIqy zDpm;;x2w_UfxN6b=AW#y2$1%9m|SaoWS_Gt*Z_R^w(KPTg3yxakIuGQ7RNPSC9{e< zrV9`ZnZsu!v5&pgvNKne8(H_QT#Q9gb(#n`WdW75#4Uw(e@%xf~`1_n}Aa7 zpMX8j$n9Bl`tTm$`K9SdTtSu~0DEY!v%?GINYnVlWRHgA5VB&5ScC|qR;)E6NkM{I zDIJt4eqz9cjtC#GwVPN&kR*mvvXCKQM8oykTg=j_tcr?tl{W7`sN|wCt(?nqo9(KD zG_$QIvi#8Xpq@ySizMS5A%P{I&WchJ>h~kwI`>z|iuV4n5&!TW$$vUK){;vt*A=>?LF#e@^4n|idj&?KQ< z_SzYGUQ7N#`_>RW_&mU{#BMkF-=N;I(g@s}LqYe^xT`M|6MO^w)IlHH-Zjz_M*U8d z>Ij&NpdV83n(;SZ4cyHR{+4~i!QWW0q*OVneG5pP#&0=@E*8z(9_~Nbd(Gd5OLSE- zd6C4_tY%f;E5oDCmfyTA%cpanM;oM=j2Mdzs2m7fH^N?Na;Y2Hz)WDXP5jPC*rXq8 zeOXN&F?bthq0{4vs=APPxQGOT-i4*{g3Z0sT!^I8lF9%7UFg3RXMxt72(_4x`)?@+ M^GEnGd=l^d2hg5}T>t<8 literal 0 HcmV?d00001 diff --git a/resources/builtin/roles/fa-book.png b/resources/builtin/roles/fa-book.png new file mode 100644 index 0000000000000000000000000000000000000000..f6bda5d61c048b2f86a14306f2867c69e272e807 GIT binary patch literal 1774 zcmVj1g>Kq>Y+VW;RTEr zdW&u7fE+&)j*9pI4?8(M+vhapuijX3hb(5jY3Ak&C}bkT<+Gt!8G^wS5VFATA|YQvEQU#Nl9 z)&^ub47CdA!qJ%=M?%$F9pz!jwL}w+9QZ^Jq^2eyM_}lcSr3lF3UD0OwOMQE)kYJL z0x%)<3~R7F5^Z+e+0U&BNIWEt#u1lu9wyD`p0*`nA_R`Qk!Lp|X-)&R0ZH?D8wd-M zW>r)dj;t`w>K?sQGU-?3ld}DER0shiZ{*pPNSf0e5o0}COc>&f<3R>wH#%unRYV{L z_fST=%Va?G**Un0#z7yv`B`p&iM4!+~)1EWA$#j#Bh zBJ=r;qrD0oXDy?9`>)6s{2miY4{;|Fy>ZN^v)yAxwNA(4ub9oWiE`SAf=+D_`J6jk zO8E`sL}(zDJgl%ivpjiAaK#6H16dgo$F2;>@levNMv2em6e5@UKEoR5dvB{#$y0m{ zp(UX!9<4%tKjM*R*!XLkbFg1_#cP`MuJrPSwNyTEO^7_?{~CREC?HiL&oA58Hs_#* z5ZRi058Z<*DO!JRa}E|09=CyPp>W*EfIXTgU)P+27!q>+egkO)6G}NEo?q?%+U6XT zl#PA91KA#SlN7TtJR+?^>en~tV3ZJ9gx=IZpnI0FJ^S~zl=nO7XrTbeAm?MHX88ZY zy21}cZq7j$A+jQU`!j(o%{e$nXl%e71wiIF8-KR&f6y5uRu0fWh-}1fYhaLA>cl1? zGMn!=9V8UskehP?A@U-BA0dO9Ivanw`hNUUnhGFtbIx=jGQZz<;v7(cah|WZ!IIdBitqu`20CHN;JjU__Lh%U>W}A zoK|Fq@{5ZmP0cw6$c#UTDFL#<+4ysW_xL{k=A0UI$6=xfjN;~;C3MGOqLbd{9K?{2 zcHBvu5BTrSK`DacFja0!*_v}k(;0uzTm_C7YK%WPqXNiiXXDRozj5S+QK3@K#-GZ5 z0~x^&!`>r^3uH&kAix(DK$hBE{A34cpb0+=d#Ml&g~-N!*Rbv>0U|f&Y!)K3`wXO- z5*$aIYuK0leqmiz0;G-eoj^T&#!*WJ4106V4617-5vbWmf1twngI+L7G{|{NVI0Hy zoL|M9ZR2J9eO+BwgX0ro^u`~|fl;Hm;s|pIkt==0QAP_4d#Mm@h;CAs+wPSae+~(; zS$qXz6lslo%_Noa2iFw=sp7okZR;IcR`j@8Gq1DxwSe#s=S)_6-Zy@y&$E9 z$RR!hnW+TFsSvN^Vm<@eujhjHNQa zZJu>}7uk$I^)&e0ZFO5@7oxZOy^p?#9>*5ML!Ecs#BU%MsbL&Ih5sDg76RjsaJLMT zzTg>7VFmi33bMFQSmfClORaYo3Ly3i{XbNajX!LMh~HBH!(M|Z;Cs@0XzD!Sjj?KFNIVo`AwN9x5w zUy8+gjdC593!xRJ zVGl0ij@LSi4VZ%Zv{T$)+k*oF000mKK>uHNBNYGu00000000000001paFG2aBZEA$ QUjP6A07*qoM6N<$f*E`)r~m)} literal 0 HcmV?d00001 diff --git a/resources/builtin/roles/fa-briefcase.png b/resources/builtin/roles/fa-briefcase.png new file mode 100644 index 0000000000000000000000000000000000000000..afa70e2a3a92b8601a99c149625b4834d83747b5 GIT binary patch literal 721 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 literal 0 HcmV?d00001 diff --git a/resources/builtin/roles/fa-building.png b/resources/builtin/roles/fa-building.png new file mode 100644 index 0000000000000000000000000000000000000000..d033366cf7fa759e12d928f3fb78cfd29aeb529a GIT binary patch literal 1088 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k1xT_7rin5zu&{f&IEGX(zB$Oq%b>u+Y*2Jt zLC}A9|I)XNKs8XXEA`VS?fYjo74!RDl1&t8Kb~Fl`tOb~$&I%uT?r40o_I}ek+sAC1q%2Mp z-EpYnk$KX-qw$Ye6(bXeLPO3oCKdq)21ayZ!sV&DJHPA>oqzL=x>WcsjhRNz$L~iKTJbIko3LpDZW;j(FL1r(y2L1mnlSp6r(QdUS8398~Ey z-+6mg-1!YBW#^VmIDF>D9N%X?pA07N-u3G1m)Un58yG-NxKhr*bV4#MXSr|LtS6Cb z<^3m9-}FTLe!8-xdCzj6qc;xcehT<(xXWbVLQ9p{jlmz zpC?bRot~oa`7P_}vwhe6mU^ezFil`Tb7QXA&dVzDMmx9rtUG>gN4w{}$t4pOOYb`Q zFk|xWjjz7)%((N=v7w-m3le<)R(|&XyKq^4_5I9@+tu-vUB?P$9GeiVz7Hd8RXP>U zX9WHIf3>5p|HR)#k4=N7%(-GLcrx5{z3%^~Y+Lu&b$G(e(r@~dz-}H#jmW=neI`J|sr`@+~#*>$78w zO10Rv`27_-+Q<0*xj4oc3CZ2V<>aU+U)x@Ht(W~xp!PoO)XVCZT3CYHMv5fH!+7B zBQ-IxR(c7m_AAS*j*t4bbzb~8i$jka_TQ9f}p-nQLU;oY+b2537I$B1!tvA93R_=iTYb$h*~j;5 zec#2Rl%iPm7aL~1p27R;eRzq}#q{~V)!7xUUMuBY^Q`6BdiDwP*uo|YD)_FC*?gTD znC!r54;vxEa5F~1S~|jaJws;I-o+1hS%$HCNU*}R)JR&K5HDojsa`Ol`VpHEkhd=6 z4qvQ@!STwK7IL?O<6C~*ciz4HQS}OMX2;O*SKq%qZ~$d$hY9pV4bG;KST4UTZEs%tm*$F z{v9cqRQTw{G506-RsPE-OgENs{kJ-M%A-SuTpurN5SZ?5^I^lEY2QBnDD60I)w*TQ zvuVelnv2}EFW=*C^UZEfF(Lh zm48>NyjXS5$gWgWrC(!f#)n@Qf5)$V~tNTow^+# zrZ{ck3k#nZ(abSjxx_4yQK(uVF>j09YUvJ_Cq9QVm{;bcm^f^c@<^S%n6c=vgVt^_ z-U;j3f^JUFXy|ARNmXD=0wF`j%*8tCN&HTMl>wTmnNvQ+T1lR;yY!V$ihbgyFVpzG z?(;OxlkE5~)p*XDH#|Yth+N7wM>WXt*C9{& z%Xf|{Mb9~X=I1|AA=^L}4gm#+1_nkZ2;Z#jJg(YWhdpK3>3Z>QDc=OMu6MUA9n1nsw#OBT$V!}lJw@lJRNwQp(`=4r zKDPApTK8zvMVaTS@fWjRSWnW5QcX9M`ORA$UL^h7Fs4Xc*-+MdW6WpWqa{64W~q5j dQb9&9IiqCPJ)Us3)f1HAJYD@<);T3K0RWxWfm;9o literal 0 HcmV?d00001 diff --git a/resources/builtin/roles/fa-camera-retro.png b/resources/builtin/roles/fa-camera-retro.png new file mode 100644 index 0000000000000000000000000000000000000000..c1ac04d27bdb7277d69b16fe11dd3bac6340c57b GIT binary patch literal 1477 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 B{IUQ5 literal 0 HcmV?d00001 diff --git a/resources/builtin/roles/fa-coffee.png b/resources/builtin/roles/fa-coffee.png new file mode 100644 index 0000000000000000000000000000000000000000..8d0850f0ee7fbf5fb326b65bb569e3cce769a713 GIT binary patch literal 958 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k1xT_7rin5zFt>WTIEGX(zBy>f2oyY|@O!!0 zQM&|AAR84}SU*3Ib5_*#U}@m__eZzoZWa(pnwE5XTkh>`b8G%B`a9q1+1fL`>E~x& z{VQBmaYQTTsLrB7y(h8rp1JX*yH8ub+~`5VA_4jIaJ4HrHySj~v`09fZGGOsR6B9& zRhJh+N2{2Hv!uRsHvOG+R#s(p`i0U7G51{0I~M)aQRbVZ>j`94>c_I5d>Y9&>Fiq> zmFe+@u?`bT_8q!x|4T_;u>Rz2E{;j(IRA24`E1lQ7pT9~JEh@M6VCyb<-$DQO|85f zIvv>(Ebl3|?P@Fvs1xY4;}>wQ@ru;2?{Jy^yF>Au2KQ6;i7!5j1-_hj%K4|G$}}mn zO-p^I$h$nsSafM$P+;AJz(tp)PVM~E>=EM@vSacjpa^d#SAU4nDSjhh04@dz@C80h zKIID3{Vb$6LsQ+Ay>j*sXZt|En)Z|f?*;Zd|C#z!LE7Y{{gL`t*4wQ*j&G8Aa>8Zt zUcN&@E=$jAYOAs>JTC3Bv|3x+_1wz)Q#1YUuJ(KPb=71O$B0?m_v&e%KXr6+=fh8I zvZ@Q7Y~4FK=gii)+p`nD{{CEF`Stf)udeUy79LF7!fxO(i}$#s9=_(X_)b{r zVTp?x$3v{{IG7yE4z<(qHaWKRD%*~pjF#uCWepDLDD9nZuxQ(XQ!Z8E0kadlmA1~- z67<_N^A?BpC(l#m$GNh!dzMIhS@sKVm7exQNU1e^!;~}S28WK#IkLIN^~!_KbB-|ec%4g@+;#kCj6LMR)3ZIaf|QPym^m1~bs_`w< d8Z~DDc?$Qho<1Y<_FMu;#?#f$Wt~$(69ByCujc>& literal 0 HcmV?d00001 diff --git a/resources/builtin/roles/fa-comments.png b/resources/builtin/roles/fa-comments.png new file mode 100644 index 0000000000000000000000000000000000000000..927fb91bb2e80f2a103c915f50cc5bfc9141b17b GIT binary patch literal 1739 zcmV;+1~mDJP)qr?$l|U;qFB00000000000000006^mG-36E9HWz^5YbWbrC}pSVoaIwy zW@c`enW4`7xBkOvI=%UO~T*4gg=0(2bch<6% z3fo!BQoi98?qeR8b1Z}O5KGW_#FRLP+xY=a-S6DXMU*K@1B5rC%(Z-pHl&}K#t32w z(xbrPJW2&^LN9YXiPR<_Vl)q{Tz{5^h9u+p|RC z7cNJgDO}z;1kHWti7)76JL*9P__DV881fu;bhe7W{Cm6J9|We66$qF8RsT-$W9hJ zh*7WWG2X=0AzMKdPGmK{jbgyTP9Krrbky7CiUHHm8hS_Upbho5K{4PH$-QFv`}r#)|<7nxf%i3!kE1 zwUeS4a0Z&9GsG4SK)vcrG2++s%VtZMhkDb;V#F9URnLe$#QchSQH4G+;+^zoRxDya z)Qe6RBSxSpTP=2RjyFf+4EPL9*)Fk-o6ubM8F2#-&2ACfh|bBbag zF(2l2vz=bq3vQ@?wPqcKT)rins=OVe(7L);+(675Xs%ja*V;8ZwjAtCW`E((XR7LOqJ!r8+ zTtn#m?jqt{G~-q>e)eqR0=a{$x8l^V61Z zqv^9SS!m(j`rUL$hUn#?3^(%aBT6yG^CW$eAqIIigYh@Fa9_>;i0gn%zqQBlU3T;A z_N5t%tZ*BpOegARvL%CAl3f0;gr%ID-SUuru4gscv`!MYF{j#nW^Q3bW~;n0OyhU7 ziLIwBuH!=Uu}UT}KqO7aFv?t+$xPRqB;r02HvY$F=mAcrx9OMbF2-^>kFwsvli^)Q z8&@-#6B!_BzykV6j%FN#l-QSj*pC55Fpk4Hj`Ntw!+gmu3#a7!rBxAeIeVNM2t89; z9UVOG*h9?%4&ZsziMl2D8%lPctYSFg6VS(OcA{46kl^1Db3C6pHX0hx$?3dp^IyFL z1|%HLLY8Jek(fwO4GH78nnziQrmVu3b(}qZgGg!-5YxwKj^{Gw@+fcfIX|@z2SjE4L{vJO1HCkV)6NjplJK?w#ef&)k-3$n0LMYo#OS_WzIJszR2d%eNk# zQ)8)BD0bNV^kkK9%pN~=Ud|Uke&R~=o5GM!j5{wsQJ6iQ-F^SE=Qq~cIBctbuJC%2 z?TOAQ#%8XjnvoZk^_JOmPP{4qDgLlv`{W=o+hB*Z)Oxq*$Isn^E(JM+i9hTpbttM4 zb7){-WMbhEP(WjNh%xRI(^ZXibmT#mlt5K#h^jH^-}mZuj1Uv|qbbE|57clWS^KTO z;{V+Ls_V*xrfzjz*46uUXOH!=JqlNM_!kFt=!9);%4Q5dw_Lp`{}JRzuTvM5SyRHk zrO)yD?b4gYX?YG90D(85iJy_Ti|QdZqZ(8_bXH&O7>GiG=I-8{+G|dkf9QTvauL y0z2yrHK!ff6_o3HrtjiK-HF&E+%xE56|?AKwUk*c&(8xB7lWs(pUXO@geCz0*7G(1 literal 0 HcmV?d00001 diff --git a/resources/builtin/roles/fa-database.png b/resources/builtin/roles/fa-database.png new file mode 100644 index 0000000000000000000000000000000000000000..bafcd1e338b939f7f3f9d0b9f97bb4db62f49a94 GIT binary patch literal 2146 zcmV-o2%YzdP)tp61QHWQe_lN{!A+)=CV=w>}_6ysR`i~)yA9_KLkaL2ro z^ss??{Fsr0pJsNDz!joWT;p*{v2`2~)okNOTtVvw^T^XfMJBVki7Q1VSVxJ<%XK3? zxYAXErTEobD>gBJD{=Mkh$4$V1)RcFqWWl;Tnc_p;VM=AG|0PVtix5Z-XtKOj$-<8 z6|SXHOsklO1Evw215vwK}Ax4N1f)RodVuWCXV1yVU7$F!TMhHd-16m3R zi!0;(3 zG=yoafGGRN{cNA2mt{=C8OC2N9`sQ;jQ5|EGMz2lVsH#Qp!U&0Waw`K!plC%O5Eu8+(Kszu%H6b&_Bw+=-78FCAA>4KN&DGE z7_aa*2?6GFL#J%-yL1sDL%7MQkhz>9sc+c&XU?&hsrbiln~aZgqHO1LlYamE`iPf_ zGKoB#Q6C98l+wsbuKcf~!L%d9ImJ4nL?|VXEWCJz_|Ny@r;rfMbh49I`AwI0gya}R zH{;yo1P9s0OKfF5D_P8w#CV)emea*%o?|l~DwCh`pqw zQt=*h@L4K@O_-M?7%e@_BFoYT@|epd0msYdtS3a8uI&gOg3MwU561lx{Tho1sn(8g zkMJ-~_rSTabILh`%R`ZdJ zF`yJL6I4uwmnh%Joy&Tc7Q9NxC}A5ZRf2NIh{$^rM1W-^R7@_Oqm@jlZWSunNJ8}} zX&)P@!Vy|TKJ)3;E!%sKZL|?2O_<$NHdQR+jtN^QIKmS|#-k+xhf-$n9B-NQ`@gPF zIYt+=sUm=bXB3iSxs=nylWe1xr1?6+|6BcBWgnaABuYJDikUzrJ{-K**m(Xb4G%Up zX{6(jK@LGGsivJ6YuLsq-ey3ruOs}3t6^1&E99n@LMDhSq(eg;KU(5~kCb*oIxLZ) zQ!^c3VT*n>eNC(Gsh|w^Kd)<|BSlD-E4eco==hl;OCESaaUDI<#pehpJ9Q`& z3FUOWh$DA!G^&u4UF4{d(x~TM$yNNqDl#pVAjlR;zmIT(I&4)FO<{JBlt#s?%pt>4 z9Xyn>lFxx8 z0djD#N9h$i2W3m0#Hqt$DF}Y{$r=NCzyi{hk`dyH?76HIyC|1`tdK>FK6Qd}36_yB zsS1Z?deuxWe#b^Cu_an9qP(g@l=K>N35qL*EUI~#AN0%i-eeJBvc*_oNT-My?Bxd& zwodUXn`xyG@0ee7<`Us4PW?L#CjI^c-Mm1II*Q4{9)%@3k1(_7;uxQ(o_-&d>@2r9 z#&axVF3nU?@;B`WY5%1iA)iWWnaPuEU=No@>B_%KTpbXi6m z4k;?5kFX^tRPeTPy&+FXp7Ui2?sI&J@TFABZ2Ae`=v6J|?^$%X8lkmT~ zmojSnOs0ci_@% literal 0 HcmV?d00001 diff --git a/resources/builtin/roles/fa-desktop.png b/resources/builtin/roles/fa-desktop.png new file mode 100644 index 0000000000000000000000000000000000000000..f463db8e1b66d33c6d0a16f7b1f47bc6c5e7c4ec GIT binary patch literal 760 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k1xT_7rin5zFzxqraSW+oe0%HgEY@_1){E-} zZ*R-Jz3pvJQnFOF*qP+pWuNX}KBuuxu0&pK>$QCi$9{|WU0wN~7ia^$$J zdgk6+`jxu7mzYmI(N_bj%^&;5%J<(>5L4cqqX_Qu5XDS33@W#Q?sbN8Q6wOkf% zt@piLEpqcQ-SeeSA{JI@sF$(a+;(j3`rcDseTE$UR|1~IV-Zcf?C#G%xofJDqd z;?!~`xnJG!bgoHsK{b$G38bU1?ReS*q_wZ^NL_3aosfU!N2=QU<)!EPR(hUTmnpm1 z-za+WMD zJ5%F3S!Z%?af;rFtnS$cU8afXqK^ X9NphH@h_hr2NL&m^>bP0l+XkKlPx#b literal 0 HcmV?d00001 diff --git a/resources/builtin/roles/fa-diamond.png b/resources/builtin/roles/fa-diamond.png new file mode 100644 index 0000000000000000000000000000000000000000..34a68c328458416a82a1f68e095536cba7fc81e5 GIT binary patch literal 2065 zcmY*adpr}07grLS++|3O=CK)tJQ9Oixf6}+b+_`kq2%Vh61Jg+Ww=6?Q0THe zB26+;A{+#dmoXt*`NcUqg=7i9F3k?SQM9F>nySB`RdR>wGFQPtp6FkQJ9l+j1%)hC+KJyd;Tj}WK7I{Ar^LugvG96lW{6~f4|vngGG8^4ys zD{2hx;+r{Hduc=C@PotmW&|T^MmVDva&Ee1e@udcWdVxd1;> zE9v_RvsQy{dYIT6iY;Yj^UiIk{vcw|&B2834<(g7egoK&+aO60?c7MP+p|rm;k-DK zWkw%G=f_WDem#jOmw}x&>6;ggG=O%iLnt_YFi2PmZW;b(oVWlF-$;hk%0}78>Hipb z{Y}VW_Rm09_}`V9_+{x7N^S~VdkyEE713h9_2q1-Bo$K7Cz>)nqGj2f5wdKaGdj+N zIbEL3<{>tc>*eSCdULop3+8{enS`!XubqK)x+AWAeEJiV7|}9NU~_SToXJT8=Do-2 z1c6LdB8FXBb<*7&{^>KrM!u5t?YErP3edTF(ecm5 zp?$?62IsHSW>RrS8Q0;ut60T#l0wW50gUwdV_lCNIvBvD?#K)3?cK>-D_$oPkO2uq ztsB7Stb>TYk%fKH06Jo#cQ3iR+N9rBl z9cL-R!OV)>yTx738iOkr)peB?y&Yd6VQys8Us)fW6k|p}&O$eeJR9W$Z;#?79q(Q2 z*K_l-t3SdtKC*u_zjlm;xuQC4FwJ;z7W|7bz`3vIvvB!%?Bnzk6{o_^xG1kmDW=Yi zV&VcagUH!(J0l$)QjxV5W_M=0{@CutwClULI$?jjJ+Nnzq!6QW>~f|Jq45uhlJ*Qv zx)_2bz#Arq;s=Z(n`}U>YS;|k(dkhxhTC>9Jl==aRexVTer{22i&hZT%Jx$VtC1y2 zJ98j7)W~7Up^!}veB-7v?oV&^*Vh)MpFGoZYr+K@Y$dV??Pp8vNMTTtpnU6Am08&8Et{)YE8LLc*U~nnBT;e>yo0r zovE_qz#%n)c9ejoM2x`=>Nc-g(_8y35zx@3;0Z4pD<31~axEgk>j*YOlb(2EX*)MG zBTu7`r$RP5mJJbZ{k9gtR0lZk*rQrIQ@ewD%~IDHzR|kW@0{qt1A?+UEKi9rQb!7& zoZ>7E+t?DJ3pJF+%ldGJ9CB-dyL5N;>-#sXQI|TvICPK^X&8ny7-SDVZi=D(sHfi!7$ja18w?s2a`l-RYf(tGC-Kdu0(}eH@FKUuj2VS*Z59`W z`sOH|e2pMM`_NzIF7`HVBb58TWC?*j#&&MkpA=|_NH6?*oC{C$y%XwUW1DQ@IL1U1 zdatR+)gh0nLcer@FZJp%Zv@gJz zIr(BDZxey3UZj|#E_H+DHJg>6 zvNqf~$y&$=DpOjQ1FcpZpie1;)FAtAx~mT6nB+)ZziDNU$8lHReT6cqOq^ z?Y8$4Jmo#eG1zmeP~%Rp4(TcCIhd^h032NqpYT%D%_|l+dzP>OWM_eB)ToNsp4hrr zxvE1#&-18Do#1r|{EW(A?Inf7jD=zPd_tYoy{+F%_Y>cR`&RW+*Y1R7Jl4ox2EG*D zK96ec0(Y&_Ba9C48jW;QJlqX(Rb*(?8$^rnle?*+=3YegjfNdtl#79MN5plg~lk@8GKQd%BVITbNJ zk;jVE^FZ~ah9p=$W-0&A%}byX!Ka{pA4sC|4~n2l_|F1UQI;J#s^ZiNZohyngRk}^y9_Eu$DN?3k%Pf{{mI9=WhT2 literal 0 HcmV?d00001 diff --git a/resources/builtin/roles/fa-empire.png b/resources/builtin/roles/fa-empire.png new file mode 100644 index 0000000000000000000000000000000000000000..2d9dc3a8dfddedb04aeb2435d8387c2d736fc2d4 GIT binary patch literal 4598 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 AD*ylh literal 0 HcmV?d00001 diff --git a/resources/builtin/roles/fa-facebook.png b/resources/builtin/roles/fa-facebook.png new file mode 100644 index 0000000000000000000000000000000000000000..3d5a5f1f9a84a3cb2e8a56b7e3ad3a50640b8377 GIT binary patch literal 1109 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k1xT_7rin5zu*i72IEGX(zB$+k6k}*u@OP#7 zN+u8!0@9w#ZIL~B$bjen?!L7fb8m0UJ%8xNw%pqha^>>d?*5*4Z)0%w(*>LUd)3~r zX0Mzi(v;Ox_wA95+@s>UB-V{3Yg6RnKdFdslt0|^$%)16nbkDG*{iPJIejf9_3#Oc z36)2dD^XSoO4_#X=&8q#yU$s#js7;wM8+G zYjXGnjyzo5{w3|{+Sj(Sr<4N=r%V^Vt9t0i;;%Lhd(=Ai|C**_dR#8)NBHukPftWB zwA62HJ2ypvz579Z?5sXz1FeStTW4JhQJ5_h+WXCywMaPd>WjT03YGFFR(%tZVRo5% zIZV^xo0^&9=ZePJJ7#fPzMg*WS&iO9u60k%9^#O^0*(hKDVtZ|1IT zv0A_FmSmE7(p}q0Oiwlj1?fDtY0G6>+!|4Se>ua?O)cxYYft9dm(+n=H^Aw^bKAbxGWu}q`ct``fZ z+x@D~Y;bIoNNXv*>7`&P`Y~aH-t*6rg;JbLRs_dcRjk~h-s&K?IkPvcPKj&Dj&&O{ zdv|v*6&Clo>IW(u-8SDhd5+G6MYqo`GmI1UxR{%NSzy zt-vJM;lQFK(8%F}!34%dV`^e4o!JmP**(fEUDryo%0 zo&ZqjC{rPelbc9~`h+;3_)Qm}_%|V-xG_kn9#H6~4^SwK`-mXNT9?KOCP|Q{wIIoa z#w+iXx2<2?mw9*bt$lrR!XAHLW;@_<6?V76L{E4%K5_Sc?D&jR#U2<%HJTIttoiuZ zu<^+^XRZm{tdnjh1CxiCN8a-vzj9YOZ1Z9iicp*|=a%l#9gzyA)#oquoMj2x{5PnP z#f|mT{%aS~Rvuf(^fP{XM%lQlpAB5T`h$=Dc RUa<-!=IQF^vd$@?2>@Ht+P?q* literal 0 HcmV?d00001 diff --git a/resources/builtin/roles/fa-fax.png b/resources/builtin/roles/fa-fax.png new file mode 100644 index 0000000000000000000000000000000000000000..af00c31269b1b615d64a247e0697384c0e9fd711 GIT binary patch literal 1304 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k1xT_7rin5zu-x}_aSW+od~?u{k%58ZfP(&S z^Ld;(3_L&;P;g-V+Z(&t)x)=GbRCxUS$3>uTkdTrdi#3!|K<0cS8A7D(!X45bkjC6 zdusOedDfp>=T>Vi?m3^-{?dC&z=}=c^OIs<*yN>0|4N;}r5t<6<9l7$)FbtY4fhSi zYr8{*Tf+_A_fD+JFJ9AY`FeZLH=`3zXY4+gysO;N?pVwupRZaclFp?5e-kKlCij0z zkb<5ki_yF@P7vYOOi7vZgQfOR-;SsFtO}ofccB|Hruh7Ot^wc4m>^yw1T&V%43lvm4BV zMLpb|n$0)L)b`pny6|6RQn!j*qBP;e!>Bb5>qR`SY!C|Dpv`sUI^&kz{a6mFJx9giqg%8Y^CJaH#nj z(fH$4Khwu+nLzp*)5ojbERR;7=QE44hNPZJ?V{p z3uYH3Uw+b=BQWE3!{ph|)}>B2B5}!M0no zm&}?aedf-NO%7atW?i{G*XQk;^mD!3P$MCx@6?7^^71_6rTLcI*Enq2T{B70%-`2< z`_J@yCdH2zGM#+4eT}z$Z6n_X(a%AX=WmkgJrL7;^{0*LclBrkfjbr_=FM>U6Ap2w z(gY6C6WKlL2LlpW_j&r-9WLlTnK;k?Z0*USkf@xP$(2CUGA0RL%XzP&qr7sbsge0R zr3p;?)p}*#Uy+_!9K4kA;#M8FuaN@4N%thjV#{;wTk1=F6dL7brnIaMp8PxYjI6{D zwKWbaJ}hzgx8A=%<2Or{%6fC(d#4*4cQWtXp7}16k3Y|5TEAiAl&#=^e6*jbatiZJ zmkYiI5(y#6Pidzo_XX>-|3>8#Hlw>QaJT>3W6Zo#LkuYobk z&V3|292m3LSRSo3SKlmOcUbAmbs6cubJ0NX|m*)tn27Vlp;uSEM!zvvrVU+MRHmdBlzO?uZ_xF*4U zwprg)nUvloyB}Jtap>pDvDtOxd*9W`@7k?5&e^;5=&@+;Uv~_rR&ZRMqq03Hcjv|$ z74u;KKP|!=B){D_Fl&=t!QRuaI=36xHHXK1H@bK1nBlvFcfN?Kh6Xyt(yO= hRo}Qd3RZFfX-3b;&z(PKnn;7hJYD@<);T3K0RRNgIvM}~ literal 0 HcmV?d00001 diff --git a/resources/builtin/roles/fa-film.png b/resources/builtin/roles/fa-film.png new file mode 100644 index 0000000000000000000000000000000000000000..2c30ffe47709c40c522a269f762cef66f6364c0b GIT binary patch literal 1276 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k1xT_7rin5zupIVuaSW+od~=YImw|!ffP(Rj z{jc9XY^Y=fDnWpTZOQNTW<51qq|uk1B2f^Q$Yu-zoA>e8e3@n^7^oY zFZD`)+V-z|b2+_MEOx)+!BBoTqo<2bsjw9(hh~eIZu`XiPU#$DTkyt18>jFrFX+1mOqMY$ zCzR$0`E32FaQ?AP<@9+=8r_<=?nvmnJax;0`R`&aTe;H&w?2@6w6yV@?%MmucHjHI zTDB;vw(#B_&!`XoSH@c8?|NpkyE@u=a+v=6{h#%I+P>edvUr-vo{Wm}Ejo*KUb;N{ z)-T>2%WNdK7y5tRcjUmMz?k4y&vz`A6mt-pS6n=ebHdLZ|J65TiliK~G+#c0VWy%$ zBZrFulVFDfixQZb8_hIRQy^Ml_Ts9{nGPPwWhtUhg0%$q&Rfd#QtaaEL_U>&ACzP( zwudOFdlui9Pk+;7Ar{10{^Ag0%t{BD{2=b~6`Q%!C69d6oIUeo&oZ4Sp7n<qN}lL#8Z?x@DkVK=MtocU|-s8)gN! zzSO(fOivzul0R+L&M2w#$!v9N@s*S3-o~DtXtg)s_}#fQ^*X0{viEHyd$_pU`{tP_6V^BwOUH+pkWv z-+T6D$pcXZOXpekvkz&Vcpjouwb$ls`Q$_5qVL(m`#aM7S1UxHtCYHM>&Stm$&Hx~ zZqHJEO&N_oH(9l=#^(3WtV!HYKCXyc?4xi+xvF%vLZ6#UzVAwhX>MPp-DbV%-aaSn z{3YJ`I}QJTD%`D5dHzsr=&6rB+^4PQc`~2D49c1lY|9LnY8F>^teK)vUtqr3#(LIK z+dqjCD*q>CzS(6aP96zi+qMyK(LP1A6b-mOVRDTYvbnp~E4Q7=`oC=PanYc>C4WU%GeNK0m9y zW;9jPcjn>dlpO^X(@p0+`*|+p+6O&7nYzb*X;rg~cN$l4ecyDg`3rBJ!_SFcuN|$} z;$=^rThbvavv&QJZpN1n{w`g*#-}SP=H3_6wKo2$nt2yL+pCk(TQq51=Ui9!Uxm`UfBaHPK2b6KdE}|7o!92SJ-8867#&cke;xQ{wHl&e a0Qj)rFLkm2kFaXel9T-G@yGywq1f-X-0 literal 0 HcmV?d00001 diff --git a/resources/builtin/roles/fa-firefox.png b/resources/builtin/roles/fa-firefox.png new file mode 100644 index 0000000000000000000000000000000000000000..25fd9fc7390ca52f1ea1568db599c622ad608623 GIT binary patch literal 2300 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 W$#uglAfNOA00000)Y^2 z6tedYNLW#hi={FO$wk$>A+`u+0Z=4U~ zX?Kh;R){H~GZ>7jP)5}bPQ>fYR&T#?e*EzDg>$3Eb-^bOUd0Pm+;eHWbg|@XeLjzR z%nj>*(>akUC|4|S?+*EBvEN&hXzN5-QTKbbbzS{N?jT7i@)piSQ z;cbueTFHSnK+Cx_p?Z|d{E9>5E%2&kR9OM@21~nXW6m=n*oO_mv^NR{bQ|P$W6f8s z>v40c_}$ZQqC>yC!x)t(2tTEjaOjpOodX*!|K-SjDh+>(x|w0}w(Bp(No1I`8=rAt z<5bzzw;M{mH!tT!TFI01PBEkQ>Ov^uE858geX$MP2MsO*))YrWOS0H*(I+Q2&9Px* z-r}7H_AMm_(-yi}gppTtwx@o0^#~1~`Nig1bdKTu;bn6_AgOSU{r+C|l}65xB2QuK z&?}f(!HRss=oWD`{)5fAAXdKQ``~OgC5BqW9&&UY?9cT#<=tq^YQ!Z>-m8!r@%Vs) z^OMBXbR^=CO7F#}N_V;3-9Q>@mad)-Df->xLYYgg zhJr+Ud+VRwmx11fl{sg%?q>5OuI=pt*Xv8Ev>We@lB)56E5Krd?_;p)9?5mbi;I}5 zUcRY1e@;)o61q;DD3niX!8X&+N^3o-+(|Uw6b#+GJU-vCN6c5)?DwK`HKg;YX+ojz z5E=FKd4%52cfH+xkE9O(`X=x1J7hrz0ex~>4VXo6=#1h$zM=0Vo`UPN+5_0@#&R+qn*34B_BS+>ktCa;U%VQbv?u$E! zCN)yKBdw5jUmT<$C~`0kVtn@9Yz+!P04Vx!{9bL#fW&{aKLMe%UGwKs&_f-H{Xt6nScTWcFMC4sy8= zz5Vb>mKW6()l@vWwb(lZL>K$rEctqiGe|@^>C0g-L9)o(;kqL+rYI)VQg{>=|IGFy zco1-1f%HZiXY-o6r>Rj91_-qNGc4DzIgP+qO1Pf4?9k-)5LQkq4aJuy$}R#0fG=QRU?Lj1eLf7@*eH$$D^;IXB;*>f2{=e$ggfGo)$VJVJ9X zNP6OxwPr z40uJ`-mv!Y>~a9@F;CchSiMHF5f1G4S7CFdm#?{Cph%CRWkHU8joO$@<~9v73Jp`4 z5F0^uMxKWu6yeal=kDF>9}QL9h4qT@;?k9y>udAI zMnCR(uyQ@k44{+ja5MUPLu4gJ(VL!^aj{mJ5*<=YDc!-#Est4lBC5>Q9SQVxvGaM0l=aWU-C1AiN>rGOn+1Ws>#bN{b zl6Qv^)BW*;xvM8fSki8-Zrx^B_@0!hDb2$ZVYGd02{~xc3@HC8~vhuE#g@eM0;SPKf37*``>WE`F~!- g2f)6)!7pS_-ocj|ewuht!{^lPhhya1heFu@2bF|SWB>pF literal 0 HcmV?d00001 diff --git a/resources/builtin/roles/fa-flask.png b/resources/builtin/roles/fa-flask.png new file mode 100644 index 0000000000000000000000000000000000000000..ce978ae7d3d9fb66f7a9ae398cecd6e452ec700f GIT binary patch literal 1223 zcmV;&1UUPNP)S=^6h*;TibYEEmExKhCZ|gP?AQhj-S=02M|%MP00000000000H={Poss#+8|kcd z8ClYqPh4lI^GIhtd0m{7E%OCii_@~dBuE#hWxOQFCjAc7!>AP6FeAc6=Yh#&|e2!aTLAiqZt z1Q7&51VIo%1QA3KK?Fe%K{&8?PW|;IK@_cUY}>YNdtS!2ZQHhO+vZ>UJFT&&PSp9O zZg;Kq8rPj~9{SO!vEHBF7i$ zvt<0RCvx4R>~|$2!dxBa`g8PkTvspn*A;Ww_(-@1xh~ZCoeLq?yKOuqY=m47AMv}7 zK&~6wSVTjcoO$Ri5+`2ab7%aDzYgo}{t3N{K@5xHJyK1un1W~qakjC{NKhN1Ge zy`=mDxqM-elrLFFD4iFykx1y{*^+}g=I$6=K0RWPlCX2>*rPK@*~z*{n3EZ$gF*Uu z2|3PSPV-5)wS08m9pQ7Uc_gfhT#vO0z;VcRUHd;02Kfp(eoX%aM>d}#$FCT)|B`Sn za$VIX2&*F3v+ds`EQ?&Pob*Qj%i6!lu;gvz_&vjg^5Y+oT=%oWus?F$))HoC8ghJ-Axn6H=!&=Do6q_VC6}hfy?ZN<`Bgan}n3CW7x%+$ThtsC& zOl8f^E3-|y@8toWU9qXH+UczBUT<(}w|hJgZ2VI{-6Q632|@kvaCfE@DnqLuwnVN6 zhDn*Xu-b?}FVQ&<^CQFgitc5D%~aM;_uO{Cx%~KqS^W`zwM%?BA>#Td)UO}^gn%3BMt|@-0(jbMskn7Gi#jp!<-P5Y0d!NR$^abCSGIv`|0OEU()dicN z9(UvVn*j8d-&Pc)a0#lwODbyuP=PB})dZjdKd2x`;V4v*M^@GZpaK`_?XcuIRGI!D zHa_f%Ds-2sngCQ_uQnUt4pgbPTkpa8sAAW%-hv^%LbX^yvjNUUm3wY2O#td~+1?3D z-bD@gZjS^h9D*A0pk8kRP@fA}ba)aqtQQd_L{7sf77Z`VZ%g)uu09BG;G1av7US zSP-TEaDjZN$Fe`m-Yk3n4lAQH)V5IWMQNaZ56sC|i1bV5uo$o)Uy_GofmqEmZbuD! zYrw`EEW`DvL9b<*$UCQ2WPdKM000000000008q!$kukxx(ft4b002ovPDHLkV1gX%M^^v< literal 0 HcmV?d00001 diff --git a/resources/builtin/roles/fa-folder.png b/resources/builtin/roles/fa-folder.png new file mode 100644 index 0000000000000000000000000000000000000000..f6003fcd913ab2c0db12d5331efa51b3e03082df GIT binary patch literal 752 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k1xT_7rin5zFm3mAaSW+oe0#llQ|WQ0){FOk zAK8|>dXDt%ZMnC#j+o`YS<-XE>gs!w`rA>fC)*yYX<1*rutx!?9|<(9nXtAvK<||A zPJMozYwu2kS}InjoINz@MA1$C_sjK;{+-pmIWF;H>Wtve;jfHt7I#au=1k5Hp8itr z{01GFBEKy5-x{l@uqrm+nS1NvzVlY8|4uNklzn%7P2jWMn2EPO?mNqs`tF3>O4~O- zc}~qa!S&O2>vY+eXD)f3Z!_!9e!QWydg-?v#i!PsaNW!HQ~j_Jv*eTOSK_v3&uq+@ za_e)Rw(*3+)%>4=XExrMcI$VT#^nj$r8zd`&1|c=?J(i&DU*~vjS9D?WP0Q#+!kk< zY3?X@Ds%4pDKc~ar_NrZ=8^qd^?KUoYfU%OZ|>Zx@GT}RQ@i8u%(>CrN$p$3j`U^f zcD%hDCLn!2DwlQRc_V=%r)%vT-(2Up&6V_ftLPECEZvT^m%;>YuU)H9<`(Vvrg2-- z4QY_lTU<%;+)5pPrJFgFIuz786q-OZoZv*}0ELs_GEg-$5NaVTVvP8dyy|T6|Mzw$ z(pQ?Dx4l|Bfjua9-tIlW71WpBve_R0-Eo`O*2=uQ@0xp3ZLU7s6>p?*_w?MAX4hjQ zeog2N%3T+|_=~ds(pxgS*MCvXUVba*x4QQ9e@{X<({3{^^_P57DHv4|<2!w0jn0uP zdnYBnj1QZT@+t0gM5gnxM+d)pv@MmD?D_J#)~0>>x0JPlfAuSMR!y_R~j(<6`)8;I$4mgVUN-E}6*ro07lJzf1= J);T3K0RSN6RJZ^D literal 0 HcmV?d00001 diff --git a/resources/builtin/roles/fa-gamepad.png b/resources/builtin/roles/fa-gamepad.png new file mode 100644 index 0000000000000000000000000000000000000000..db3e7df75e5afabd3fda71855ef50f53b470d881 GIT binary patch literal 1375 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 literal 0 HcmV?d00001 diff --git a/resources/builtin/roles/fa-google.png b/resources/builtin/roles/fa-google.png new file mode 100644 index 0000000000000000000000000000000000000000..cd1ad99b1bc8a9a6d7aa881de23003852a89a26e GIT binary patch literal 1851 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 literal 0 HcmV?d00001 diff --git a/resources/builtin/roles/fa-key.png b/resources/builtin/roles/fa-key.png new file mode 100644 index 0000000000000000000000000000000000000000..8b7f810fe58b06f17e305e65e656b08abca14991 GIT binary patch literal 1792 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 q_%X~tQ2gh1_kZ0?tAXnUdWkfk(own zS+p7y31X#7Q$bx}*aayvL98^``ZEnRZ=>y;ed;~W`#$G)o)7PH-gDmHQ}QDr#2ju5 zhd?0apf-%FW z3rn@6Pw*I$cOlbLVSS`kTq}9k4Qz?a;5qZB?BmG;)n;_2gTnggdGoNTW$~M5UOE=? zw9%g{Oc;$(`2$kV-XH#OR7C=1moj%i*9RkrwoQ&5|8db)zc7np_V zso(Jm4th;bsaE}thEMgh@l>jnzP@OM>R{h{SJc8DDEVxIPhF=CkL9F-&ejcvqlaKl zD&%Zk95Fe(qhTstj1v_>C$BYH|K#xa53$vqrRzsFVuJ0|39R$a2CiuT!8EPo=C9p&n$xxpnj6= z7F7;Q6B3IV{1#(LhasnlrKkexx0@=+Sh73@Vvm(PVY^MCkg3@D+>FqgMu-Ru+QVZh z7(l%>qWvMz@qJj1K(~hNc9+v-_yLY3-6mPhDDyJ~EX7`|WLoRaH1bctF$-Y6{WVm7 zFsS^=G~+I;y7DepHdsTw2GmCm2NF%96L76mSaf1I&eRf}Y>;C{d>SbZo16DYw}=?d z*EKmls2R;0!)Xd&pE*U|AAHpTrit0DI`kQm)lpS`CKpA{!%Ao)h%O>n<0qo_m=fOT z1Ss+%Ps-a4DZrV_e@vD6&jSM!A!lwGyZAhN)dvT$$kE8oaj_r?fiH~&YwpY8>xyB1 zFKUX50Ir-3iBUF9cpY|c_W-z|>BxxlbJP$*hx^BH5gTHk3 zv3<%CJE2*?qZ5z1P7YaM(!@eTfNO(@G1?YY_T&n!*Mll7Vhp8*i(H=p+{zIIuJ6TE z(1C=brK?g_BQ^ff(u~-X%O}{M+Lt|1T{}*r^l)IcLR0-q)0qPxHM6ny!tl=lqvNd~ zj1H?yVnAvHgEW4}FJ3mCOTX`q;UPRn)0yJ4q=hcu82Ld3u9eGRXYolL@pheKu|4;g zkCmEsmwQF$0!{Bqi;bN#+D%&|OG_TOejM0~#>x=oM=q|m@-aLWTZ}cX1l)b_gD8@2&~BNQdP;!l5ZmLE0l+@%&)uQxKZ-R64--YOuaa26H?XBa6mCOpBXWh+_-M=5@advM`rMT85 zK^^Lwk5ydi<)j7o9VWbEw3bcwM6{O?lP}T zE}6L$I_pt?gO_K8P*UE6&FF<RmV<#+eK}P9D1n4-A>eNXCs6+ZD0Co2 literal 0 HcmV?d00001 diff --git a/resources/builtin/roles/fa-mobile.png b/resources/builtin/roles/fa-mobile.png new file mode 100644 index 0000000000000000000000000000000000000000..3147473cb701eabcf3e15c0ad154b7799e99f307 GIT binary patch literal 840 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k1xT_7rin5zF!Ou5IEGX(zP-D(F2qry?PI=Z zd3ifmdHHju4R)6lLiZe9Suov<_tpb}^LH+H?{+myKlkTZt!N0$}1~j13_-g zrf$NvIX!Gz$??ihx5PYuYV&^2G;MY_x7$}$rcW}>+-+o7%yX%B=ajee*p8ht49kBS zv}3!JwDNI{t*4fjtd$a#b`L!NG-%Irtz)c3UV2ZhC54aY>|`*rmYY!R*l?$_HF-yj zg0vj_iE9ZC+m0{>iSu~G$|&?6Y%n>-cu9oEBUnaZ-E{5nYr7_FS~cPIU51yZjzx-J zu5g&da5E*=XEviJ(~*~pR_)&4T@)*?@KR2pfq{twh1le3`FTRoEv6Iiqm$$=3pv!? zQ~o?hj`7oDsWZh{3{Q@S&VC-ki=u@^2E_n0^XSPf{Zm`M%Rz3|^DhDu=5N1Xv#hnq zo__*gZpb;CC3DP9F-}^=$kg$lfssYPf#GF9^R>)-?Q4UZt^PDI6a`fNR(x7|L!3jw zcUD%thw+t1%q$Z)zfCvcobbeP+b#P`d=8UXD;M?rRZjgXGC{@h+l~55aSoGMB$NMp zCNp>nEy?)r`4>p5WdHZHbl5c`j_or~B`})SCfsw*1ICl!nZPht83pbpOa4d!b`QlF z#$TI`PVD>saqF7SBGns#-KxAn?(%VO=f+K1y;oK$<>M0DBGsFX_usY4Owm61Eh6US zlo-!z>!Q|Pyu%;8c-N%6hBm1im#$fNa%su+DW=^MpOk1>`e|lwTvogC@r~@l<&IYK zqvIY(@zo#$Ifc63hm~--dLiT18_2g=gaR~;fdJC4li(3tjvk`s>)`sI zKpr9>tc=UiO&%P*f&4_vfRu5$J;&!_@7H?+aU5qr!adqKE+=KaRt(6e;d3nma%jfm z;LMkb0%KtesHb~)^l{Z0Uo2MkD#}T6w`s3Udu?H8p%ag)44qhTF{{dhixz&*%EIqeC8z{I^pjpo z;i`aKZa@^99CB3UCI{OFw3zAL$_=_q&*+P;BQ*?&Vx!!a!N)n+Hp(eGEddZshh;$8 zDj>GmTIF(w28cb5pmJG71H>NK(fK?k1#)KD^D!VR3xRw~YnXt1&rIIoCJtjwYC@;0 z{yF|vf%-vTqLvO@aVC#2o`o?K4)~T!SwZB~;tQNW<29EFHMZlmI7@5Xt%<6#g#gFT ztSC8`Etws2@tGgkP2zTSX&?tl_QE`pLV2cZs0q4>r%U0uR1#a8lS%=zhXhFKyc3M$ zZo&Uuov%_V@pq3TW%XDPj!PtW$JeP9!8}_szqzpRP=0`nG-BRPn3QLV2#00O;8|;#WAB=x-x809 z$VI%&WWMGL#`7F!vNcPq_lKnlBwMW{g2tq@fxHwMjuq2?<_# zU7`XqGkqW@+KVLz3&;f#0jUoIkPkRK8l92~5R(DHfS3%30SSAtwMmc`lO}CU8}uK@ z1~`*Plc-g(Y&s5?BhzA*t7;{?0gu~Q@;}Yw?9tiK{cC%0KmY&$f&l3M>u#h1$p8QV p0000000000000000000001m!;eBF$ViqZf8002ovPDHLkV1hpjN6r8M literal 0 HcmV?d00001 diff --git a/resources/builtin/roles/fa-phone.png b/resources/builtin/roles/fa-phone.png new file mode 100644 index 0000000000000000000000000000000000000000..ad5f77913fec1ea92cda5b2bba01bbc7e9c63903 GIT binary patch literal 1499 zcmV<11tj{3P)E21msoS`jb^SwB9=4B+t|cm%kf_{c+175#lvX$m>@14K1IXFGD*j=5^YI|q~WNeEoo5F zaMaM2)FWv)Droq4Oww_DfQFATl8)mcG!o>)^=Oce5tju^Q1jL?U0gOCgc|og1LCq`IBMDw63N5y9ctFel9%H$)TBD|BtOR# zqusm{#e$<`aOqoRgjjK0lY2wlFtOtp?{^gz44?b-dZBP4nz0&*6o~KfeOg=yJdS4S zBXJ?HhwmsU6q3x(ayVFA2>d7O?5?;Fn2cs@j<^u0_>&t7h0}d-7|WK21_NTx@g$nD zd&Qn(H`Gt<1wz6)e<)L-a5kE`Q^lI2H`l6qpICF;>bF1_3S-b5%@9|p+ju@FkwWn` z_Upu&<32xZ#dIF2P!?CHM`JD!TaJDj{&>H5#_Y;Edc~3>p@QbMC>?va^GR~5G8T;`+jq@MC|#ZRj&Rcb{q*G zqdB`+>^Mf!i0RpK{!|qE{}RW4(E^=I93@^xbGMXI_)B#LNjhUpQblw3P{=QfF+BF$ zGv}Egv12?9G?Jk`WRs65c^gTE}jZ6UU+{yOyqaFM+>B zjaooiGGjX0MC&<{lEjEXR-;B$n34Hb)pZ%RgwK~4F`A92S=W(dICQ>`X6jkSOMI9_ z9X0FoX8%uEnsXXwKw`rT8mM8*>64T=70un%bW2>AZTyqeIW^5`mwBj3HBKUtm@p4D z?(-ep_s&GqgJp%iBqr>Hns@KEkMN9OJ!;(ROp|ypp9X4NgVQL<78phq4bqd0^>ay+ z2QS968V%S@bj5SURYwE#J_pnD>$u}IG zR(K(`FFT`$ zhtYy}m~<8nrG^%`KIt^{axYrY?w8KQZdA|$Hd#6q31=5>e6mzJ8~rRt3)5GW#WAKJ zXOuMziu(oQc?ccSs*Di#XN(!bvHaWgiQ5uAoHGQY`2{56wuOY|uTg+svsXlAWWU6Zsr9sLlm+iL=0XuE}*3R^kEJ000002=ccc Bx!nK& literal 0 HcmV?d00001 diff --git a/resources/builtin/roles/fa-pie-chart.png b/resources/builtin/roles/fa-pie-chart.png new file mode 100644 index 0000000000000000000000000000000000000000..23fefc60c3bdc3a336b792d777ac960cc6f4854b GIT binary patch literal 1587 zcmV-32F&@1P)R z+Jk?-ee?{~7{(EPLjGarvIvq|FXQ`fB&fhxo*{*^0U{hC=_#tRlwXk4tb+)j#Q4(1 zYYgQJBsDc4LOzY-h*6#$NNVapQX3L4j9Oek5XB(El=yry&LN0;5MfCi&Ws%h;s}Va zBHG`k22p@G1tQFjDn~^gAc%7y!k8$2HYOm5qaZ@ZsGS?v5T`+eiqYX{NL4)(>7ondO6);S=s!Q=1tJ7`Kz1W| zkoWXF-4z56(pd}02s}t?>$N<;`U(${+Q<sv#ak(Xq}74^p@{G+eNL@gRk(tLqNL zgLKmMx{n7LVISm$2l>Mm+vZ3jf0ze3U>l^S2l>;k>bghwAP;PV9QPo9+9$R_e(@lG z+E2=kbOj)Jykc{9LCUHE@zKJgJcz2e<3Uu#3lE|yzVRTc zBHt>=$K;qakwdQ41@U^4mD+`LB$T!M_Zu5N$QOupl0YrVi10z4Al7#-ElD|xBm=?+ zxrr#)nIcGjMXaoA_#j6R6+5kxq?xsM78UGNT9WcsNNR=;G6qqz4NCQOGj;JC@gb(sIqHKrLl2qW=NWXtV4x(-cl#pyt@_13f_LERR(%%lq4n*Ph zr6s8t3P~+RuQ@2#9_u7|c0kG_O1HZRl6)kK`^?yh08zUg7D*N;{EZU@+h&cVfjy8u zh~jOrM3S@z@;2fyNu{iiTrxJ}3*{smLnrC1`h`*j+mM!|a>hwswhQtuqJHZ`BspVj z$9+Wo))*5KNfY}ZT@VMhIuw$h2<(F-DR6>hMF=DljE0zlII(3WO;pJM$t(JQ zwwUP@$()Rm{6tb8NG{(xMKUvkBwdZhXn{Dj8756sD>lhB5=LhncZ_6OMo8W^TH`hH z9V3}y(nR%Qkc=>zqdnr-CYOrj0tur#wmC;Kp~NIV@v_k#xqJ~Zk{bU?QqQ;$?^EC$ z$=DK*j4-Z5E5x~tF=?Xa-$=F?mtu;8BqNJY!WjbNR%~#PWJEDZ?vra=iv;@}BpF%^ zk|!jMixF_zL6ROtkUSyJxEcW`9OJlO)PFY#<8lP-afss*xhCq7V7((8M+i(jVibZn z@TPoxq6zuRzg@*G-r<>2jBluF;urxd6mpy(Y2q7A`AHSWC=w>#kz|t+j5EAte5t6# z$Cfd^r6D06tl6qS!$`twbeDSlxR zd8WAeNKi9t@2gy(S&5!f`AE{10~w^aLAO$0qf7+`90RR91073rNgIp3# lmjD0&000000000000^xSFBZGvl;Z#Z002ovPDHLkV1ljX#0vlb literal 0 HcmV?d00001 diff --git a/resources/builtin/roles/fa-rebel.png b/resources/builtin/roles/fa-rebel.png new file mode 100644 index 0000000000000000000000000000000000000000..1ada36553be284ab478c96fc479839796e7ecde6 GIT binary patch literal 2394 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 M07*qoM6N<$g6XAmHUIzs literal 0 HcmV?d00001 diff --git a/resources/builtin/roles/fa-reddit-alien.png b/resources/builtin/roles/fa-reddit-alien.png new file mode 100644 index 0000000000000000000000000000000000000000..897eca4f39d7c13e27dbf7058037f22b20c0be80 GIT binary patch literal 2238 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 D@=qX= literal 0 HcmV?d00001 diff --git a/resources/builtin/roles/fa-safari.png b/resources/builtin/roles/fa-safari.png new file mode 100644 index 0000000000000000000000000000000000000000..fc056a738800f13e5c18d395741fda2b2376f6ae GIT binary patch literal 3632 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 P00000NkvXXu0mjf?aMlv literal 0 HcmV?d00001 diff --git a/resources/builtin/roles/fa-server.png b/resources/builtin/roles/fa-server.png new file mode 100644 index 0000000000000000000000000000000000000000..3369e094b7201aff07682c2ac935f9a13d873b63 GIT binary patch literal 714 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k1xT_7rin5zFirGyaSW+oe0$e*+g&$-hQ!!Z zk!OhojA?22nr8w*CU;sjYg*Zv4a<#!7hO**Re%0F>)z!5i7)SjeU$^6f&vugEjh-a zbfUhNMbJgSu}7haqez|aXx9zF=PW1f)gQIL2w8vR`9{YHbBv$+_Lwg!x6v~7`2J(j zkC*?ds zJ%48Z*5j*Ivl$-O)4o3E$JcVtD;p0L89uk^;{51)B~*0o4~w>=skiT4JI1W#2zEb< zVH3xu151`M0sZ)I{hLiX;;bNVu?QaFQ0h?l{-|J0M{Hr-gg?b1lHRvbLRu5!o!E9N1+NgHCr)jbAn{@(?YN~#J11DU%Csyk(@IF{y zqobRdrPJ@z9obxV!6%QO?0kNU4;m-`!>1`s7ZztY-%jD*$&qh5is!XgIH-0269zB@frQ6Gl=K7Q%wy7d^RD&x RYJN8m&(qb9i=sHbbH;wQZQHhO+qP}nwr#(c zN@{P}n$}yw9M{n7qLK;gJ)uEbFkj;x(xRDB{^S+*BUv4ADLx?imRXCB?eKxg zcMMx}EQJqHj?UnNWLEjs;^P&3aANUsq6!Ix4OB=dOsSAy_)mp|!aXV^6!udgp|FYy z358J=5)9v}kWjcxg%s1wSB0>&3JHazLW1En6%q<3sgO|EP=#pbt3ovMwff**6%q>j ztB_DwRfUAYs0s;&?^H-AT&_YwVS5!)Ofz2Bz9#zmSLP^d1J?LTf9EPdF;<-ti&`!HfUG|uQ31c3fFQpJFpf@ zGTQGIKqJP_xQ8>?n~hkW$&PP7!dv)a%5OZw@Q+u}jzRPW##BNGx#=Mm#SyvB7L!w#&)Qj2i`7Mcau|Gk{aK5WbiT7dhC2k{O$*d)}< z_d%0TGvB63u$k|qNvN6cS|*`x{l^k`$Gl|{>Z?GTne-nk z;T`isC9^8wCA?!EVvE4pl$A&pijH{soy{y5eU)?%%L$Ws?K5ZjV4n;MInVOJvl$ff zndO5&GbrRw3q||NJ!PTjpvfIAA0)oQYn`_q_2d%yL6wKVGMa*D6h3V^5Oh zi&eOVHa?#RSTD@M{14&tX>k)PTizHgIOOFyrmRxT6bwJ_V#IRAEZ^b<`ID6@ebxSj z7vw8ubG@JbJ5F)hEMt{mIob<6>@c3=lKx(a2aQK7c^mzy#D98|m5|@wq!))h*Lpw8 z2}7l~Q=aboV(z8#4?|WU@7;j*3%jfEhE^c&Z7^R6JNCTH%FF2F=(hD0?LJ&8@$frd zIw|O*+P_&o_#uNrz8lP2xc`NC>akOv8O$ft?+g3&q*JWCj84Vf*7}O=@RC`@@`v1Bal>z_&000000000000000 a0A~$mLg=u5GZhH{0000U;11zj+eY zKPm4OwqM+LYhjVA>p8=(;Sx7mG}ceJwqB(LJMlXP&h^&aSs8 ztji}o;epVi)?*6GnWhWGhNoEj@khC+K2xwfx5`;$rvJRnTpS7w3`{^mz=1)rfx(Hv zkV}C@z=1=7jj;tN&d4GFk_4+@_;gf8<#c3ehhAhe%MsOCSv;G}74!m~lPBmg7Iue8 z7tP`9$ewc1Qn}Avi{(g5%38+bjJL)2*i84ix^z(-%k&q!#8#O;H)zZF;?FRR?Y}$3 zK8Q64yIdBoS~*Wj>inJg4Q~RRKd)Tfbm&XMmaA%0H9pnYWiE~S(I4pBF0wSEY}Er} zcjdi-Q#MsDW7}7B<&wIq=A80XFP~rjA?vC+_chmFX32FLM~%x}*Lps2r|dbn+-KVS)SK~(6OCZrM0gm^3VGMp%I*4h-;>$vG8?19 zio845{r#)PzV(Vt?B(rBRr_8{TDB^1lZ52tSljBU#TkaTOBc=gCh=uWsG4P;ahK1t z#!q~@YPTDvT(@03ulUrD{U=|!T>8DKd@Ui39M;-#+omH+Fb zoW2(R`utq4ly8;Wl+btc)o0guZ?b(-U0t96$|DLV?pL)ku3|>y7Z8gn>a{)Z7h#So OAdaW2pUXO@geCxHTxgpB literal 0 HcmV?d00001 diff --git a/resources/builtin/roles/fa-star.png b/resources/builtin/roles/fa-star.png new file mode 100644 index 0000000000000000000000000000000000000000..02b66e7ef25ad6dd932f6c837336c7ff261267fc GIT binary patch literal 1574 zcmV+>2HE+EP)}2w2n3*gX&2O5XAprk4gdfE0000000000Kwf?-&8|5ZT^2?`aBYXPwr$(C9mWQ* zIjpT)lT)vLKEL~^Fx?sSsVSb%9A%tq6o>J=L<7DkeiFkKG{Su@QWWN)2B*17L3oZD zJnt%{_tEH7mnj5~Q43GIOc9ua8udmB!UNRC6RwjTW}^;HaiPp`2X*k63uT2_sG}8= zT`42nLR~!UO4(p0>T1O#m&yd!Q4j8SsVp!ZHR_ENgv+Q$_qbMan1*`x7ZY79F`P$z zxZB0b?xWEkNjG9Rjrw%Et0jY}sBeET!Q~Rc2~@)^E|&x*qgsAXvJt~kRMU;Fmtr3k zs_9q8yI^EEgaTaef>B`-6$aWQF&@?R5hWce{RAfJcdNVzyN?1zg{I|L-f5!BPqMjXs_ ze27ax?{YN`IF04j7c|Y@qHYV7KGkBC=amw^F4|6KpGfFEt zxglyRqin~?Jiw1QL;8+;If1PiqJ^x4S@fN`g>NSx7xX39u`PAdCZ&S*O9RJl1s)W% zQSw0EFYuh82a^Z#X@TbiJt=DzJ!tPV${tHig9Lq@N&?BcxJ4B7Cj)5(zPsWUC}_1b zQ^OqsfI0e;(X_Mmt#FeH`h!6Um8?wzipi?*aw)~d^$0kArB715cP-;Mex_Sm34Ve< z&EoihWfHq0BSw*YN4wNU)k^*_n3cCmZr=_VE2w-)v-FO74?mm1@u?TrO6WAwSO3r( z(| z@gzC_=iT2_gX0mhI_BSBRe|GvvU>a&p7$QdU1W8wI6v(*j$6s-+p2xkTO2p2!!e2E z0WWb}r4q*^l6$`PATpKRMiBdRB$@!vj z92w4g36RuNVqw2zg-sxbhy}8(O(1uO1#+fMAU}u&@}f{-rK(4Wb zBjdpW_2-s=G~zr^&JK_vFMuqw17t@pfUFw^9AxQWJ3!v>0?2JvaAbWkm9YY(73Ya+ zYyer!8z4P4fb8WBkd18sxyl92B8f?rvlkF)27WLMzU#^aHsiBA4}p$rtcfIVmC_ zr>O@L$&tO2s4b!AauKd-J*yo@zWNci(w#tQt9s-2PcpiJbXRF{(N@7H-iNC~jkorUDJ;%Nk6hH*_a*f4k|gHZ_x{6^OAD# zgUBWHdp)5fAQNhVlu*z(f!buPo=X@>+EXu(2MWhCG^8|iah=gPtAH2$b?Gk1(8aw= zzfCWY#oc@FrAx%?mh)tRim#D1ohMl?>V;#xhMT7?t&GUM#<)=1s1Ik~C{(@v$y$6l z>7%}e=!K$%I000E}TMu$|0RR910000000000 Y1R^s0_kSRDS^xk507*qoM6N<$f&m@d>;M1& literal 0 HcmV?d00001 diff --git a/resources/builtin/roles/fa-tablet.png b/resources/builtin/roles/fa-tablet.png new file mode 100644 index 0000000000000000000000000000000000000000..263e50d73391b6d3bebcad4a88515b47081fbb82 GIT binary patch literal 761 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k1xT_7rin5zFdgu8aSW+oe0#%m9;>Uw(TiDk zRF8)@sPFLZs_xo-JM7q{pS}Jq!q?~Nxm2io*zc=)UO&BH*XpI2dnc)QqM)M4yz{$n zp1J#AQyq4c0F^|L(7pg7r(|>b0Ud^HgrHb^2*mn#L?jUlzX^t6lqc?^3=KQx-q8=ixWn%B1^$ z@1GOz>*m@_*mq%F-qusxFS*~g@0s?y{+{+vz1oGgw^vQoj9u*oXdOYQHak||JzkRw#Kjd!vT=(wGQ(u%6 fr{Z}j>Z|bKAd|&brp3X)B+cOI>gTe~DWM4fi7hhl literal 0 HcmV?d00001 diff --git a/resources/builtin/roles/fa-tag.png b/resources/builtin/roles/fa-tag.png new file mode 100644 index 0000000000000000000000000000000000000000..558fbc40fbd8608a2fb5ac04ef7026ae366f0259 GIT binary patch literal 1070 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k1xT_7rin5zFn{%QaSW+od~>i7D8|sR;O|QD zl}sQe1f)HmcWcSXCJnCeXX*X9x3|I28ufpdrFQRHb?(fZGk0st*8JYlvHgmS_hFr~ zN0my!@rA~}H;X*Bo_FxD$~gt8S9aGDoAU3aM|9|SwC!%Ftob?b@F^GXyT^t0{lDK< z`9esyOnP(l!|#bQ^99zH3ry9mW4zfu;e}^fiA~RP?~X5`+!86SPAl|IPvhV z28GY}xqJ)LmP`qIbnYxCP-y1F9mULtR4g5z`QF(neKBD5bk0pRkIn^yoZIAf ztbSCr=;$>aZj~G92jXYl@e@$X*e0>8(7v0{Rj2i``0c{5!$BMV#FET$#mOc=99e1 z9=xjsT(>RM@;J(_Hc@Ju0#MF$zL@u>iE1aUTEf&ScO^{f6?J^3;PdGHpI2Ktl}`Qy zD$~j{?7R|Cyxr4BX@Y0T?JFuwGhKFH^;vZF`^aE$QtTx?Z;`6C%R-5Ot znM?fZE*hQWi{ewWT(j8v%AIhT6LwK-YBznn52$!&+`X@|_0n74qRwkieT}?dzEZOc zl31XAk~w+Sm9mEgT{!GH* z(+bN|Kr=)-EthzlQFQ&}#nj&Z(I$&8SE!g{f=wDUP)zZ1b>$yX zOk)Odf}2$?UtuFdsb?JB`4$)Z`I?#3_2PHpbhXPLnNNKw)L4eg<9U)c;)!w=bFlz)p$?0icT|DS?&%JM9& zr4XVAF41$FPU4M8%JM$_B>S-sm-y)+-WaYfhfCJuL!6NW6xVS^5>Pmtkr;@PV zb2y;~vgPk_2%arbxWel=J%DNa6-@RMOD19Fd3j4kN+kcn;Bp z(q9(E)MqT8=^GC4WWRW$OkGw=XfZym4Eo0ciqz#<2`?t39^|FSH=8d?h%qtcAjcv7*fWwzkvZ0ho{<yRynqb zrQD*8ljF8={Kr^ZV~)9zId*zx7VB!nF;BeFO6AxkdPZU#X(pL(93hb`h|IA^^ei^$ z2>MN1TB;m-CATa#Ccv>za?fIuaiouAv3R3`n-QLDqGho;>OFMje>onM*s~vDO;?sf zxSxj~&RX>4EbbrYYubw%&H46U%d_;8nq^VUFrNJFd$rd&MPG_?Toy5$7k*pb=5dBn zEHP#&kpKVy literal 0 HcmV?d00001 diff --git a/resources/builtin/roles/fa-trash-o.png b/resources/builtin/roles/fa-trash-o.png new file mode 100644 index 0000000000000000000000000000000000000000..03c0f0362a97a3f762aa867c2df6b67fc21c85d7 GIT binary patch literal 1714 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 literal 0 HcmV?d00001 diff --git a/resources/builtin/roles/fa-twitter.png b/resources/builtin/roles/fa-twitter.png new file mode 100644 index 0000000000000000000000000000000000000000..5dd349420c9b480ce1e47cf8546189a1a8aa5e6a GIT binary patch literal 1634 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 literal 0 HcmV?d00001 diff --git a/resources/builtin/roles/fa-user-secret.png b/resources/builtin/roles/fa-user-secret.png new file mode 100644 index 0000000000000000000000000000000000000000..c84267103cdad1f4be3012538fa1e6d37b93b49a GIT binary patch literal 2108 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 literal 0 HcmV?d00001 diff --git a/resources/builtin/roles/fa-warning.png b/resources/builtin/roles/fa-warning.png new file mode 100644 index 0000000000000000000000000000000000000000..a950cb7e8e10844816591ef44b4fc9a56efb5ad3 GIT binary patch literal 1415 zcmV;21$g?2P)! z4K#_@=rOOlI1#mQf_YWNbQEBkdDX;CD8NnTRS}DzK#Q1{KAuE@o-{9YtcoJ6VqVJV z<{cE_9lFg+7h9r8TbP$Bdie@P`hs5b(!>F%4F{N)B8E^~eqhMF^5bmOrnAgTKbJ(A zZeF=@3o77d^U93HP(h2DS6)1g3VPbSvSKwq5cX)bvW#q${@QEZJ$vXy!1=aL9t z`HWIH4npyKMoARIsAN8)6p3?C$6=*iFdnslf@FKrArtN{JZE9)Y1~>O%z=`i&}WVBI0h;!n5X06ld2w?|fdknSwe-RN&L|$+mj6?64oN+u_*`ZHZ9v7fM*CcwxaDn}i$1D`=E{lkVP_S9{TON0zV2@cuynuq; zVZY?DB#Q8gMZ^av!V>1u#S19HM-~y^q6p8^X+gd(kJvH9(isNa2x;xh_6 zplDqY$E|4FH+wGm7Delhc@iGUs=`L$lEQQkl>SbZnwk3To3AJGRD_MjW8Z@J@ z4k}=A3yc*|0c$sBF8K%*uxzQXuzW^gM^wlv78o0#LUyqHqn~e4A?pxP zk*J_eEHHLL1s!2|$7q$W1K9@^HflM?C8)4ni;tHPF0p)L4l2R^78oa^N%)MyJ*Wgn zR{S{b9?LV9L8Umw0^@2_ily_MOI|`Tir+Eh4pfSlGMrIZ7nS5{3yjB5N!IbXq=bi> zyn;&dQD!p=JE77%YJu?)D$R~QmrUd(3ydGo|L_@wV^E2{r#tZ5q*bb;^Z8z9mFyMH!i6ogVk0jNIpQN!={d*>lM&C^N5g@QlKGC`0RZ z?p*Q-%0MnN3cH|8?b4ZZ$@eG&xy&dWhcb4Ibykd{gl=Fs;IYmPC_>&d3Rj?ndQ-hH zjBuHCPRv6IJz;_I1WIVGbxJ&te!i%|Z1aG1Ml6dOaUfzDpG#s7#Ag&XK#e&Nv3~vM zl203TAfMEHMqxM9r~?tZ`dm`41NpwjGYZF}#vO<_E`zxws{;|^R>!!iu?KR6)h`x6 zjXe;tfYmJ?%HTlqnNe6CHTpora`l=^Ud!h|GMG`=2vz7=3yfz`g*LQ$L=T^$N)1^+ z453PWT9X-tJy6A-WC8l$K+?xM=zqDl>vhM( z9cSr_`0fF>kR>br{e^qX=2y%_|KfgNMh$*=>uPf!o~_ZJ+?MtDyy`a)?lorD=Ni!R zT*Mcsp}yckmZPhV&vkkmsN0&{Pwl~h00000fFOVC6$YpP000000000000000wg5{2 Vb~KQP8{+@~002ovPDHLkV1g_bn?e8p literal 0 HcmV?d00001 diff --git a/resources/builtin/roles/fa-wheelchair.png b/resources/builtin/roles/fa-wheelchair.png new file mode 100644 index 0000000000000000000000000000000000000000..602f744a46c9e3bf7e9de07f86770a0914306715 GIT binary patch literal 1905 zcmV-%2afoOP)9s8 zwU!>?7(8KqZ|MtAb>LPOilR`HfXztqPI>X7VW>%;{Ev=_*KuheQK;*eWm#59F83>1R4k{(*=y z@o++0v~=Ks%rI#LO5B3iNSjsQ$H4DdXVQ>z+);Z>2JKdX=d>r1RyppeQH=YUY(K-L zf!}$tNy?*N-)FG}ae($jvY!!m0;NPlm4CGU5VlmE<}u`QL$FSY&<6CS|9N$o^q~dthEB# zVrNueO_cnGIn~U!fsaG+V4kI9ea69f2Kkas8`$WHz4V@>H_e&kFdNvo5D#XdCBhs$ zll(}R4P1pR`de7RYv{^!w_(46=PjutK>|P(4Bvygfb8@}VA~r%+q2>Jet)fpl77v3el)NOeBYz71T~Tq_v%FtSB@gzuW+5E{F(5n9W~sz|3O>J~wtzeLX_Nl9=Zor#HhfpmKo^x(@qU$=BgA&Pm|H7iLDHr6uy3UuhTUhNLIe{K)E*y&o z^Av3v-(q}$2eZ{`APTO`Po&414{fn;LAZmGWrPyX;laF2)Td9wFDw4kN%UB=VhJ9T z%ZV~7$L)Ab#`d|xPUTyiFFA~!fjiOq^7Oleb{@rJa!AtB+CV@%AL9A^$3+(};xT!F zlC>tLRQsBma*89=-oaz?ErYFfvAgQ<$+;QsTjsbHD(ntPAcs=22BOT}c$NI#S{)HL z;8pS#GaL4kHt;oGB}bT;QQ}^_YOZ8>1GdIqz^mp=`xl|DW=WiD7+ZH|EaF+bMouHL z|D!Is5wDpS*iJd&{nWmJFGuQMdFZuyfjq{am`X>D5AOvW8D8fE(rY0?Z^D`EcXBMf zY-R-uS;#Wh^E*!D-b6z&zhIih5vKAno^}!}KZOpi!86LfL{^)yjL-0t^CDwR`igc= z!c)t)>`vSp!Z5DIQ^*N)T787ET;9<6?MKjK{UZ$FK)%5HnYZXwXEY2Eb2x|Z@H+At z$1>g|m9k9aH{99hkM8T)$q17S3NwUR?BsARC5@WoRe4!Io+PcG%az6Y09sQZ_*C3({qM}^k0=zpH6zs|}{tuGD- ztz+{Kn{Z^>0*PLUV{URR{{9nK{%dC+W%>3|slusACH4mki=c~uBO0;D3S9(E0nuE` zIjy&+{|bNdxM0upU-yl{z%2FQwPpWA?Kf%O`#gJ>{ag;E4uvMF6O*0_yG&@hvP@ok z$GlVl$3rJR*x3iP>YaJGMS;;{oAO!f8sEq$<4)#10xFv-y3cF63A=4*ZEtcB%l}~5 zy29B$tSETG38lmAe>_BY8TM}mN@PN!I7Z%aQ3ojY_pd)6CdV!G_?L))%A<nyYAoglPHE#wW&WoAv$2ysi?CYwAh@*O!Z&l{~ID?jP1M7bz$9$j0D{}NBm;*3^f?|e z!LyEE_d1vP_2uNu%=7bJmsp>|?hVwoe%NFAhf-kkuB)0`p4#8gOJYuBH0GXdRW?Ai zH~xKh*^|${DC5zR=E-9(q81!s#ar03@5W3b-BXtfJdTx6Ic_^BKWTF56pWBd*>H?_ zaVn6r_EA{ov3^l$&>^8hd^61G7Y9l*oHy%+rnKLis#=AF8)c!18}j0q+x6%VZ&BK( z=46nra3jM&Wr+N1q8jmt(6tr8gd4d&JU(==p9!Z%bYDT)ORTqjY{2CD_?Z17>LM6z=VY3EUR=~2x-^GRRKp|SC6A=M=X(2yttz@e zCR0Aa;O1ID%bxp7md=!?U+gO^z6EHLp{uGZ8|qj)9zzQBi%IClH`1dYe1|p1!M_F75EUJ2L# ze<#=fqo{wAEK!ON`yn`S9@Tzzic^Jn&L&#`K zT#xx-wLUI->m$FfpN|!JpirAbpQORn@rjwg;(5DwZg!aW5Dq4nQ3R@0yYEe-#LglK zTZGTnC)B#{JzQiI^U#^>q%^OI6r-6nGS;?7IuaWN0RiYuLOyJ~^??z7)J7gtm3AgY zGHV^hMWqFr?C5Xt(<&N=Q@v8-JOMBjw2lWSBdg}m_L>@3MI`WB4y~buJ2Z>Ltb+Xg zO=CgrCyHZB>%;5|oy=davvcQ`o>Ve#I*^X;P7#RAq>)TR9ZdCyKRj$OqPk>Cuusly zOXt6C)hjbBvoe;(97mDkvgkcqBVV1_ZW3B@y^J>CD}!4!l#Anr6OtF!!PN1K7C19o z)*t3l`g(xk$)Yg+PG+wK-+q-$lw4kqBp!dAVPgYArLrO3&+Lx&wo@)^@G#ERZ713B z575g$f26N)i?{7(AMY%!gYWJeONLmj5aB_E-J>|s_25TOd>mDp`J(9uw7%9==NrIo zOzymfDmW0P4!}_d2!RJi?vp}pX2Oxo&ESP2#m{EuR2<1#9Im)S;04%$XBBQqg?UV=ga(7GNXl-0DeUKgY% zdud{${bg|c-DSq&Y*nN?8Vu$-KEe+3EE=@VTuVfk4oQ`)Y_apPF}%=~eQpoqGO9=G zPVFZ-OqFAEI-Gn2Mpp2?cT|0GRpekThP$=KX?Eta4mF>hGxbIbE5}fzdyNbfK5H$K zvwHvYt$O`gt*cS2xf{>3?X=9xV*?H~qvC+|hQy8m(e%b~I%ITL;w*I-cvSIoRzKV? zL4vtiOxDT#aQ_Mt;`(uj#Gn1)ehCs#ikMu``62B8f_3{w$uYFw?b1g#q zj^^o^u_9!i7AIYsGL)_&4tY?s`V)nJhueB6m#YQOPv11q((C5j>OR0@kbiPBL*@U$ zdsI91F(6eQAbFPA>Djz<;`CR)0}fHMRoBio?PwE)9q4QGON0nJ#|#!_b+lckUyMp`7Zk)C=OD z79sQhNR5PlNDWrsf9Kgs%1M9*!;T^2x6`T8dnsFcUyXR$%NV~j9dC1o8C@V*mrkxO znUu=A7G?J>kz+Yq6{w*@%=`YW_@Ow^5pGXkKX;qFLhA5UxA-89tAbLzCg1#i`?twZ z#D-arybnu8y4+k^@`{C(ZUT_=8)|85k46_7)ioPx+&gxK;^p^711-;e1 z>^F){{({yk2EOru!S(>`dy0^_Hk9OO-^j}8c$jck7?7uBGpVs#Ypr;mQF}L&BIxz% zbh55V+0D7M$2ca>wV1bY5L0DY7R}-cf~SJ#=axrH8QiaZ6}N>fUb%mM_1UQx-|kL~ zrT%%r&g=}@@=IKg5{6V>$X8#ExpDk{aXqqS7uHc_m}rH@Ff5-%haL#hWr~&`3Xcgb z0OH2tL^h&Fh_;+}Zni)(cTIN3S*igumy-v??olDMYFh&J7Mk*j{~Tr%7H~3Q@^rwBhd83P>uy%#ZT?H=9k3nE?seUB3v3Fb9cILY8$X7qMZEi(NKse8NWFPX zq?PM4Q=Np~nE22xl#~>wX96)Ax}N+ctKj~t+z1%*ZMY)6XEJkK(;{=1;l~Oc?7H!Z zOi^pR$m~&FM0)k*5>R7B{=Ll~p|N zhvOdbf5uxJA)y)6#e;d>3w%OpBEo}Y-3;J)fWY}F0-XMn)CYk8xIkKvEi5FQNw)`{*r5E&Qa9AFT@f0ee7 z-D1KI(H2;Ekc8iXtei?8sWyl76{oq_kRP^rEIZcFtT2%~H~O!d5kK4JFiBBM_)10tmO`ZRp>>k$&=UJ7fI z2OkOSMtacmr1!YU!@ikx!8 zchft}JvaWBIURCriLOYw5CG*TkK;~AxtzYd%J6br(5S8nz-h`9XF%)hSw7f?Nv$nW z1_#fn((VY7_6ch}CW07wo5VA6H134AOMn^a{pH_IX96W|n3dqe3!4Z+T+_v-;+651N0@*&^3!lN~qoRm(zre|1EZW0z6-?pkSQsx&B% z3lJw6J@kDU#JJ;^Qe&CwZdOX?`He&0%a2X!12e64lK2rzl%T*k59#3n$HX93e&(+N_<-xKGM zz0YOK=l3(X8tyVN{>hpmIXmxW>W!#I9O84mey0)kMqXWWPuq1Jxb zKR66VhY0!kj-j?MO7$!=AaY~q*B63@*WZ90>$K0tx=#O<#frMLY7MhXo8f#K|ekZ3=JhoDg) z%8{+G??KTBr5h3{?r$7(1`1I4;IRj`FVbH~+eH&%oFCoXv5qBQO<9v`_95#AL1XJv z8jNoBR>N(49}FkqF1Vn>NCk8^3Oi$3bv z)RDfVEePUJfs@ryj61H?jvb4v4zF)us58N87a zCr5D0z;Rr!`OY|13HM{IP2Y*a_F~>~cX4CQ?7Cm8_Gj(x;p}hN_CudPT@D6Tf}Xmt z=4sg$gCkBhmN#!MHJ@m2nYMqmkV!xiAkc)XgWpD~_!tdIG{Mw6vgZsy`-0$rJCrR;ps1UA3XcJ}27sgzUu^&B&c$}m+u*t0}- zuQJYrBs7cbZnmJ>P-z*uM!f2=r(oI0lWW3nWOCWkF}KP}*YuBqTr3ez|K$wPHqu$o z&z?YOms0<|e1j`dQ}+6ZC&dRN=C&@Dj(L*$=a(kxdba{Ch-~cs`a8t&Bpk5OskjuD z{KR8^oby)dUYH9Ii%3f1qjv9V3 z5ETjjD1o4Duliz@Yfvt-tJO(#C>bHEunz|{z>^Uhk8vSbpCbu5-Tkdf)51pXGg?_x?S<-+kXN@vObg4)J~B z0s;a%;4mvk0Rcg6{v#>^^x$JM!vzGu?r zp{~J?iz};RU@$KUG>(<-9>(6JEK_qQttXE&X8ONZIJsx^l&^b^xqscFK;eIPZ)+xQ zJj>mhdI~Dqsjxi)RA(kBN*X1gMPemZ1@AKVijJZ2$;Nmfb-5g_qT~43c#652#=wh}W%dSTp%P*We0_$5! zZD2177BJZ4dTc_&_Ei}cL{yq_2|eCFbR>yz6PjSCy(PK_R_+vB7qH&fZ)dY-kqg7U zib0(^V00Q$XW%S3i_al??}tC8C(o9_gD`W_CmLHbp-7bLYJ}IJ+1_3hTtv&(vnXO9 zc!hx^`_gL{i`y=yIe^yp&zO^xd%XN6hT)i38~66lP?)_VL9AWLM=r?fssIC%9`QmU zc|{&7a;IVXDAzT2N1$<AOGo|gc!)s)R$~+x-s;o3DFhb@Nj9|0dEaad>&;p( zszwDGh(qP8t{bvLrEVA@A~&hJLldb+7-b=^3hCPDD`W!1OdmD9=WaSD%6;XU^HiTy z$KiA0_?4Av`qpp4lxP&FGLtdv`XB)-l-R9vw_kd@5n`Juv${&WZxYfx*U+_n*-~*1GAw z^m@Mwyhjd6SPf%ogg%PmONaJ&!T-vT*Z%j=4}+aif{>16^VB!BD7jIfRu$u}cpo+U zI|tXFE68g_^Rbfbjw))^PH9s(3G{JC1u4d z`K`6;^rA6$ueXR&(ltV(H~>m;l0{oe%!qc%W6d3x8tOvFgb(nDTql1^=l~|jLmdH` z-!wO99$t4sm3Kaaw<`;`IjPNT#Zs7q!=-1zd1H4hanM`P1U0ybErm(>M~2?QtF4j` zX?WhGj&N`2>0~yx7dy{({uE4g89DjrL_hMvY~? zsE{M6s8bVT6CXeeK7S!C^>d=Rs8C)7+X&#q#sh_1OMTQ-3R|bF7nYg$H+1+DQ`p;L z-7FO>>!hT*1QT`%A2yb8=Y{tJ$I)1Db>-0x8on9cn0*+A(e)Z*s?2{=dS&N~12#eE zq)GFZ9dhv-%pvox3cXBeT3pi$PaaRdGDDQ_hs}XN+@~eo4pyMmNVx4E+ThSGXH(^h zg0i6iRFg-M-nE0%YGEgi@C9+6N~+o;E|}yE0Dr5u*hj4v_c4&D3@8)9=?&3%R}*jp zQ`tXZ#GO0O$2kc?Sg?6@Qiebv_mPLnGjv|1uVZOlt@S$0xhooGPW@{cUywi3%qA zOh(~0))`>c@lUdi9-gZ3y=h%asEMi1exu)89*`BpsPEssX#%A>-$t^lmn{zhd|2*| zIYUXbGYRwj1CVr1lW%FO!pm88zE6pGeHmdsG8f03p;`qDnQjhE zeiC0|Gla~YYY&ap`yiHsJK52(AGMz@%aBb{O2adgww?M~cl_(1gkpuQ6r6Hc)O^=r zL&e&8FEY^)05gBMJc#Aas+7d|{KxEv{mJgfdc5Q- z3aTr)z1-R279GyXvFi5r921IavEy{T4{s_fH$|qhwKLQMFmoQF(@6Yq`FpGS(u7h6 zYZf|!{}C^9X(8EEq-UU17vJ;x0x?&j&+p+re82s_g!=m3+v9jglpku1qlTca#!+X) z_~cSW+FxDmmF{5p*^sm6k9IckV`g;BNAQUV8T|N5*`-n?tjW*Fq1*tR!cWJThFuyx z6GHs6`Y9^BeCz+#2oPTZUM!}u$ub(h1%3%S8XDFFX+fLa5pF)0Q-Nm0~#~-c>*rEomv{^*$-oMsmn(@sRs1 z5wR&X6fi3?kb!{Vpd$-6l`+odT<0ey8B*Z=O3CRO_@k<*~@s`$ZAhD_jaa)ZaM`gt%14*T%rJ`x^!*UI0 zN{2tB<=Ky2w9KN@eMI0&A$wh)5V|L`jQp#Bx?n9cZUOU!VXq1}P@kZ2t=4Zq0r)bQ z)&SLr;4cT5-A|`JEK4T~@_Q>U=6iETjR7dgjq_8NfVF+FW?}2acqsmA82iQL0(H-R zNRJGw`@-9E<^Vtv=R?(i$u(yy_K?|1EF;t#9uIgn z^z`&+*nT?jj;W=1F4}EktYP?)uKUksb?Sr zZNLZypJ8a#k|!}xwP4yXJR4iDi*D|onyM(d=Svrvi-vz{Hwbsr^5E$e+NVrR`2AXV z;!PH^1zk22{Bn8;MiGX8hO83xww#}v$#vHy58B0d%r{UEgHhGWqH9_8FOOC#(wgVA z4+@J+XMCM*3!t=*%;`rv=qQ(G`9JD=^`>aQH+|#fodF?lM^vmI6?wqzG*HgCu1Pt% z5n_{a=US{3G<*S&*Ie!*e%hq6435+0uROB_N~}nvGNjZcF2Z!+T~~fs`HzjBW*1yQmXTAZy8v<}^ Ld#lG5UN`;)BiG

hcX{0eqHG-BXYLt!`tsU#u3{pb0Mz3mX$F(k1 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 F{0GGBJ@fzo literal 0 HcmV?d00001 diff --git a/resources/builtin/roles/v3/briefcase.png b/resources/builtin/roles/v3/briefcase.png new file mode 100644 index 0000000000000000000000000000000000000000..c6abf5ed42461fd03eb1bbe26ba87735794c0e16 GIT binary patch literal 4350 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 Woh~YmBAZJeTDUlRI5gM+r2hfTA_vR> literal 0 HcmV?d00001 diff --git a/resources/builtin/roles/v3/bug.png b/resources/builtin/roles/v3/bug.png new file mode 100644 index 0000000000000000000000000000000000000000..bb2948a93aba530003b47d34aefbbee65880e41e GIT binary patch literal 8151 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 iqiM4~&ZBSb)?NvJW`Afh;efRu!xi9!fL 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 aeHlNIb|xw5RrE>rHWp^qrns|^8~+7Qp8QP! literal 0 HcmV?d00001 diff --git a/resources/builtin/roles/v3/clipboard.png b/resources/builtin/roles/v3/clipboard.png new file mode 100644 index 0000000000000000000000000000000000000000..60e2acd4f43b697e427ee12322cab45ee83a9bf3 GIT binary patch literal 4737 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 BfY1N{ literal 0 HcmV?d00001 diff --git a/resources/builtin/roles/v3/cloud.png b/resources/builtin/roles/v3/cloud.png new file mode 100644 index 0000000000000000000000000000000000000000..efb644001f07c851f7c1add1b5e6f003cb1d43c0 GIT binary patch literal 7539 zcmbVxXEa=G)HZ`rMvoRHM2Q|GdbB7Z>gc17h#oDB-iFK&M50bYh+YR1y&FWLmuS(6 zZi0#E625uf-`|gSy=Sen)>->5XYXs5YoBw!1{-SAP_a`H5fRZm)zLH|A|e+1@1_6} zBq0&WFd`x@#iyE&&4P)4bxRQwggrc0rHx-{p@hf(({=o@qb;d+y}6TY`5EGaLgNjm z%YRq$2QR6Ib;u7NNPCALrapNg1!)}q6?(N;HZvr)8+fvL(0SD>!8u~mn_DqyUYdCg zyLx`^bN=8jbEX>Yl!ofZ=tMkQnP@Y`H%B)1yX2YJ20YEV(R)B9SApLRlA@%Cd`(d& zmN=NvEJ>NB@EzrB_cy2_M3Y+V`@L>*C{TLWI8{%HR$P)?g*L657sw)j+QtU)fbK^h z#V5X_Q+>4^yJ_?VTksU`+#eJ7IAqZYOtEro^e>SOa(wSS<+sNdQy~!GZ1kowVG^A> zu@Z^MQzei&Uf?|uixj4Rsgt5A@0t7Dis@8V_QUC|Hh=TI81;;f0<+EP`<5Y|gXR+@ z714iT%txQduG^*%2#+DOQp0Jc{>9B#zV^$tI7tSvXV8?k@H>iXT9a8E-$~$RNksF; z%{DOU`tt5kdl}gWMW=6Rnz5{~>uZ}<(;j0c*~aEFz=Cg7V8^S(u(36bz4Hmay)U1`rUur(0St5G z^9?^K`R%TdT(_;CaT2oc%Z6`!E{vFOapL?LV;2Rbw9a~AKLQXJS z=-Qr=W6cV@W}Uoib@}s|IjZSTZFDMB@g-CrKzTR7Hs?{;6T9X!d)P|EnN}YH#EN&# zH7_;F(aht@AkuvIX73aC_{wtO-pjy>dM&MqEZt134K_jf8FYg5T-=tFxw;;q<)Pi} zEitw2n zMVCVvND_H#%*~^zGQvY}KPIZR1s_vm)t7(OsVX6rKzJ@W|3i*5ATGDhim1xu;cV)FiVEEt z%PS;6rUJ`eUROHucynb{kpprlF#GIqHLpUa%5>-^h26tVrgtcIMUa5`P`<#FVg}m0 zXGj>~W+fpYA^z@F<8|$il^HI>2;6uE&YBSWm1}B3g%!xZcA=MidbeW4^zzcaC6jd~ z4ZewE87|gpI98q(f zDl}_R{dsac6q(dBQ%~NNyJO0B7YwOmOYh`UU|%ONyXfwwOmqkwsv)E)V29i}wWC%V z%(na%#Zia~?)t(;nR8>Ah|(a3$@jfG=xA^N==Vq+@bzh2o!ajq75Tk#bC5H_!6cB# zUP7e;lMRwajBoa(pyaDXXFdyCKQX@i6Q%yt*tR5Nd)=%?1Dt$yvD3OIT@U;l{DhYz zktO=UfR$CCF#5@QW9J@cByEXns9!OTQ=wUPE4xX6?RQ-dO%*`(B*_&z5yUx-&Sb%* znA<8w@{%&DW>NV~AsC?OJv$Vc>;d90z>nQpE_M?}Ep}-X-2YVXeMB!DlMoU|rvI>b zM_i&^0{_Xmf~5b=tThu>{!=Aq46v(sQQeig1%2&t}{o+kah0&2U^W z?D;SR8TTnlEqs!inieGgrbt<3f@w#>wLU%I>TdYuvtVvxSJ_Tq%OYe!F2Ca?)QOx? zOi404sR*)JYI`(pIW%GH)I~ZZN^pm*JoTU<%q8}kiX{!xQ%kf$p26}on``Qr^X5X{ zW%w!7?$s=OlZ;x-dha=N+kGA<#akyQW)$5;AW;Jqp^vJEFMbO*vLM(xR@MY%bB`QI-Wy@`tA{|VMs2Fd6+DdU8c$i?IcP#USt z4LDfl8`Zz|fD9xyt5%Cnm%p%ilV&`_1#O`rNz5e|d`woiwM9*+p*)lH<)1aB(1wy~ zjB19$^TDpk!k9Zq%h=6()MBk<>0A*mJhJnb{Adb6&queNtH_7M3CKJP{&6Z_jX+$2 zET(#NB?61S4BFXk6*U~$biX74vP{oQ&>KvMCl}7ZiI*PPeYb-b+3XH_zR`{=j@F_^ zYld~WYlW>L2lZ@pXrBn6i%PG$JG`x1)tKF3CD>5*Wl{o>FqZcO2ArHR$xJN?I^YWe zg1?X{#K+uSdrbV>_&;n3J z=B%wKrBn4Zw5YCs)R<+F$l@=7nSHBQOK!IqbJz`Bs|D~wC7U^A>$3qk9(_{LT!L`| z1iU%sH|{3u?o%m$Vxzdj1B&(2Z4$2UB(hMF!jug~nuobebyDx03U1J7aa8KVGiWT@ zB{6PU<$N0EE)lD1%B%>OW!%^Hsi>Sw>ox=U30pfoIv5I_peZS)cH)EQt~10jCx^OH z1dpG#e04gEqOnjSte*&Woa#Ci;hkf>km&xr5EqYO*sR#w1*)z>xvU`+k|NBRY zpN0j3Z@Jco4voWr!10@<#`9}Ur4j@o?YPRI8=w>=jJ%;}&n}O)r9G+z{Y854QU&q_ zn^1fY+})p_eT2zP#^;Th3NlW}B0;LfWMK0r9U{wS2-8n`B$HPoLEFI5WZNK5msQ+h z1@8M4q4C&UA;0#&IBd#y5jg84qG1;`!I^1X-#2fa{;}%A@!#HDy~3jeLOa42UNg-F zNOlT7Gz~H>wGK;E36k_Sj3M|O5|XovgG$eHuS2PjI^mDv?(w4Qy_;$5fdnrW?0SQ{ z^X$CMXi?a?Q0i29k8Mg0;gjWwhWJOOt0RJE!sog!+6=5}|+G{z$2 z;xbQ(hGU;ni`}}UD1*){WibcijWi)$iuFDcBm+;PY~Fs{7wY2S?ATxnQLx#WQ;qKB zAKdI6Xm(J4DZQ}uq2)iahrZVM`u&+fvA9_3jcp`yO2WBCqdf*k`GdmrHiT*Tg&?kG z^rIqkAY=`#jx?c<&FK({@_ z#ozW2D)sj-_6z@F+a49Ij&4#LBN5GxUHw0X1VQR{${pg15atmXsNk9g!RpAnNrQLG zm1wqK-quU#4Gk$icxC#bW3s&7M=isv^0BzI$XW(X8iAMhpSG=Rzz+)Icyad!V~r)^ zcGLbaFehxx{kWO+E0<}o9GH^ww0iBx{z97ihCsdcBJXA7N^rgRAii&Fe=!$-MVT5_ zlu7JtKDchkDmQt8zpePjg@vok^v*D9*LuA#vSodl-3s0Xxwjivw*C=g+ z4}{$O$W6>g-7_2W#MX6nof^G?2N3{tgxtB8l-ffpCMNGJKljczRU)!OQnnygB*gLd zn*%rYgwoYd)M7VRf@M%lp0Q8p7Q^xHFA?djmF{JYJ!Zhns3l?%wvQUDeo(IU$K{R% z*QeeMUWSon`3)fZVeC=qMP7{XlEtmxlw*XJdUc4Ep@%#kY&lNX>vAc`^ z8?r&wA#Ud}nY0L2$I;u4V2@DFc`@ZW&d`)>YZbbRcA3rmS2M3k<5)-%P1S!g`JvF) zbW-(yxgpf(sv9aap(RFeLC!EchEn79E9|9a26L0nvZl1+a{VI?L&n@RN}LF6Lve}9Ha$@zALn4?51aAoWdxE z>RVv_0L_aL1z4)+(m=zL(5n+iF2fBhDbV7l9Z68H3U~tJ{*Pj;HV7*E?S z7843M^8ObMXI|8UTDqBb?N>k}w9M#c(_`AxWFKlSA26(}7fl~Iz8}Q4F50Lsoe7%C zaZ)lTiqkZz2K^bfRl#q7AfxLWc2^uEp6yNDx50+!c3Fez&0kY}$L}TgyV>`?%RD%e z%_Vt#YsBDiQ6X4`1hh)x>mXDpLhWoI$GZhXU;ecVUmak@3oH$^1#yvp9LeaSUlzOw zt77N4^RmxC-^T1qTuwNjmt)HAzehBaX$qL-Eot=gbr*kg|HZh$_n|1d2lCd=2G}7!e%5vBZ$0L`a^GdYZ&uDr`2ann%Xg)J5MA0L`lGz{ zQX{&C-YYA`Gt{W&EDZXuH0>ZyK1(Mqqf**Zx^i01IccG(Kc>xrVTLUW;MqS0m!SbB z34UM96d((imtgSt#dFvqJ!CGD5-qU09}%pn8RQ-F;V@5`p6bZf#JhKJxM?7^I7l4LkhW&gVP z<|fPt6GXIw6QW0h2#io_m!tojjIc`ag35%*U~{=}Xd9SQDEe^I_Qa>i3oaU5z2jTrQVpU6(4y1NZ?*SEDa4KfT$ zu$Qs308gnh=mS&+{%aVMv5y`~7_${BjJb!E0(Y+_sV)b<<(+>h38g}tNs!Hisr}a> z>qiu|%Q2+vJBoo1i?LZ=3o0&^V+-Ht{oFOWTmeH?3x4rJPUUAP88j&~>0A5}LxFgm|48X}vl1pDwkd6{`t*0S z*$R)fzbA6LX)C}N5%!YDI?^s-TTbavZ^7$T_c_uWn28S$`%rSZl-p*dX&Ypo9{}~TfA3C(?urpmTER*L zh2_V2Gcmv#8+R~NkPxQ|VazfWVNK#jmEJ>h2UeqqQ$Rt721!L(na(a|@JFT-RizY0 zdW>8BbEHKpwS=;7U3^)7r}yHHM_PztGm+THpKsw+&39$+tcSZVf_}f@Eyzs{DZx|uLH)1w5Lm}M`&%M8@KMUE{pM2 zlvlH}T0Gld`r23UkdP`XK<1?S;&&i01X6a&GqB`e7+=;<{<{`4s4v5^@ChMDNHBY< zv6xoI59Bz|1lSUn%AuwCZ^GKMW z7kXsnuCRXJgdtNg>G(_le~(cL{bl~qq`o7J*#1{3mhoR>-`T}&lwH~P?~Dy7t@AHD z$czy4wm=Xh5`bL*EJ;?3v+875RkhsSvf@;mRCTbn$iuKcL4 zZQAX+`VV@cfcXLh2abSqcN0?qim`bMZ; zmyOa3AZrs7u`Y4WMIqjq%D{a$Nn|>5X49}|k3vbp3NQp2VzfcI&e{Lyi2?Mwxbqqk z4-M(k++}{bq#MqHCa?vnJE8;-ZsC#qn^*2&{`Cs)K@CtDa6$Yg6wypH#Tv>V@0KNenx}S_~~vT+AQiH$j;l% z(VU9#A+!FERS7UST47^q?j7isFR;-B4ohr`O@tJtN<{Vb^*NGfR9jU|>^~b0U2TC% zVeX%ag70}A==5sQ9l8v}SB9M3uTDQ>%C(QZRea2dh+n0rEF}x{W3ne>ArWnceWn$+ zTd(-$eX=}C+sRb@h3u0-S$Af-=7X%(zeq)gAD%)wkt(Y>HGh{_1{K2mf^!OM;_KSS8s088YJ>~tv(Ggxz zS)Qq`d?sQt{Ht#kw_Hs-os^Y+94aa_KH0IeP^)=J6ToTVLy`z*&T8th0^bv@mXipf zKyl_fm|F>VU2F^`P2B?D>}~65Cnx>1+CAJ&-6V$#+j|+n%(TJCP;((q^cTu=2@D%# zEreBxXz=-ks>$)s@6GSnAx{aNP zz9vFgnQ{?5InJ2P>awR{l}4K~6(Bb};q`_K7i}5ShfT0|vm@OkmDR`b^`-)i2j_o* zwap87E^%l!$WJdrPwP<8G1(=fp>OdI8n^pd|A1q4hhM!WK=Eu{gpGu`$4#%v=9IYr zeQc|6!}Bq(mAjM5;)N9Ei(Wp1nUJs3iHfEXU+Q-7dOLN4J~tP4R`of{>}|+15j23A zO8-`|Pz`H#D3$TnDJRoZFKvY@lvW>@2+v?sg~#<$RvUmMf9ksWrnEh^L`*fzAx8$E zIN^;kjEQi8YUF+J-Cmyx*u(G63jhb&ml?-Za~_l7NhQ<{|EP0g6ePI_{teTI-^TNo z`S$0hxb+PKB6@uJ1q5gM`=k0!7!c`=pldp@ZtTm_n^;;fZz^6r#UA-{DB%ba)6${Bwm_3Dm4(p~yP1L}+-Au@UMzde!gF>3RNs^fn zeLAt9wjrh!A=4qpF?*lzoETD)p#^&~(SR%GPdx1Km2)4>KJ0gU+J|dOLUg>Lj7MvK zB#8-XA2qq-P2I>gi8VWXW6KKxBttjV@DCVnrW8FTP={}vBL@wM=<9!Ui$DVc1oFI! z#8!C!WWxWR{Lnk$3@(qwZHcs)1X!VR=vUm`9E2k}3+WD^3V9Sl>m9kE8Utl8+x?!q zJ24(Q2rInkIe+`XO@nD1DdB{YBKmKawb2g>TWAc1$dQ-=qy~w`5WOPf5mSQ@^#JHu zc*OokOwaN^;{V18;{V18;{VThBH#adgJ?j&SRA*-J07?HPD-C@8ERImLu39QzP%Fl literal 0 HcmV?d00001 diff --git a/resources/builtin/roles/v3/code.png b/resources/builtin/roles/v3/code.png new file mode 100644 index 0000000000000000000000000000000000000000..b7af59cfbbfb63318481cc666f4e19156df0026b GIT binary patch literal 6685 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 X4oK!6Nxseb2hKs$!zQ^#IQstqGJ#$U literal 0 HcmV?d00001 diff --git a/resources/builtin/roles/v3/contact.png b/resources/builtin/roles/v3/contact.png new file mode 100644 index 0000000000000000000000000000000000000000..6b3095dc3d4245ed9fbddcf1d87a4b1626ab4a09 GIT binary patch literal 6819 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�le4rDn&S3CSN`cOH*Ebs64kVbGM% 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 Z4# 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 Lt0U=zcg(*4DpclA literal 0 HcmV?d00001 diff --git a/resources/builtin/roles/v3/discussion.png b/resources/builtin/roles/v3/discussion.png new file mode 100644 index 0000000000000000000000000000000000000000..e4519d664fcf118dfd230c1d0172280915ad53c5 GIT binary patch literal 10059 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 literal 0 HcmV?d00001 diff --git a/resources/builtin/roles/v3/download.png b/resources/builtin/roles/v3/download.png new file mode 100644 index 0000000000000000000000000000000000000000..f086222212e4fc0a043df574b32c93d6f4dead23 GIT binary patch literal 9392 zcmbWdbx>T*6E2L~CczihGnx>fg7ovKqk(>;B9=JfNNo{7=bQGHB8PlAPo^;li)l|B|0Hvhky zhyWuA2ubk4!s75#f2C;XkNvw>7#kymv)q(5|9E1JdHjF6&T_jNp1A&UzPK=Ux3Wid zcOm{=!B#Q|L~bUly}kXHtSp;oh94q$i{p|-8f`k1$5Qp6shYE!Uu|Tt%9T8>9mIH+ z6h-z02f7P!Ec^`t;Vf`^`#LKQ!G~CSB!_d=1we32k8jC*_(6jDnsscS|Ms34>vixS zS}gzUD>{2%%J3IqKe|(qW1(rS1a!lPqyU<~xC&rv`8Pr@$6Otx&8s-#-skMSy%6f@ zm!%8-OQ--|mqPu_GDp!UOKeb=e!gNrvA2FD16hZs%^XVs&k2+zcHBxMt|+&9Soq0a z1m`XEnp<-#wDm>^3$BMQQ4#@e9~mGzM|BhbJTo{jSC_ZH3&LaJ|0AQ-(y~p~ESC2c zxve|Zj59KCr4&F;ZAfOJ5-DtCm<>MokGsY+bnGOEj!=U;z=3t3=4VpR0U#h=%FE)b z+3_=MjAfc>2jBHU=(s0Q#TR^UXmR{?c93J|hfGu{=WhSmR$*pNc5zHE`QT6I=k6t^ z#nOWguc}8*qgqj!J@r3g0ho7F&7^6-x!py{@+Q=NL)J^sA20+KztS?<277rh6+G~(` zUK&&)u6wrV`lere;P%VaKUJ_%NOV9PtDpLUk=kL}p_ru9hN=4Sz6u9W2}WHXnJQ4} zvMoNkR?*a{y{ZtXPf}M9u$UW!&%(b|SxM(f8-PaoLFbxylc-}||C$GIFR`}?4%fAH zTlt01mK+OQaPU0H#r3?wM6Rdb+(+_q>V;!aPT&z?J>L23=wcO*OijE0fdBse-$S?G zmcL^Uv{A>)|Le||8T4q6qJ=d>x>6byvyW%d5eWRaRjh*?1bN%t}_-t--IRlgVy#re(2QsuEQQ zB7Uu?K}o}{iIp*4kRe%=a4=&y&o(#Cpo9E%So!~i25shKhp`hzmgJLERSvN3&|fWTtKX$F?)s{l z`enT&x!9qGe}Y=huHa~#lr>1NgcrVHu_LP&@##|kVxF{@(0Kp!r$7Ytms?m3bW>t$ z;{Du5^y78Ll}-=b&6CUZbM3jDS8dIn@jfNz&x`RP_zW+{<1B zJ%ZrlTD=_?EoQrUpEX0St`LKFo8L&os=lg~Jja#ia7Fd>d^(x!XFwk7E!SJAoL%Tb zvP%tLwQ&}69~z@9!ff}NPCtp(WJB#yL1tF+Y~0WAOVk*{af`oH$cTVc0E=)1f;KFM zF{E2xRxW6H;jxd>qU?djfe3n5A;glE|I0$9om$wLV#fC*mJ6qWcE;Frqg3>Hk5y7} zt1I8TOKvN!!rX_eDIcRAKUsSYC2-haXq4fWDoEN+{IKD=^59mgUe1fg*fgP`OvnG` zPxEIzd6&8=-q%0s^HvPX}2})CsY5|a%w?JY<3iRi(AN3 z?c+br3RPI|$eQypg(GyNZMP4f0P4q5i(BwY1f;}elrsb^#H_$yhh~T6hNTTF&5r(P z`i6oqvo0oDC|Y43XlsbmUuDLuZm;>%_bOLm9kC2G@yzJI`BcFEjx@!WYFF)m4b9Ub zUrR>wVknV(vZ}#++PM?yr1tS$1`B(&kF05)bk*u(I@g1uKX#@?Kkf%~e)2}(%lGF( zhCa9F+RvE@)edQl;B4&>;y-I#NXpGW`k5hYLk|y(yb{^Og4Y97y({PQh;65%m4<#& z5|8{OeZ3V4k7qC)DD!L_T7iRC;8ULc*b#Pwp}7Hb@8#DQU9;)aIXbSk0aMSw98WLb z>VwAlygr@Y#8t@U&@q(xcL`sA_~`!;29FzaMBJ%Kmj_CpLwTN2Yz=Ve3b_zL77J_6yaVg-22oH55o5lt=%Qh;5h9 z49q;_1Z}ZOu=@e!3WDkVODc>SWjK)U7|qiZLaOC_DbuM4MpNC>8bbjmxR9%C>|d1^ zjCYdZ*N1O{mHceV)cTGi-l-*Z?>DD_oRq3HzyY6`iAFPIi}r_fbEdca{cCe{{d@w) zTkhu2zef34`Fj(pph-uHn3&khN4@w23m$Mb;^|vXE!t@dnK6Xr^6lva~(v?KaCU=)#_gBZ6?EXdT`?43sB}Dg?^bnb(JZ% zv`_>iIk}+bO(WNQ{ge^gZqi?#Dbcm_kRDKz5{6iI@47PB2t$}mX;fQ&HE7xFTW8xv z03!>gaLOe*Vn!%jcwW}S>$=`7g4Q#c=vr6@Pc9^#?KlUZzv6xu0zkUr_Wg%&RO`^h^wk< z42}w@Tj3m{KiQE8GMZh9!1a+7XCPK_TRs;&Stg-#;8l5q z$2iExwT8oJMg@wZU0YprrA6mk*~3l2(aME;mb=c=nZzsmevsjz=WQQow&m=zuQdA$ zg5Wp=c`AJwr8y$L_qS43?+97>6$6h(LmFSy3I%0xvGD)5xfB1awDoaY0^vhp^i|b# z!{8kY50Q(#_ZO1Fs;z-x+y|mz$L3-t{M7E2dA-fVkmBF+S%ex)J^7stx~CcXN9%kZ zD-epzGFE6LR)_a~ZH;Ajr=!U*REb%g7%oyikX#yPz+M!0f5R>po`6&u8>q%mQO!O6 z_95!5RSM(ZMIduBnsVXaiRN1_N_HAQk2ofR}`SsR~ ztKMdL_12}*Tx|I1%OW{t!(?3-`^WP@lmjdOf5FO*@I=D_F-8n{WIdxR!la<_$ZWP+ zSyaqA4NdFlL{Xx_Rl-qWncTs1XQZ_u(dfO|;4;iJv}Q^!3CV$$>*eED0FXCuj*7d& z35flOM%|10-Xd)UL8dKPMhKf;ApakeB0^=I-!chX<}cpmbRUwXcRBLCD{J+MTYWAe zR1ghg{EvXo&M&E<1OX&n94;Fy0*J`=JB48~1Ld)U`F-w5uB%~8+DGbO@X74>+B?@KdCjuaJz8FBX z(qyLEcO9G*!SfVK;CZ%YjX1^|O#ocP1k7^`IKc4WI2vS}rCK}YC0i<%^5iXcvTEmz z!RwlcTH(lRyT{1=84M4@WE3Z%3chMOR0EQh=8_D}fOuY--`1N*pbz*Ju(2Dr!@^HA z>hgv3k*2Ysh10pvPbDz&*G-m>R)?M>0ku<5+}g`*uPX}P6#+heA#qvW!(5+&gJ2iN z>?oL}b+d58qb$YEi@!VMmfi2M`S>*f0Im8)iWcYE#%4wE{e}~YIm0XgMHCN+eLi~aM!5BQLc!*A}xeOPF!2s?RZJKstbJNbG6Bx75X0r@AoT@3+ww$`(Cci zEqgt%z7Ut`V{5NG5(zwyb!ET1n@|G35UZx7JQnG}f&`3{3U%d^XmH+YV3#)*<}oq` z?3Vf(`aHeA`vFfUz0KoMhrg`9wg4G{c&Q>`3W|-$g}#M>IR{1X8{wQRnYawYCuv-B zlh$Sh<2?R-1x8QJn%39SsOJ_^ygpGI8;Z*F`8O%mOV+AA zdy@KF4rguZG91if&tx&BrDB|=Hvj02mug=of zFZ${l)J&Est=1ozF({0$I{L~{Q2_dj-#tnHG)#7}!FHp7xra~lo{{4m+n$Tdwy=#C zRU^tA23y@J%gR_bF&fqfg@|_M3_A0kj?=vUw7>3LRxTE^n2Z8ze<6KB7!M0f@G)?A z#(yQ0@Vai*S@{Zy&g;uG@$8eFC-CqLD+T8^BdAwbpyOeI-wUt^jn`Axq zZ6}H8)eY(yFWl()WCUKD88cD;x}W6!z`xF=jkp^hv5dJGtT0OQhBN7CY#KsP(!S3eLxv9{^oZkM{%xHW9M{Z<^J?ykNXb^SY#XnRjQF)gSHYDLNkkt-zI zrC$v-@h)bn4U(2jR_{KN4&uo3oU2-El}Hs+1ffw3yqoA}=qqb7myL=)ry(urN%dyvbVJ$GTCH|D4_me-U~KXuRXS0dzX99W zX(jsb#nt%r=ZW~xi#uhsLjJov(Wo`ba`<9VavDc-UJ0N#qLW;dgl*x%17%`J0z1)*i%?esr{b(DX9u z=vB#!&@T6!ljey-bkwd^JCH2$oDw0!3z70tR8-Y=&-1u)wE1^IN^~Yhn6N zW`&8V-b39f$)dRoskwIHHV zZ<>ru?hM@(omPVH3hX#!cZ@7*5h@ok-^;F^K0Zx9ZUV;(Sc$#6@@4uKR3+gOf=Zis zOhrTCc1zm#O|_|uK*eL07jmchAJKsULbLM8H=B?l29DpuzE-BZ>D4=FgP|s2j&5`g z0V?c3irLX8)Bd}@?d&i6G&?EU6Qq^=46d>r_vh0#SJU6!J!voVVLGz|iWNR_`PTA_ z7qX2X3wt7(U(nartScZuT_uT*?y|w>Z&`>ATo6l94tRdFV;w>Y(&W1u2Q_TEX=Xp}gzMkdRl>l~3U5D$r|+&L z1g;&dW>eE9%-v^ZNQ;jREJw6>3hzt&1_hzL;8?hF8E-lA?&6`Lw+-^Vdn)A zQ$_H&fK}sa`kUVPiF2wwt)r@;ID>OBT%*0mn0OTBMV_mmnkQ-b6F$uB`LlFo6>lyE zJMn;lWuKjKJ!i<%*D03A;YG> z*8bK*&q(Lkzfw2%>w=klsgcoH8AbvL)xlmY0zo(?#GwpBr`Z;f=WCU-v#Dq(3oNCP zg^zK%!q5%S^HvoSPajfox+T3F%%B0N$HCOjFz!{ZM4T&EN{0WsI~qU1#bxT6qBG=U zsZzcmOoLi(;>=_ws#UwB1hdTHnuH}}KS_h#FS)~|ZOEO2=thjl5iXEx&u!E{+C(G2eloD@OU_O4n$=pz~8gTeT2Ekrxx zj#_~AzlQIQPva(RxgdukuFNSE>xVWh0z)JvkBctI(+hWI75GXI7A;H@snu32Y&oD_ zzU0WVOh4M(rIzP|nzUq7Q+c1wnd^Td>jTBTs~_y}ZO`(PV3u`pu=_9HBDrI$FQ)ME z^5XLv-!xbSc@0@EjqtK^C>_ZBI7FOnDi|g*+ks`T^DQELCe*-~l4$FK;=j?Ad0bif z2j&A92f`Zp+4-;_a`s}3;-Lo5-NlOS=uEJQ4j5irDO_~6P9~-s+2XT6*CelTe5A%& zCu!1=!ji@KCSjeQN!U$Rn!{lrddx4`D)XR+LQsgzp#^;z^=00-4guCGNiawxT+ikE z0n+>B5Q!~w-3Jrp;w0FuBplE<+qm%B^k~G{lZeUXPVuw`XW)$S08r(+;Va_gxP$G4 zp9obzM7b_8kL>{xNc_|=Q@X)7?e^jFs$a>91yg%>7b&J@smHOw4X>1pxbh`KjzxAW zps*1^;f**dp#QoHb%4-tjJOLUwzQ4helA@+s}Yo54rD!xJUWequ98?N@3BHmA0F$O zP4Tnv2}z18E)3&i7R^_t2o(UUihX*=rNWPT7flxy@94ey5fZ7w^96DSg8+t4EeMb` ztR-5%&&vpY%|ry4w=})Tpf0bj_*T@>U<40*2FGbmPrtLn{}CNs4ud5YO4vRf-q%KX zV7i+A>+0z4S`7cBL}KRL+P+WA%(C~yl~3Silte2&URO_#+^%G+qB%S!or-3Jo~61p zvCBJ0r8s%^eBwNnk;2`^B+N!zJK@k;BU5DK-f9!qj5dMuko)pGK9;9-OI>lyKgL$q zsBYh7QZ`-+)6;zZNy{v~o{;X5u$8`Fme;qO3JKenhk%&LYZFx@aN2S0KOcf9-@gW! zGK!JyjIOG(>O^hLE3n5>gCYD{zJsXh!#6?lCQd+dw(5r$L1AdbIv%#QGQ@NGS+JFfkrP-)PDVf08nrSwO$@U!q zAyYktNJ{Eel|}}s>zZY0QN?5UTMR4xlCp5vb-LrqiwU3xw-&eLP+bY0$%@f=2n5J9 zq({s*vyHnX`G-%9FASF1ybtQba<6U5p9jvNmY-#5^p_eMkB{gVGxLD|^jv)(iC9;+ z@b+8DNF!_GR0cxdwFO(>& z8S*4Xm{&jm!!Cc%1gAk!GYPOsve?T!llsmGm@6+o#M3|GbW=l)Vs=A3X2#Hd`~IFR zePVg$^7}roUKi8yJ3$D~k6;z(`1|0KZ=rXLk0b1~Id)Q+tzHuv10CZuEV*Y;b4%9V zvJv7lb23(-Q(xU#*qcj_4q{CyHF3eRkGy<`mgi3(ZJBvb%W6t>cz>3ELwsf%{5GHI zMn}~l7oL2vBMq`SS9z@e(>CQ84>AmbxvqkAhdz&<2g^7P>YdZo1tb6_rkw`ZWQj2Q zQ0G2Pq(d5?S4#P5Rv^ZZyR1ptln-IsIH{?r6OVEj;q?hHtccl_$@@g8r%3AX#ox@@ zB_nWwXDD_cRXwS%#b?wH0-{kiI1U+dcm3nRBrcBAb?W0h~vEt^j=Xqbm;=A_z8-i?3&HB`+Uy zavDorW#X(9O1%Sc!1YvQnTBS$hdX&I9CpjXj|CZSo4sCKR2|_JG;{H3(t3+Qxxg<{ z?=G8#Htw5s6K$Czwu_}(EXOHvUM9CnstQ1{_@Csxfy;02@7tl+!&vw?V3QeX`O;`X z9X&nP_(x~T;P0n+v9Lwc1Oe`<+_T_4SRn<{H|AV{97!r_`^w$^9+$SFu{fgiwd%j| zuk=7lztgqSpzgv2L>v&}qNw4&Aq%7y<*~7z6^)UYxNUQpI2kqp4uC&!wZRW=XC-~< zB?h57y~?!E(n^h0a|JLbE(U;G~8L~(qqp-Q1fOg7U+1Gae({^zdW7AV! zDRcZBm1}c3DE1zK^zmXmF*FIzMyTDpf`ER2M~*f3-p6p zJG}yyL=pF-W!`Jf*HPt#o#s4Mw~W=*>m7(U>gI&?u!tgFFHOn5$mjMxM!>NzU-7pz zG_N}()5JzI1+5_oMvPufp3RSORY zlL(^LB+k#RYxR3^-88?6N+IUqJZ@341E_l%pL zTcJ)R<-zh~d*x}QqH`2+)@lo23-yp{AlB~j z9%#mhPWL~HZkFFZKX@aymnpwiJa&y!#O?m^&$i4nsJi>*U43aC&J8u zFECqB8&-7sOQvoHJft7BYLjC0_bKp!BCnJrd!Ogp$oIVOR01pI zN=@nSv}V)qJGJ(ZDq{1S{8^Y>WaE;SO`Ew5_>>HqOq*3fE$#b9!t(8dSyb#ME^dOd5EU=Tle@3bt;?HwoZK7 z`=qPb_m4?;-5k2ePznrhOCzZgg8gRxkoR*E|E+~UvoY~;C42q0Np)mL5X zX@sSD?0ugIu%>L#%N9n&1E@I@MKGLnAJu5C8lazd*N8=ATR(le9pBbbNZ~8=HHhoq zBuP#qgePK{`^HS#E~0s zhFW^qDVie*)Qls)%*zfy?0{Yf!ONHbd{{zYDZBSQ=nuI4Ott|KJ<-Ij*FWX)>{U~lp9aS4A@vy`e8aJ;d2Jnw8{b}-eR^gM1X8-#ZZp?uu=2>6W--4XAi`kU z7QwkNhd-NOzzv(`iAsS1p2B1bFQ3#Kdn^rhIdy|fR-TPVXori>#zdO_xmU(MW9jlq zE+X8a4m2eZ+xoASp2g~inY~SJetx<s^liQh|U=8pkkb*(79c|sNtR`0h~&cp$qO< zLIbxQZ&mOi3zSV0EdkO z5KoN#4C1i92kEH0ff$RzV4pINZ6rjP_5+_|Y3;D3jNVd$^vTjYf+Mb&?761DFDofv zQ??B>3MV1fM68q%ieP&*5G9$Vi()YtGPfNf#(F1ID^!++U79d#?JMnq`vJ3O`F8K2 z#b6NU-vOWS+iKOiTK32Y{C^;gIf@X0Ilh1e!N%Y$&38tcj~H`@gg+ ofd8k@!v8%8xiI16(lb~XeX9UUFJi81C19UVRD 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Ν&aA~i5hbuH>_F$Fa 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 literal 0 HcmV?d00001 diff --git a/resources/builtin/roles/v3/home.png b/resources/builtin/roles/v3/home.png new file mode 100644 index 0000000000000000000000000000000000000000..808d639f0dbfc39113f669fe24cf0b65c7307c85 GIT binary patch literal 8787 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 H82LW{G(Z)v literal 0 HcmV?d00001 diff --git a/resources/builtin/roles/v3/library.png b/resources/builtin/roles/v3/library.png new file mode 100644 index 0000000000000000000000000000000000000000..1f9ea1a64acaa91295372138d6de4a1790341c45 GIT binary patch literal 5783 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 F`d`DLO*sGn literal 0 HcmV?d00001 diff --git a/resources/builtin/roles/v3/lightbulb.png b/resources/builtin/roles/v3/lightbulb.png new file mode 100644 index 0000000000000000000000000000000000000000..1aba0d32f31aaa735d2d693b6408b991844ea507 GIT binary patch literal 6435 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 X=ce!Bgb2th!R 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 literal 0 HcmV?d00001 diff --git a/resources/builtin/roles/v3/mobile.png b/resources/builtin/roles/v3/mobile.png new file mode 100644 index 0000000000000000000000000000000000000000..fbb1985015768c3a70bb0b13fb9180b4ebec8b57 GIT binary patch literal 4209 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 Rr}^Iy>M$LZ8YLU#{{U8yX8HgC literal 0 HcmV?d00001 diff --git a/resources/builtin/roles/v3/police-badge.png b/resources/builtin/roles/v3/police-badge.png new file mode 100644 index 0000000000000000000000000000000000000000..8b729bc35a63be9a369442ba34eae50894e38f50 GIT binary patch literal 12753 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 literal 0 HcmV?d00001 diff --git a/resources/builtin/roles/v3/purchase-order.png b/resources/builtin/roles/v3/purchase-order.png new file mode 100644 index 0000000000000000000000000000000000000000..906d72a5297c4a209d088c745f0d82ce190703d8 GIT binary patch literal 5189 zcmb7IdpOiv_eX<4Vk%QECrz%MnL#2Ue*lu9#CNYEHkDzhN{sqwTgW!j7os<>h z_FzE+6K%)6vpX+t`nJe`^k8>&DpmwR@Lde?rgwxpIQ>h0im-k!dTJDD+k5X?f9DCgT+0 zbEq=6=dhLG0$UL}Yh_XU#P$*~m)PLT39J?rw;>{S`b7jMO3?zw3-odK&^_)wnP zB%aU=NjI4Y525(htIE9U+9$nT;&*h}*$MTH&Ti9#2bWdSrqm#aeQPH@1d^z<*`}*6 z=zVmQ!SU+nWo`3k%UNIr9V*;lLnUhHiy)?4(X? z&PaMK1S*pV8u*>1S{N6(Xnh?!EgV1B?X~)a#1R@9&b!=tY9p=lL)MU5s(B|tY5utf zIJejG%I6k^QgheBRGM1rOOEu~sXmB>=^dgOJ`TM#c|;Rx(olI*eCf=WQ^IB6dzfj% zW9s-m6;uxP$WE;YvVO5#0!0(X2}F*lH^wV|6cJ|?{6trjTh(UOQGzwBRy3(0H4b%4AC4YZ? z|IE9}y4^ccu49vF@^?xmI~x=kOwniv;w&o!5yQ%hh|$I~#jqas{w?Kr{B+>ZpCt1V z*iZ1C{Ym0a{BK|a@csquJ>WmaWoWyJjcQ|`_=!5uIniJy^ZwE;1AFbPD=&o%hc_%I zzv8MNR#R?b>*uRRkEd&PSFQ%g8REg_3|0fDfyVG6dx4X<-_uvJFdU)RBTO9%yoM@C znE9~#Xb7&JGdC3n&Wg``^6M|ZC^A%qm)wJQ3E^D=98i*eHJT=uE@>&KLY|-5#;IK# zs#c5&ZBEDq)~$2ork(+3HW*Av=}vz8V#fE>X2V(p2AoZw1d4m>A>R8oA+M8Cu zO+XBAUXK&F8dtASa)EZTT*?p?xVgU8U{Oq<8PF;1*{coUNg z+s*yBHv_$n04sZJM<&H8mFJ874X*C*JUlgQ2agRzuFhCV7Fp{_#VZvTzm)PAqM&Y6 zgtrC=vTg$uP^K`)P@N9Aa&XwIDnEhsGjyym@rIiJA9Da8Wx4+k^L~czEUH81KX{1T ztNf#>=SSB-EJfy({|jA!RASWah_wLwzN+wbt!k_vMjW*$f)XgNTiK5_s&bUWZ3!MSQvZCgfpV~yH&7>|KJzpgfR%}~!A z&P1VCO`2M)nsr3z{A-qqvY`!W2dty=Jcjr5Uq-q&);~U!kDTV|tdMr|cnocn#U75x zLx;m%lx|hrtd&!gJ&d;gkoI@7t*`A|H~2bt=7hL2Fa~wbzT4(`WfZt+B6eo0TLwTC z#PvWS6m!*e&cJ!EVfHQIE)kdy2F^o&&l>%p074{_Hxd2t&+4T=?OXv zJWzH5pMz8|maR>`u2@@EaoiYx6_1`wPPUaTZ0)g(7Bb{3WoH3(seuQVi+pJakMw*W z=H0c6cADx1LY|&E3c4B-X}E;2XIyhn^D9R*@h!S|w(@-FCK2xySUlb;^O;r}y@tKy zArmp>VlPixJw_r3(asq{-zyAuY&W%V40@x}{2Yq|tQb4yap=%kH0@I&5FykCzI76E zsHuS)G#U@6OAu5cTAF`$vf-x2GmT&Mjl!k%0yt&qgZm zt`yU|G=6zv-WT7lvFf?yNraHUA1efR?u^oFN`tH~&3`f~IVRuUWag;dxOiUzHE|Z@ z7N!Kwj92k0pP^tO7P0ae{DrFPPs#WHfZ&XGzR}4~w&;S${i-~|aVVu#6F-5yuXmTM zF^}r7w_9Y)>eJRRK^uBBCnd(@o#TjNa{pOmz3(N9mm>F`!vT*F(we^&oOsKRTkno} z@yRM&M@aaP`8A7cvE#(cpX=6eh#xwNVDoI%%r(+tL|(5Ie63<|?ns}okK5z>c%J#X zH%7tkHx@u%N;cm_Q9Td@FXDsWxVtopT@c{RIh0-sWU%X%0AQ#^8!uyle>Wi!M_dJ_ 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 literal 0 HcmV?d00001 diff --git a/resources/builtin/roles/v3/rocket.png b/resources/builtin/roles/v3/rocket.png new file mode 100644 index 0000000000000000000000000000000000000000..f5a758ff815d8cec16bdca7dee500de4bbde7adc GIT binary patch literal 11037 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

YPya(}dajO4BEY+w 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 literal 0 HcmV?d00001 diff --git a/resources/builtin/roles/v3/servers.png b/resources/builtin/roles/v3/servers.png new file mode 100644 index 0000000000000000000000000000000000000000..df20dd64922265239f18949c36a17a4ff2c45014 GIT binary patch literal 6447 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 literal 0 HcmV?d00001 diff --git a/resources/builtin/roles/v3/sitemap.png b/resources/builtin/roles/v3/sitemap.png new file mode 100644 index 0000000000000000000000000000000000000000..8ee6e232b4885d6575767f75380182fb7d69fff7 GIT binary patch literal 4336 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 XhPe?U1_~UO>H7LQ#@c0?h=~6I4BOMy literal 0 HcmV?d00001 diff --git a/resources/builtin/roles/v3/support.png b/resources/builtin/roles/v3/support.png new file mode 100644 index 0000000000000000000000000000000000000000..88a97467089a421956f723901c9fa7159698e10d GIT binary patch literal 4726 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 literal 0 HcmV?d00001 diff --git a/resources/builtin/roles/v3/sword.png b/resources/builtin/roles/v3/sword.png new file mode 100644 index 0000000000000000000000000000000000000000..30040633fd4b9674c92041184f0949e79352b118 GIT binary patch literal 6791 zcmZvBdpy&B^uOyAlS}4)3#kmbmn4@)E{QH?#%4Aml9KBrmql|Ym##{ri*2^aT~qFt zN~yM?B%g>(R2qf&zSi&Z`2F$QKP3Q_<)x zOip^D9zycGcJ;d8)!(0S{U!1iS@7BUuygA5Z@~xDzI3hIHf~tBp7v!|rOtm{{qOO$ z7w>*qua90i(-QfjP5A)SE*0h_Bzjh89isdFxL5o4jtxgdwAGWXZPJa7s&$=P;?)ty zRSW|eiX<`?LchGbgDops&rn$xm$!e4XDB)%e4h1ATxF=b;l$R3UXd>K>5-fe)9r~9 zt7j#G9QMqmKCoB+&jFIX^Jm$X$P9W1zd*>tL|P`Tsq4;|mZ8o*ajf{AjB$H~6VLXp ziRYzWQhlnYSJBw6Q6Ta6tk7-oA1RV?V%g+U$1UW`v=GLv{LszCsqr5oRSc(fvqaCe z&fJXBiwR~b^srqbj!>eJ{C*cM`Ipr)udOFU#V|y-1=eMBW!|{>iA421gG$%YftKL% z7#AyOWP!%p$5HX)n$}Uch3vU&Hwy=Q&Wv%BrAbbc{&s1V+DGJ)58V!ODslI9BU!*= zTmnd*`WLpEHJ(CmnHvdqa=8hkJo;gJ${>QBurQ6=1-2c&Bi-w{fj`q<_{)3yG4%j= zOhUvP%-h|SW0kcrQIF@w+76mKuP|#*oV1*|^=;EmNOoVsN74Kgv4aCYb^7ybN@uAj zPtj)z?=A(7SkxYk9emM0@i!4F5~K-t4|J>_Y?e?l)R|5&T9fWN(NXs2N>V`jiF;OV z=UpHC+9T69`Rv~5_xp7(FU@oE-8bQb`%!+?i_KZehB_h5VoRi%+O}xkqs9LCg=;Sk zM-(R6#AWPufiOkjjD2@vwu9)^L)!IQnPwYXi_Gzs2h1u~6OspUa@L)XUW-=1B?5ka z8?S%PX4d|0xzKXw4`D(^QO+p!m@MWL#VzR7(zlxYk}6i4`||k!&2^mIQ7E|v0X1)o zEQ?8?uBm3#TcJGDBqJH*8GTvIxyI@KUz3VHU18G=zHk?*>b{5EKXZv4YKA&1+tlmL zXKtz9igKdHp6VI6qUHB!@lR`~5)eW6i≶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 A&j0`b literal 0 HcmV?d00001 diff --git a/resources/builtin/roles/v3/tag.png b/resources/builtin/roles/v3/tag.png new file mode 100644 index 0000000000000000000000000000000000000000..c91bac473aed47120277b9ff131632fcf2af012c GIT binary patch literal 4161 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 M(J)j;s6u1@3yO#=d;kCd literal 0 HcmV?d00001 diff --git a/resources/builtin/roles/v3/trash.png b/resources/builtin/roles/v3/trash.png new file mode 100644 index 0000000000000000000000000000000000000000..ce3f557d442f5c5eddb61584ba8c3bf1762f2e0f GIT binary patch literal 9154 zcmZvCXH-*77cMQK1&}JeinP!>0sPrCz zMxKYF{fLMJ;_ho}J_;n>#S;?)gT%ZWG>Y2H+(7sL_ZhGEq?7u|NlesN-olZl>Au+F zPX2PSCA7Wys;kqyyj{Im*Se?{+VLFX6U;Wx8O6{neIA_*qyL#y@ zZ6}_uhE5mop1_Xi-?z`H%r-QJlg9r%#WoSw>JA$m6p3wVRorHSipP?D;Du{T+WJJE zzs66p2eQ?>oKuEMafCxMTwKLM&=qf0xO{j&A}bXX`V?cU5@b83WDr$Jn@99TJ1*C> zHlJeUgt)baT|?e!sk&5BcaHek`Ynx93s z)|Ia-)aSX+YmeLF&abc;e&Khyo^<)M z$nBbGu+Y(`oPhj*oQzga39Tz_i~KA;gR3QfZpDISDjmCsQrzNv)AUd0wa_S8gbkWOX9)|m9s)41S9OR zPk}3579oQG6U1_sqtzCa@F{p1gr`8$lVu2;5v&hr`2<{SSF-k4NXCEDGFJjuQ~wP< z1%`nOPXFIfXqaC;jel>K64OADY?~cxKhm6$a@1p;#L}ueN{PcDH_IbT`so2JuY1(3R32+ax@L&&!+^4Y$W~_p&1(g~qJVk?7KT5!$;vOkQev zJ=bH|b^ycad#)Xci(WUu8x(Hgg7gdQ5v5V}BIq7gOO)dwjcxP{=hYf&)hID37G1m0 z;aB4+kc_R~w@M7D-m)uqhsRd{3k`q6-+7VVTzA5tl&`srAf5d&(RQ!br9)y6X#fuM zUHzn)M|H6y=!disrLFaX=W7lj=u7|)%uZ-HVS+l_){u-1Q#PX!uw=P>aWg``E0|sA z*KhJm8u(^cxBK^KW_tEi{9Bsre}gNJN$=HiKA9W7w%95iRIf}o95rtX1=JXEwoQykz&77BeGt%?U=&+VEV8RRo{|m2A<<~mg~k{6b!Qa zt>gOGeibduMrfm;HhPm!T! zuzYXS(ix6jeTP`iO$;|Pfs>gLs{)U=58$3z8|=KRf@2(f-vm&zEAN-CZt~jecFQrN zr_9w2ejeQ*9U{S*7+!pR`*na{riep%t5m-oO4+&y`%Bz1ifgD-4^}&U`T2p}&aHyv z)0})6(PW1r$^}vTWdBc0-dXUJLP{|pHYfRd4%d=>QVoFsvZ2!H+ z5%6q6mH#}QB?Mzr71Wa5FXQ#mU`bG5!@$k5SsIPTsf$eUH0nw083d)`gW0`-X zW=^e01-0uuLEf!ZbBTa`W+_tBWLMqYR(PLheCz|xP}@3c$W@4)*svai#26FlJVD4$ zx>)2M)i>LY9Pp?*?Is44xZK`*9lyYaa6MbV#al{s>B{Qu1*dV-zS}#somwyKBdK|6 zn#PfkOelHBu{zi-#EO`ieg{*Vk8=-F75v6{pR!z$JHu5TZzuI6K`CNJt)Uzt6 z(JJaN(?xY?n@O!p-S9wF{Ymtlq7zsr^3 zS9%&*;!N8#FBJ%WGB#=>P~PqX?|?LJD{=ElwuAhk*--Jn7gloq zk3gUIRWAQ(zT;M2czkb)(4~7PhAfG@lJ3#NuP!*3*@T6g{B{_d#_^pe$gJwRs&gBG zge|p$^2KU4wUmaYCC3mhk$&4o+&*@adY-H5qQKgDp3vX3Rr?7Fk^r-JO8g!)V?%5? zn+X(5CXL_hJ_%gp^4^V{4Q%SC-=i53lV@!hIqoS_bNriR`9%Vp@!Y(oJhsdF-r@{j zxj3b~s>+IZGp1vSA2K)@6wI2q^1kE|29)82N-m#S{ zvDfXrN2$P^0~YI4!v%DW3ilMWobI{{me2m;c0gv8Ul0(yOrlAya9VWJc+{)?=OJj* zsN=h$v%6~DORGNvGTfi<-8(ApISNu_+IIv5O2Jy8oW{0R$Q zEqdTEj~fR4iRI#iAgukITt9{#8z%_36EA;$qpw%3U%(5TYL&|%PRB~qHjfqWS1;4@ zW)uMb6rhz0)d}{P3yHf&mMt|W-fY!L6?C>D)3)LFP`-+lbVxPIV%tfV3bnVU$Wt%b zA=Ur$x4Cgyh>?4`x{OM%788kA@fTY99ocv3ZjZTfp3yZr!4c7w`KWFPw4G>($eNf&8EzQFCm_rxs<9u4?=&*M<8buq0!Rlb@DQ4Yn2Dx9$E{*8Tv> z;8m$t{MNf=cvh4_sW-*4J1?QjL?VWV~H2Yw?19MNA()i`xUOif$A z$JE{fPDX!n7E)?Q-sQZ9UJ@zKjZ`8&LcXc+_4N)x#&0*L+~?D}Q%&OHuv9f(+Co-c-tE*448s9}Pd*DC)J>3@BroQz0Fq#7!O}?&fXO=vuWf@|U?gbG%A4PIp{$hrx0e*@U=|{Q3qR;y zEcdp))pB}d(b9Ru_M!DVdY#=%JkW;Mx<2Zh&&0Dcb5Duy7LM(gtVa~LNZg*n2=3W6 zEaca8$;n{(G)%K62J_?)%Cdq^>k>s~M@vp4k;-hq0n`L_lD{FmDCYIc%PcXqubX~! zn_F`TPnUn^gVh!3nq<-4f72cczqmhsxHiMrzy6()Lo-z(%0nO557|vd_2FFDZ102o zq_k}ty2cydaJ_E+`!_Z(H8!W|e2VLSpHinCkQQyF17dHDN1sQAWI6xtNYHsj^;ZoA zjbZ7f_lp^A#cm;jwGD*D{IOrC`kZ!rJRf1#O9U-R*m)!=IZaNTzq7!*UZqff+8 zDA@^RAVt)J=UzTb6aAszB((4meL+G~_``v}#grWY_-2nrKqMQm*& ztb}6ouKC)aliqGV;2Sy4t7=y~yeb}lv9E)4cj60T;=_}2HYwB)8lAOkruSZa#K{=t zKRkX^+_!M}e3xXt?r_jnO}1v-oPtBt!2OXbDF3R&+f)}hM7r-;^2E18Br}=`rgI@5 zPg+zBj$D8KO-F&E9vO@6EPm4pOyUx44E97o=Xl?mO1H-!<@q$SwUG1G$5{Ul_`h zFY7)8j7TQ795t4{6Hf)G-GuUy(`76<#GO2&-w@SJG#q7K9>Zm{Zx&;#=%saa$ zNt4FQ8=YnbEsei~9e0zjQbVA@+k~p~`dD~oaL=&C>hJ~QCF{#pv6^wae#T2^YQVNs zf216(I0$h|&VGK}`d5Fy#;Pde(@4EB|&5JcL1t%NJg z;zxY4As9Cj)Fg|49nT*WTo8tOQX)PoVO#tnwrMvn5zNz6PIM9*Eftu%O&rM?`d|zQ zd2}M*mD{d;tkZ5ZOv{L_2wYM=r>@IHECl|&I?igNB`Xeg7AsatCEO7bU6qTXa~810 z97_Sk1%zAoA!Z4&ab~r+NItPB_ztRRY6OZ7qW8s{H*=$f{@r+n;fs2SF_Ry2D{{&t9>+@;w!t68WKln#7%AHiz z4ZY1WRNoZKc})*YuSx zjF`xqm1>pTyDuo@dVMg8!UROUhiH&EFm^i3q;FdQe&926kQu&QJ+QM{?_ z2GZECcLaE#4+UX5xeRnnowZNd@H#ZLd*l%4x9=G$VfGd^!(&t3Um+uDH!I)P=zYW! zo95+U@*`tR=J?5XA9h4FuD|{4+pnuet{Qs-LzC4>#c$+xT#Y>%`hs@V5Z!8JO5a92 zRRUEMCyNWrHc;iqg}`Q#hzdTRJrAz+{EKUgLN(tB1L=-KBAVN<1uoWQstKlzGLt?1 z-4C3ph<+zm%~RnX=1*?AU%r&NjT(DllP+?W>m+h^J2;@8XCVD0u`1o+Z zU1jZ@@dg|D39M`W)RnLShE2#D%CUCPyzVeH*4c-i3BmB zm;=dX7R57?av&l;(jLa-I~DW!0~tJU-YXnNkDwAkFiRph z`f&`1K*2~X;DHA_nLe`fx-cRh7i5HtdNXS!)Y;H9h=AYKR18ISdO)DSqX!Aam2+j9 zShATO4ZI22%pDZ7^?s}2`Hx)&JHWZ+UnsvT;Sa5bDhg?KLHpqRs%CBO=7(6|NV}iB zG^-8h`U|-KhW!>Iu!!yFj{2>ekm4Q}dwer23+=F;aG|~|#*>O3r+J2TAsJ#puxcb* zim3I-v>PU_nc5V{DP?jY92H+_A9&jaJEQFBF!6+vF(C-c&96$cZ^TNsx(9mMLQ1>4 z4h!AxBocGSq0&#U;$184g^+-2+h$_675|*RnAvdilvY2DF*^0=z9$1|K-$~_+hF%E zZs-3DhcO^paNSPA4p^!F_j^;8X_x{%CFZbU(QmRCt%?U35oO?t zEM88pfn49=WQbVs2JWcMB%GEaC_@eVBF_OEWTZxCe@fwo7?2#h&sUN z5IkQmy=6o=;MtE*&H6|X1xp_~1^xB>Lr}4TMzq=hC}V`n=6}k*IpcophW?swec>Jx zv#~jUclJ^j;O83%9{7CHZ&Lc!tIhS+PI#X|;L_}Rv?wq3rtXpt-@sz(g&rDU`)XU0 z61wwVcrcN{W$JY3AtIXp!RSu`MZsPU|1)3mfBN1~5xj2R+6|exVzvd?Q1f3-DY>bU z&!a0_xc{k|Uq7Yk)t`!6xpTFF+!+yTn-+Pg5`l>kQA@3@C-|58Pn^yp+^aod)p2#7 zW-WXzkRVo+AeQ|a7@Kzu;9GBN>Jd@|U64vLB!Rf}jezlqVSv5Q%mcqzQOwjaf2s@FP{HdEG&HAmNQ2VQrRXIV*oG{rKN^eBdzi= zx8=(TUs*Rgw1MZ>++k!h4;MXx(*C!J!bu0~7!~{tj!KaW&G^drD9&1rYd{AC*TSDu z@QCUNg(CeZFkVwTN=K}WV-OY(3TliF>a!Ll3t*xehK$P20P0MF4w9ZTSO#lOmSK#= z=2B1H6~OxLPTWV?U&jc%!2W73p+D$2&O@8f%;-7CF=GS(2zUK+|5OOnaeT;zNH`5F z9EovCB6@EJYI9j*{i&hF$Rz779hw;}Z`4Wnz*a;}Of=_#v&UB7Gc}X{!TL9m2YTtj zfO-7(ac`HEje4z_La^oWcr6dq*Blx3mPh47i{&r{rjD(nKfF%5)3GS9-;X9=VUKPW>0wew`ux7``T(sAgDcsfv6-^u$Zz}0V=cy`wj?{Gn^lv)fk<{KIO z{qsrhb?J|CpAx_pamm2My%++&5fwBWxz;bHj4_}-efrTA0M`REV`Pk zxdFExi>pfSM7mQzNs-d~!I4bSWoK7Me_-k}E0Q6d6+RHPQNvH502~HE#1IESt^Cw?;d{(JRbg;5qQ)*Z9xfG^{9lr~d(?zg8@<|ROF(R)ut{0zY1*FVv#6j!r zUwr~^8qpWwOnBvTGHL?Qv23wWVaSj~h{S&>Rt5-Pcm^#&1x2MBh#@*1`B9(4piI{%so-#A zRQ^X;c;ykB)h{O3*@X!Jf!8>y+w=tCnlqA(D1oca*u?>2eDzD z9Y;m7Wuul0imb1itz8elvZ11e4Tsm%*T@I{4i*;Daf`k8@e!{-F1U(ao-wSC0kugd zix}z0Ga#(W6^`|DaH1holGnt(7XgHP@f$6;rNU}tVAxMchgr#eSrTAlt*F}g<)nE! zuIW2LJ`->S)U$BEr&6O1jXY2N9aL&zL;q4lSoL3>g&w2DUOX80{y}DS=bQS@m*TL` zH?WBje%q@=gFjk_M@D-I=^T08BGm_%>3-f{aBtAt+Gd&q1N6SsTo<+n<97l6t%EGF z*Pc2AiPkP5{L_qDA4?3h{`*AdAdGI_T<@zSs7UyQOB9;&9J|UQJ8uARNtjVeaQ~)& z>QbGBbohp?e8nzIr4q4Ua3?RXFhL#b&qfaLJ~#WSLG_tKdHDv1nEp}jWxWqA zD#OZv>EESN>YRUceUh<`8l|q9`=UaA=ozM^#@lVN#klgo_ioK;Ei6LPIj=FG#t%Nb z67Z$*BJ7(VC%NY;eMd_2yTc;#4DuPl7#V!H@$ux$LPm0ZL2Qw;yEh?}Y(|h9(KiN)=ZwE=$8p2tOQLrQNkoHUy>pMsd}Gg>q*4;|V&w&kdxD^q zo`}#hCdptV^X+dUlRTo1QKf<_@o^?aY$(np;7px?N8Xd!^}?U zq7SQU#j04n2w40L5}Vk*M>F1~>9w_^tGLx@KUe#U41<5$cKe#7O&W|%n#D~7^qWJb z6}|CS*ec&VGdj%A^Z@4jOxAx*lj(KZh z;qsuSQn`7oq9u@=Zr(LAWz9-;7X>rVsd9UC<8j6Vr?mbbfnSFD_ zJsPICUvX2yy@pjz@$eH<_6J`|Rp5>o_%Ew^Rdc!CuW@&!zPSQzDRq6TcPUfA$9{A; zm;E92MU#Zk>J?FLFGZPO$khGgIS4|gE4&e2WWJw%CO{~4fwLB$gyxCG;AcL67VK18 z)-16i~qZJ&Ir z!s)8@Qfja)xW7A-mBa2SCNZnt;@T#M6Z#~ONOCpK7yDPku~rqE6O$e=X)8nM7%Y!2 zsfUV=Ou!*~Ogv5r{Z2J+i$fOz$&I)N8@Umq(u1slpWp)^y(#F7`nzg*kdJbkm^kSMsz(Yy;C;q28Ks)SbNwzUGp$=?r#Lg4?zW zx4CE5F3R#2-gXxh_NKPP_z+ch^hwqg4fy{;&jdT0`EF^FI5sZ7u=kVW;Ygd z$OYwuhi2p{Et#PQ}{M6|{G zD1)yZ)D{-M2D+i@q9U%WB`^T!CcJ}MSQbe$~ z(f$Z4YXTRe@Vz`m$)#O=bvlgJD!+Y6gY_AlzP#dWrinZ5Bx)Bh^^IclrhQd|>6zy1 z@cuJ=Q;y35xA8ELzCVcC;cP`sVs#=Zvr10W9&fD(#G5GCPEOTQb)xY?T6ww+vv4?eFWL7`g zzw@zTDW>(}&+>%-2+IEzvHzHb8|3<8<{!?f{Jm+us~2d(3NGt9SBXeQpwS>!mar$p yi4-_25rkR_O4LIl1^urNG*^9~fl&XyNeW6%;9)C_Z1U>GoBMYSwQIGI;r|E5{5R(S literal 0 HcmV?d00001 diff --git a/resources/builtin/roles/v3/truck.png b/resources/builtin/roles/v3/truck.png new file mode 100644 index 0000000000000000000000000000000000000000..3c903ea58bfd0613a2b9d86ae79992b81f60306a GIT binary patch literal 6294 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 T)Xwa6GryMlLujQMBJ95aCNPh4 literal 0 HcmV?d00001 diff --git a/resources/builtin/roles/v3/two-servers.png b/resources/builtin/roles/v3/two-servers.png new file mode 100644 index 0000000000000000000000000000000000000000..d5d408d9ad6d3cf8741c15bbb5882870c482d5e9 GIT binary patch literal 5460 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 Kmy3`t;r{}kg#=yz literal 0 HcmV?d00001 diff --git a/resources/builtin/roles/v3/umbrella.png b/resources/builtin/roles/v3/umbrella.png new file mode 100644 index 0000000000000000000000000000000000000000..98c7c1236545200fdb673445e3e4c055ab20d99c GIT binary patch literal 9812 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 S?|b{UlBSxTYK^jO#Qy^6uDKHc literal 0 HcmV?d00001 diff --git a/resources/builtin/roles/v3/wand.png b/resources/builtin/roles/v3/wand.png new file mode 100644 index 0000000000000000000000000000000000000000..6de1cb55ab2f71a4cd7b3b43a8d5f45c3e8dd1ab GIT binary patch literal 8717 zcmaJ{c|4R~)R%4S#?EAotYa%%_BFdA#@L7X$zBoJ3uE6STe4&;W-w!mtYe#yM8=Zr zM2JRNLV6#)f4}d1KJ$F;bC+|@J?Gqe&-vab>9&O-BOQc}jEs!Y*ht@sjO-Hh;!i^j z)PzQ+2a%BpO&aU#SchEN!AApiWaOpGAQwceq~pcQ|M?5tnGX)Mp6=WvNqfIrcYZOL z9R9>&Z(x0y{Z*^%M&;9B+s~$rCDNbN%7iCbXpGCpQ}q@3{_CM;C~G~ssW>n{$z8}@ z2xjO$a#}rTdWJgm%($C=H}LBb!9)7X-J266+l`&Zv4vomb9~w{xPCcLPN&M6aJE## z<{Yc=kj^D)NW{boYb{_zFNtW#+KRuwbcHc@SivTfbH~slA5+#9J1q!?YTGixL~j@Q z)42KbnZYb?>8hVs;fE==#4=U9T^r(0u2oQF_+YO^C+n`L#MH^z>6pjB9n0`0Vnn_@ zU3-5vGuZ8LAY&FnJUhch|28e%>F<4!+Kt8OT{&Db0-xsuH!kxpHo#2uBx`hu^FKh@<7=a{Cf1(%0A^2t-Weq6oc}G-}ZuL@@Snw~X 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 literal 0 HcmV?d00001 diff --git a/resources/sql/autopatches/090.forceuniquerolenames.php b/resources/sql/autopatches/090.forceuniquerolenames.php new file mode 100644 index 0000000000..c54306b973 --- /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::normalizeRoleSlug($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::normalizeRoleSlug($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::normalizeRoleSlug($new_name).'/'; + + $okay = true; + foreach ($roles as $other) { + if ($other->getID() == $role->getID()) { + continue; + } + + $other_slug = PhabricatorSlug::normalizeRoleSlug($other->getName()); + if ($other_slug == $new_slug) { + $okay = false; + break; + } + } + if ($okay) { + break; + } else { + $suffix++; + } + } + + return $new_name; +} diff --git a/resources/sql/autopatches/20140521.roleslug.2.mig.php b/resources/sql/autopatches/20140521.roleslug.2.mig.php new file mode 100644 index 0000000000..73a29c0231 --- /dev/null +++ b/resources/sql/autopatches/20140521.roleslug.2.mig.php @@ -0,0 +1,36 @@ +getTableName(); +$conn_w = $role_table->establishConnection('w'); +$slug_table_name = id(new PhabricatorRoleSlug())->getTableName(); +$time = PhabricatorTime::getNow(); + +echo pht('Migrating roles to slugs...')."\n"; +foreach (new LiskMigrationIterator($role_table) as $role) { + $id = $role->getID(); + + echo pht('Migrating role %d...', $id)."\n"; + + $slug_text = PhabricatorSlug::normalizeRoleSlug($role->getName()); + $slug = id(new PhabricatorRoleSlug()) + ->loadOneWhere('slug = %s', $slug_text); + + if ($slug) { + echo pht('Already migrated %d... Continuing.', $id)."\n"; + continue; + } + + queryfx( + $conn_w, + 'INSERT INTO %T (rolePHID, slug, dateCreated, dateModified) '. + 'VALUES (%s, %s, %d, %d)', + $slug_table_name, + $role->getPHID(), + $slug_text, + $time, + $time); + echo pht('Migrated %d.', $id)."\n"; +} + +echo pht('Done.')."\n"; diff --git a/resources/sql/autopatches/20140711.rnames.2.php b/resources/sql/autopatches/20140711.rnames.2.php new file mode 100644 index 0000000000..f183bf0502 --- /dev/null +++ b/resources/sql/autopatches/20140711.rnames.2.php @@ -0,0 +1,11 @@ +getName(); + echo pht("Updating role '%d'...", $name)."\n"; + $role->updateDatasourceTokens(); +} + +echo pht('Done.')."\n"; diff --git a/resources/sql/autopatches/20140805.rolboardcol.2.php b/resources/sql/autopatches/20140805.rolboardcol.2.php new file mode 100644 index 0000000000..a04873db20 --- /dev/null +++ b/resources/sql/autopatches/20140805.rolboardcol.2.php @@ -0,0 +1,53 @@ +establishConnection('w'); + +$rows = queryfx_all( + $conn_w, + 'SELECT src, dst FROM %T WHERE type = %d', + PhabricatorEdgeConfig::TABLE_NAME_EDGE, + $type_has_object); + +$cols = array(); +foreach ($rows as $row) { + $cols[$row['src']][] = $row['dst']; +} + +$sql = array(); +foreach ($cols as $col_phid => $obj_phids) { + echo pht("Migrating column '%s'...", $col_phid)."\n"; + $column = id(new PhabricatorRoleColumn())->loadOneWhere( + 'phid = %s', + $col_phid); + if (!$column) { + echo pht("Column '%s' does not exist.", $col_phid)."\n"; + continue; + } + + $sequence = 0; + foreach ($obj_phids as $obj_phid) { + $sql[] = qsprintf( + $conn_w, + '(%s, %s, %s, %d)', + $column->getRolePHID(), + $column->getPHID(), + $obj_phid, + $sequence++); + } +} + +echo pht('Inserting rows...')."\n"; +foreach (PhabricatorLiskDAO::chunkSQL($sql) as $chunk) { + queryfx( + $conn_w, + 'INSERT INTO %T (boardPHID, columnPHID, objectPHID, sequence) + VALUES %LQ', + id(new PhabricatorRoleColumnPosition())->getTableName(), + $chunk); +} + +echo pht('Done.')."\n"; diff --git a/resources/sql/autopatches/20140808.rolboardprop.3.php b/resources/sql/autopatches/20140808.rolboardprop.3.php new file mode 100644 index 0000000000..0bc1954187 --- /dev/null +++ b/resources/sql/autopatches/20140808.rolboardprop.3.php @@ -0,0 +1,24 @@ +establishConnection('w'); + +foreach (new LiskMigrationIterator($table) as $column) { + $id = $column->getID(); + + echo pht('Adjusting column %d...', $id)."\n"; + if ($column->getSequence() == 0) { + + $properties = $column->getProperties(); + $properties['isDefault'] = true; + + queryfx( + $conn_w, + 'UPDATE %T SET properties = %s WHERE id = %d', + $table->getTableName(), + json_encode($properties), + $id); + } +} + +echo pht('Done.')."\n"; diff --git a/resources/sql/autopatches/20141107.role.phriction.policy.2.php b/resources/sql/autopatches/20141107.role.phriction.policy.2.php new file mode 100644 index 0000000000..526ce4f600 --- /dev/null +++ b/resources/sql/autopatches/20141107.role.phriction.policy.2.php @@ -0,0 +1,61 @@ +establishConnection('w'); + +echo pht('Populating Phriction policies.')."\n"; + +$default_view_policy = PhabricatorPolicies::POLICY_USER; +$default_edit_policy = PhabricatorPolicies::POLICY_USER; + +foreach (new LiskMigrationIterator($table) as $doc) { + $id = $doc->getID(); + + if ($doc->getViewPolicy() && $doc->getEditPolicy()) { + echo pht('Skipping document %d; already has policy set.', $id)."\n"; + continue; + } + + $new_view_policy = $default_view_policy; + $new_edit_policy = $default_edit_policy; + + // If this was previously a magical role wiki page (under roles/, but + // not roles/ itself) we need to apply the role policies. Otherwise, + // apply the default policies. + $slug = $doc->getSlug(); + $slug = PhabricatorSlug::normalize($slug); + $prefix = 'roles/'; + if (($slug != $prefix) && (strncmp($slug, $prefix, strlen($prefix)) === 0)) { + $parts = explode('/', $slug); + + $role_slug = $parts[1]; + $role_slug = PhabricatorSlug::normalizeRoleSlug($role_slug); + + $role_slugs = array($role_slug); + $role = id(new PhabricatorRoleQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withSlugs($role_slugs) + ->executeOne(); + + if ($role) { + $view_policy = nonempty($role->getViewPolicy(), $default_view_policy); + $edit_policy = nonempty($role->getEditPolicy(), $default_edit_policy); + + $new_view_policy = $view_policy; + $new_edit_policy = $edit_policy; + } + } + + echo pht('Migrating document %d to new policy...', $id)."\n"; + + queryfx( + $conn_w, + 'UPDATE %R SET viewPolicy = %s, editPolicy = %s + WHERE id = %d', + $table, + $new_view_policy, + $new_edit_policy, + $id); +} + +echo pht('Done.')."\n"; diff --git a/resources/sql/autopatches/20141222.maniphestroltxn.php b/resources/sql/autopatches/20141222.maniphestroltxn.php new file mode 100644 index 0000000000..fda7721f77 --- /dev/null +++ b/resources/sql/autopatches/20141222.maniphestroltxn.php @@ -0,0 +1,61 @@ +establishConnection('w'); + +echo pht( + "Converting Maniphest role transactions to modern edge transactions...\n"); +$metadata = array( + 'edge:type' => PhabricatorRoleObjectHasRoleEdgeType::EDGECONST, +); +foreach (new LiskMigrationIterator($table) as $txn) { + if ($txn->getTransactionType() != 'roles') { + continue; + } + + $old_value = mig20141222_build_edge_data( + $txn->getOldValue(), + $txn->getObjectPHID()); + + $new_value = mig20141222_build_edge_data( + $txn->getNewValue(), + $txn->getObjectPHID()); + + queryfx( + $conn_w, + 'UPDATE %T SET '. + 'transactionType = %s, oldValue = %s, newValue = %s, metaData = %s '. + 'WHERE id = %d', + $table->getTableName(), + PhabricatorTransactions::TYPE_EDGE, + json_encode($old_value), + json_encode($new_value), + json_encode($metadata), + $txn->getID()); +} + +echo pht('Done.')."\n"; + +function mig20141222_build_edge_data($role_phids, $task_phid) { + $edge_data = array(); + + // See T9464. If we didn't get a proper array value out of the transaction, + // just return an empty value so we can move forward. + if (!is_array($role_phids)) { + return $edge_data; + } + + foreach ($role_phids as $role_phid) { + if (!is_scalar($role_phid)) { + continue; + } + + $edge_data[$role_phid] = array( + 'src' => $task_phid, + 'type' => PhabricatorRoleObjectHasRoleEdgeType::EDGECONST, + 'dst' => $role_phid, + ); + } + + return $edge_data; +} diff --git a/resources/sql/autopatches/20150515.role.mailkey.2.php b/resources/sql/autopatches/20150515.role.mailkey.2.php new file mode 100644 index 0000000000..d29be1d27c --- /dev/null +++ b/resources/sql/autopatches/20150515.role.mailkey.2.php @@ -0,0 +1,18 @@ +establishConnection('w'); +$iterator = new LiskMigrationIterator($table); +foreach ($iterator as $role) { + $id = $role->getID(); + + echo pht('Adding mail key for role %d...', $id); + echo "\n"; + + queryfx( + $conn_w, + 'UPDATE %T SET mailKey = %s WHERE id = %d', + $table->getTableName(), + Filesystem::readRandomCharacters(20), + $id); +} diff --git a/resources/sql/autopatches/20151219.role.06.defaultpolicy.php b/resources/sql/autopatches/20151219.role.06.defaultpolicy.php new file mode 100644 index 0000000000..feb4984db9 --- /dev/null +++ b/resources/sql/autopatches/20151219.role.06.defaultpolicy.php @@ -0,0 +1,28 @@ +getPolicy(RoleDefaultViewCapability::CAPABILITY); +$edit_policy = $app->getPolicy(RoleDefaultEditCapability::CAPABILITY); +$join_policy = $app->getPolicy(RoleDefaultJoinCapability::CAPABILITY); + +$table = new PhabricatorRole(); +$conn_w = $table->establishConnection('w'); + +queryfx( + $conn_w, + 'UPDATE %T SET viewPolicy = %s WHERE viewPolicy IS NULL', + $table->getTableName(), + $view_policy); + +queryfx( + $conn_w, + 'UPDATE %T SET editPolicy = %s WHERE editPolicy IS NULL', + $table->getTableName(), + $edit_policy); + +queryfx( + $conn_w, + 'UPDATE %T SET joinPolicy = %s WHERE joinPolicy IS NULL', + $table->getTableName(), + $join_policy); diff --git a/resources/sql/autopatches/20151223.rol.05.updatekeys.php b/resources/sql/autopatches/20151223.rol.05.updatekeys.php new file mode 100644 index 0000000000..7272d0d0d5 --- /dev/null +++ b/resources/sql/autopatches/20151223.rol.05.updatekeys.php @@ -0,0 +1,24 @@ +establishConnection('w'); + +foreach (new LiskMigrationIterator($table) as $role) { + $path = $role->getRolePath(); + $key = $role->getRolePathKey(); + + if (strlen($path) && ($key !== "\0\0\0\0")) { + continue; + } + + $path_key = PhabricatorHash::digestForIndex($role->getPHID()); + $path_key = substr($path_key, 0, 4); + + queryfx( + $conn_w, + 'UPDATE %T SET rolePath = %s, rolePathKey = %s WHERE id = %d', + $role->getTableName(), + $path_key, + $path_key, + $role->getID()); +} diff --git a/resources/sql/autopatches/20151231.rol.01.icon.php b/resources/sql/autopatches/20151231.rol.01.icon.php new file mode 100644 index 0000000000..74eac7199e --- /dev/null +++ b/resources/sql/autopatches/20151231.rol.01.icon.php @@ -0,0 +1,34 @@ + 'role', + 'fa-tags' => 'tag', + 'fa-lock' => 'policy', + 'fa-users' => 'group', + + 'fa-folder' => 'folder', + 'fa-calendar' => 'timeline', + 'fa-flag-checkered' => 'goal', + 'fa-truck' => 'release', + + 'fa-bug' => 'bugs', + 'fa-trash-o' => 'cleanup', + 'fa-umbrella' => 'umbrella', + 'fa-envelope' => 'communication', + + 'fa-building' => 'organization', + 'fa-cloud' => 'infrastructure', + 'fa-credit-card' => 'account', + 'fa-flask' => 'experimental', +); + +$table = new PhabricatorRole(); +$conn_w = $table->establishConnection('w'); +foreach ($icon_map as $old_icon => $new_key) { + queryfx( + $conn_w, + 'UPDATE %T SET icon = %s WHERE icon = %s', + $table->getTableName(), + $new_key, + $old_icon); +} diff --git a/resources/sql/autopatches/20160122.role.1.boarddefault.php b/resources/sql/autopatches/20160122.role.1.boarddefault.php new file mode 100644 index 0000000000..1debb2b1e9 --- /dev/null +++ b/resources/sql/autopatches/20160122.role.1.boarddefault.php @@ -0,0 +1,60 @@ +establishConnection('w'); + +$panel_table = id(new PhabricatorProfileMenuItemConfiguration()); +$panel_conn = $panel_table->establishConnection('w'); + +foreach (new LiskMigrationIterator($role_table) as $role) { + $columns = queryfx_all( + $conn_w, + 'SELECT * FROM %T WHERE rolePHID = %s', + id(new PhabricatorRoleColumn())->getTableName(), + $role->getPHID()); + + // This role has no columns, so we don't need to change anything. + if (!$columns) { + continue; + } + + // This role has columns, so set its workboard flag. + queryfx( + $conn_w, + 'UPDATE %T SET hasWorkboard = 1 WHERE id = %d', + $role->getTableName(), + $role->getID()); + + // Try to set the default menu item to "Workboard". + $config = queryfx_all( + $panel_conn, + 'SELECT * FROM %T WHERE profilePHID = %s', + $panel_table->getTableName(), + $role->getPHID()); + + // There are already some settings, so don't touch them. + if ($config) { + continue; + } + + queryfx( + $panel_conn, + 'INSERT INTO %T + (phid, profilePHID, panelKey, builtinKey, visibility, panelProperties, + panelOrder, dateCreated, dateModified) + VALUES (%s, %s, %s, %s, %s, %s, %d, %d, %d)', + $panel_table->getTableName(), + $panel_table->generatePHID(), + $role->getPHID(), + PhabricatorRoleWorkboardProfileMenuItem::MENUITEMKEY, + PhabricatorRole::ITEM_WORKBOARD, + PhabricatorProfileMenuItemConfiguration::VISIBILITY_DEFAULT, + '{}', + 2, + PhabricatorTime::getNow(), + PhabricatorTime::getNow()); +} diff --git a/resources/sql/autopatches/20181031.rolboard.01.queryreset.php b/resources/sql/autopatches/20181031.rolboard.01.queryreset.php new file mode 100644 index 0000000000..1b38222ef1 --- /dev/null +++ b/resources/sql/autopatches/20181031.rolboard.01.queryreset.php @@ -0,0 +1,50 @@ +establishConnection('w'); + +$iterator = new LiskMigrationIterator($table); +$search_engine = id(new ManiphestTaskSearchEngine()) + ->setViewer($viewer); + +foreach ($iterator as $role) { + $default_filter = $role->getDefaultWorkboardFilter(); + if (!strlen($default_filter)) { + continue; + } + + if ($search_engine->isBuiltinQuery($default_filter)) { + continue; + } + + $saved = id(new PhabricatorSavedQueryQuery()) + ->setViewer($viewer) + ->withQueryKeys(array($default_filter)) + ->executeOne(); + if ($saved) { + continue; + } + + $properties = $role->getProperties(); + unset($properties['workboard.filter.default']); + + queryfx( + $conn, + 'UPDATE %T SET properties = %s WHERE id = %d', + $table->getTableName(), + phutil_json_encode($properties), + $role->getID()); + + echo tsprintf( + "%s\n", + pht( + 'Role ("%s") had an invalid query saved as a default workboard '. + 'query. The query has been reset. See T13208.', + $role->getDisplayName())); +} diff --git a/resources/sql/autopatches/20190129.role.01.spaces.php b/resources/sql/autopatches/20190129.role.01.spaces.php new file mode 100644 index 0000000000..ff1ff68c7a --- /dev/null +++ b/resources/sql/autopatches/20190129.role.01.spaces.php @@ -0,0 +1,18 @@ +establishConnection('w'); +$table_name = $table->getTableName(); + +foreach (new LiskRawMigrationIterator($conn, $table_name) as $role_row) { + queryfx( + $conn, + 'UPDATE %R SET spacePHID = %ns + WHERE parentRolePHID = %s AND milestoneNumber IS NOT NULL', + $table, + $role_row['spacePHID'], + $role_row['phid']); +} diff --git a/resources/sql/patches/090.forceuniquerolesnames.php b/resources/sql/patches/090.forceuniquerolesnames.php new file mode 100644 index 0000000000..c54306b973 --- /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::normalizeRoleSlug($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::normalizeRoleSlug($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::normalizeRoleSlug($new_name).'/'; + + $okay = true; + foreach ($roles as $other) { + if ($other->getID() == $role->getID()) { + continue; + } + + $other_slug = PhabricatorSlug::normalizeRoleSlug($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 2aa25e75ba..d5fbe601b6 100644 --- a/resources/sql/quickstart.sql +++ b/resources/sql/quickstart.sql @@ -8283,6 +8283,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`; @@ -8667,6 +9052,8 @@ 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`; @@ -8771,6 +9158,28 @@ CREATE TABLE `releeph_project` ( 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 ; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 421ee76400..51fb545c05 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -4537,6 +4537,210 @@ 'PhabricatorProjectsSearchEngineAttachment' => 'applications/project/engineextension/PhabricatorProjectsSearchEngineAttachment.php', 'PhabricatorProjectsSearchEngineExtension' => 'applications/project/engineextension/PhabricatorProjectsSearchEngineExtension.php', 'PhabricatorProjectsWatchersSearchEngineAttachment' => 'applications/project/engineextension/PhabricatorProjectsWatchersSearchEngineAttachment.php', + 'HeraldExactRolesField' => 'extensions/roles/herald/HeraldExactRolesField.php', + 'HeraldRolesField' => 'extensions/roles/herald/HeraldRolesField.php', + 'PhabricatorRole' => 'extensions/roles/storage/PhabricatorRole.php', + 'PhabricatorRoleActivityChartEngine' => 'extensions/roles/chart/PhabricatorRoleActivityChartEngine.php', + 'PhabricatorRoleAddHeraldAction' => 'extensions/roles/herald/PhabricatorRoleAddHeraldAction.php', + 'PhabricatorRoleApplication' => 'extensions/roles/application/PhabricatorRoleApplication.php', + 'PhabricatorRoleArchiveController' => 'extensions/roles/controller/PhabricatorRoleArchiveController.php', + 'PhabricatorRoleBoardBackgroundController' => 'extensions/roles/controller/PhabricatorRoleBoardBackgroundController.php', + 'PhabricatorRoleBoardController' => 'extensions/roles/controller/PhabricatorRoleBoardController.php', + 'PhabricatorRoleBoardDefaultController' => 'extensions/roles/controller/PhabricatorRoleBoardDefaultController.php', + 'PhabricatorRoleBoardDisableController' => 'extensions/roles/controller/PhabricatorRoleBoardDisableController.php', + 'PhabricatorRoleBoardFilterController' => 'extensions/roles/controller/PhabricatorRoleBoardFilterController.php', + 'PhabricatorRoleBoardImportController' => 'extensions/roles/controller/PhabricatorRoleBoardImportController.php', + 'PhabricatorRoleBoardManageController' => 'extensions/roles/controller/PhabricatorRoleBoardManageController.php', + 'PhabricatorRoleBoardReloadController' => 'extensions/roles/controller/PhabricatorRoleBoardReloadController.php', + 'PhabricatorRoleBoardReorderController' => 'extensions/roles/controller/PhabricatorRoleBoardReorderController.php', + 'PhabricatorRoleBoardViewController' => 'extensions/roles/controller/PhabricatorRoleBoardViewController.php', + 'PhabricatorRoleBuiltinsExample' => 'applications/uiexample/examples/PhabricatorRoleBuiltinsExample.php', + 'PhabricatorRoleBurndownChartEngine' => 'extensions/roles/chart/PhabricatorRoleBurndownChartEngine.php', + 'PhabricatorRoleCardView' => 'extensions/roles/view/PhabricatorRoleCardView.php', + 'PhabricatorRoleColorTransaction' => 'extensions/roles/xaction/PhabricatorRoleColorTransaction.php', + 'PhabricatorRoleColorsConfigType' => 'extensions/roles/config/PhabricatorRoleColorsConfigType.php', + 'PhabricatorRoleColumn' => 'extensions/roles/storage/PhabricatorRoleColumn.php', + 'PhabricatorRoleColumnAuthorOrder' => 'extensions/roles/order/PhabricatorRoleColumnAuthorOrder.php', + 'PhabricatorRoleColumnBulkEditController' => 'extensions/roles/controller/PhabricatorRoleColumnBulkEditController.php', + 'PhabricatorRoleColumnBulkMoveController' => 'extensions/roles/controller/PhabricatorRoleColumnBulkMoveController.php', + 'PhabricatorRoleColumnCreatedOrder' => 'extensions/roles/order/PhabricatorRoleColumnCreatedOrder.php', + 'PhabricatorRoleColumnDetailController' => 'extensions/roles/controller/PhabricatorRoleColumnDetailController.php', + 'PhabricatorRoleColumnEditController' => 'extensions/roles/controller/PhabricatorRoleColumnEditController.php', + 'PhabricatorRoleColumnHeader' => 'extensions/roles/order/PhabricatorRoleColumnHeader.php', + 'PhabricatorRoleColumnHideController' => 'extensions/roles/controller/PhabricatorRoleColumnHideController.php', + 'PhabricatorRoleColumnLimitTransaction' => 'extensions/roles/xaction/column/PhabricatorRoleColumnLimitTransaction.php', + 'PhabricatorRoleColumnNameTransaction' => 'extensions/roles/xaction/column/PhabricatorRoleColumnNameTransaction.php', + 'PhabricatorRoleColumnNaturalOrder' => 'extensions/roles/order/PhabricatorRoleColumnNaturalOrder.php', + 'PhabricatorRoleColumnOrder' => 'extensions/roles/order/PhabricatorRoleColumnOrder.php', + 'PhabricatorRoleColumnOwnerOrder' => 'extensions/roles/order/PhabricatorRoleColumnOwnerOrder.php', + 'PhabricatorRoleColumnPHIDType' => 'extensions/roles/phid/PhabricatorRoleColumnPHIDType.php', + 'PhabricatorRoleColumnPointsOrder' => 'extensions/roles/order/PhabricatorRoleColumnPointsOrder.php', + 'PhabricatorRoleColumnPosition' => 'extensions/roles/storage/PhabricatorRoleColumnPosition.php', + 'PhabricatorRoleColumnPositionQuery' => 'extensions/roles/query/PhabricatorRoleColumnPositionQuery.php', + 'PhabricatorRoleColumnPriorityOrder' => 'extensions/roles/order/PhabricatorRoleColumnPriorityOrder.php', + 'PhabricatorRoleColumnQuery' => 'extensions/roles/query/PhabricatorRoleColumnQuery.php', + 'PhabricatorRoleColumnRemoveTriggerController' => 'extensions/roles/controller/PhabricatorRoleColumnRemoveTriggerController.php', + 'PhabricatorRoleColumnSearchEngine' => 'extensions/roles/query/PhabricatorRoleColumnSearchEngine.php', + 'PhabricatorRoleColumnStatusOrder' => 'extensions/roles/order/PhabricatorRoleColumnStatusOrder.php', + 'PhabricatorRoleColumnStatusTransaction' => 'extensions/roles/xaction/column/PhabricatorRoleColumnStatusTransaction.php', + 'PhabricatorRoleColumnTitleOrder' => 'extensions/roles/order/PhabricatorRoleColumnTitleOrder.php', + 'PhabricatorRoleColumnTransaction' => 'extensions/roles/storage/PhabricatorRoleColumnTransaction.php', + 'PhabricatorRoleColumnTransactionEditor' => 'extensions/roles/editor/PhabricatorRoleColumnTransactionEditor.php', + 'PhabricatorRoleColumnTransactionQuery' => 'extensions/roles/query/PhabricatorRoleColumnTransactionQuery.php', + 'PhabricatorRoleColumnTransactionType' => 'extensions/roles/xaction/column/PhabricatorRoleColumnTransactionType.php', + 'PhabricatorRoleColumnTriggerTransaction' => 'extensions/roles/xaction/column/PhabricatorRoleColumnTriggerTransaction.php', + 'PhabricatorRoleColumnViewQueryController' => 'extensions/roles/controller/PhabricatorRoleColumnViewQueryController.php', + 'PhabricatorRoleConfigOptions' => 'extensions/roles/config/PhabricatorRoleConfigOptions.php', + 'PhabricatorRoleConfiguredCustomField' => 'extensions/roles/customfield/PhabricatorRoleConfiguredCustomField.php', + 'PhabricatorRoleController' => 'extensions/roles/controller/PhabricatorRoleController.php', + 'PhabricatorRoleCoreTestCase' => 'extensions/roles/__tests__/PhabricatorRoleCoreTestCase.php', + 'PhabricatorRoleCoverController' => 'extensions/roles/controller/PhabricatorRoleCoverController.php', + 'PhabricatorRoleCustomField' => 'extensions/roles/customfield/PhabricatorRoleCustomField.php', + 'PhabricatorRoleCustomFieldNumericIndex' => 'extensions/roles/storage/PhabricatorRoleCustomFieldNumericIndex.php', + 'PhabricatorRoleCustomFieldStorage' => 'extensions/roles/storage/PhabricatorRoleCustomFieldStorage.php', + 'PhabricatorRoleCustomFieldStringIndex' => 'extensions/roles/storage/PhabricatorRoleCustomFieldStringIndex.php', + 'PhabricatorRoleDAO' => 'extensions/roles/storage/PhabricatorRoleDAO.php', + 'PhabricatorRoleDatasource' => 'extensions/roles/typeahead/PhabricatorRoleDatasource.php', + 'PhabricatorRoleDescriptionField' => 'extensions/roles/customfield/PhabricatorRoleDescriptionField.php', + 'PhabricatorRoleDetailsProfileMenuItem' => 'extensions/roles/menuitem/PhabricatorRoleDetailsProfileMenuItem.php', + 'PhabricatorRoleDropEffect' => 'extensions/roles/icon/PhabricatorRoleDropEffect.php', + 'PhabricatorRoleEditController' => 'extensions/roles/controller/PhabricatorRoleEditController.php', + 'PhabricatorRoleEditEngine' => 'extensions/roles/engine/PhabricatorRoleEditEngine.php', + 'PhabricatorRoleEditPictureController' => 'extensions/roles/controller/PhabricatorRoleEditPictureController.php', + 'PhabricatorRoleFerretEngine' => 'extensions/roles/search/PhabricatorRoleFerretEngine.php', + 'PhabricatorRoleFilterTransaction' => 'extensions/roles/xaction/PhabricatorRoleFilterTransaction.php', + 'PhabricatorRoleFulltextEngine' => 'extensions/roles/search/PhabricatorRoleFulltextEngine.php', + 'PhabricatorRoleHeraldAction' => 'extensions/roles/herald/PhabricatorRoleHeraldAction.php', + 'PhabricatorRoleHeraldAdapter' => 'extensions/roles/herald/PhabricatorRoleHeraldAdapter.php', + 'PhabricatorRoleHeraldFieldGroup' => 'extensions/roles/herald/PhabricatorRoleHeraldFieldGroup.php', + 'PhabricatorRoleHovercardEngineExtension' => 'extensions/roles/engineextension/PhabricatorRoleHovercardEngineExtension.php', + 'PhabricatorRoleIconSet' => 'extensions/roles/icon/PhabricatorRoleIconSet.php', + 'PhabricatorRoleIconTransaction' => 'extensions/roles/xaction/PhabricatorRoleIconTransaction.php', + 'PhabricatorRoleIconsConfigType' => 'extensions/roles/config/PhabricatorRoleIconsConfigType.php', + 'PhabricatorRoleImageTransaction' => 'extensions/roles/xaction/PhabricatorRoleImageTransaction.php', + 'PhabricatorRoleInterface' => 'extensions/roles/interface/PhabricatorRoleInterface.php', + 'PhabricatorRoleListController' => 'extensions/roles/controller/PhabricatorRoleListController.php', + 'PhabricatorRoleListView' => 'extensions/roles/view/PhabricatorRoleListView.php', + 'PhabricatorRoleLockController' => 'extensions/roles/controller/PhabricatorRoleLockController.php', + 'PhabricatorRoleLockTransaction' => 'extensions/roles/xaction/PhabricatorRoleLockTransaction.php', + 'PhabricatorRoleLogicalAncestorDatasource' => 'extensions/roles/typeahead/PhabricatorRoleLogicalAncestorDatasource.php', + 'PhabricatorRoleLogicalDatasource' => 'extensions/roles/typeahead/PhabricatorRoleLogicalDatasource.php', + 'PhabricatorRoleLogicalOnlyDatasource' => 'extensions/roles/typeahead/PhabricatorRoleLogicalOnlyDatasource.php', + 'PhabricatorRoleLogicalOrNotDatasource' => 'extensions/roles/typeahead/PhabricatorRoleLogicalOrNotDatasource.php', + 'PhabricatorRoleLogicalUserDatasource' => 'extensions/roles/typeahead/PhabricatorRoleLogicalUserDatasource.php', + 'PhabricatorRoleLogicalViewerDatasource' => 'extensions/roles/typeahead/PhabricatorRoleLogicalViewerDatasource.php', + 'PhabricatorRoleManageController' => 'extensions/roles/controller/PhabricatorRoleManageController.php', + 'PhabricatorRoleManageProfileMenuItem' => 'extensions/roles/menuitem/PhabricatorRoleManageProfileMenuItem.php', + 'PhabricatorRoleMaterializedMemberEdgeType' => 'extensions/roles/edge/PhabricatorRoleMaterializedMemberEdgeType.php', + 'PhabricatorRoleMemberListView' => 'extensions/roles/view/PhabricatorRoleMemberListView.php', + 'PhabricatorRoleMemberOfRoleEdgeType' => 'extensions/roles/edge/PhabricatorRoleMemberOfRoleEdgeType.php', + 'PhabricatorRoleMembersAddController' => 'extensions/roles/controller/PhabricatorRoleMembersAddController.php', + 'PhabricatorRoleMembersDatasource' => 'extensions/roles/typeahead/PhabricatorRoleMembersDatasource.php', + 'PhabricatorRoleMembersPolicyRule' => 'extensions/roles/policyrule/PhabricatorRoleMembersPolicyRule.php', + 'PhabricatorRoleMembersProfileMenuItem' => 'extensions/roles/menuitem/PhabricatorRoleMembersProfileMenuItem.php', + 'PhabricatorRoleMembersRemoveController' => 'extensions/roles/controller/PhabricatorRoleMembersRemoveController.php', + 'PhabricatorRoleMembersViewController' => 'extensions/roles/controller/PhabricatorRoleMembersViewController.php', + 'PhabricatorRoleMenuItemController' => 'extensions/roles/controller/PhabricatorRoleMenuItemController.php', + 'PhabricatorRoleMilestoneTransaction' => 'extensions/roles/xaction/PhabricatorRoleMilestoneTransaction.php', + 'PhabricatorRoleMoveController' => 'extensions/roles/controller/PhabricatorRoleMoveController.php', + 'PhabricatorRoleNameContextFreeGrammar' => 'extensions/roles/lipsum/PhabricatorRoleNameContextFreeGrammar.php', + 'PhabricatorRoleNameTransaction' => 'extensions/roles/xaction/PhabricatorRoleNameTransaction.php', + 'PhabricatorRoleNoRolesDatasource' => 'extensions/roles/typeahead/PhabricatorRoleNoRolesDatasource.php', + 'PhabricatorRoleObjectHasRoleEdgeType' => 'extensions/roles/edge/PhabricatorRoleObjectHasRoleEdgeType.php', + 'PhabricatorRoleOrUserDatasource' => 'extensions/roles/typeahead/PhabricatorRoleOrUserDatasource.php', + 'PhabricatorRoleOrUserFunctionDatasource' => 'extensions/roles/typeahead/PhabricatorRoleOrUserFunctionDatasource.php', + 'PhabricatorRolePHIDResolver' => 'applications/phid/resolver/PhabricatorRolePHIDResolver.php', + 'PhabricatorRoleParentTransaction' => 'extensions/roles/xaction/PhabricatorRoleParentTransaction.php', + 'PhabricatorRolePictureProfileMenuItem' => 'extensions/roles/menuitem/PhabricatorRolePictureProfileMenuItem.php', + 'PhabricatorRolePointsProfileMenuItem' => 'extensions/roles/menuitem/PhabricatorRolePointsProfileMenuItem.php', + 'PhabricatorRoleProfileController' => 'extensions/roles/controller/PhabricatorRoleProfileController.php', + 'PhabricatorRoleProfileMenuEngine' => 'extensions/roles/engine/PhabricatorRoleProfileMenuEngine.php', + 'PhabricatorRoleProfileMenuItem' => 'applications/search/menuitem/PhabricatorRoleProfileMenuItem.php', + 'PhabricatorRoleRoleHasMemberEdgeType' => 'extensions/roles/edge/PhabricatorRoleRoleHasMemberEdgeType.php', + 'PhabricatorRoleRoleHasObjectEdgeType' => 'extensions/roles/edge/PhabricatorRoleRoleHasObjectEdgeType.php', + 'PhabricatorRoleRolePHIDType' => 'extensions/roles/phid/PhabricatorRoleRolePHIDType.php', + 'PhabricatorRoleQuery' => 'extensions/roles/query/PhabricatorRoleQuery.php', + 'PhabricatorRoleRemoveHeraldAction' => 'extensions/roles/herald/PhabricatorRoleRemoveHeraldAction.php', + 'PhabricatorRoleReportsController' => 'extensions/roles/controller/PhabricatorRoleReportsController.php', + 'PhabricatorRoleReportsProfileMenuItem' => 'extensions/roles/menuitem/PhabricatorRoleReportsProfileMenuItem.php', + 'PhabricatorRoleSchemaSpec' => 'extensions/roles/storage/PhabricatorRoleSchemaSpec.php', + 'PhabricatorRoleSearchEngine' => 'extensions/roles/query/PhabricatorRoleSearchEngine.php', + 'PhabricatorRoleSearchField' => 'extensions/roles/searchfield/PhabricatorRoleSearchField.php', + 'PhabricatorRoleSilenceController' => 'extensions/roles/controller/PhabricatorRoleSilenceController.php', + 'PhabricatorRoleSilencedEdgeType' => 'extensions/roles/edge/PhabricatorRoleSilencedEdgeType.php', + 'PhabricatorRoleSlug' => 'extensions/roles/storage/PhabricatorRoleSlug.php', + 'PhabricatorRoleSlugsTransaction' => 'extensions/roles/xaction/PhabricatorRoleSlugsTransaction.php', + 'PhabricatorRoleSortTransaction' => 'extensions/roles/xaction/PhabricatorRoleSortTransaction.php', + 'PhabricatorRoleStandardCustomField' => 'extensions/roles/customfield/PhabricatorRoleStandardCustomField.php', + 'PhabricatorRoleStatus' => 'extensions/roles/constants/PhabricatorRoleStatus.php', + 'PhabricatorRoleStatusTransaction' => 'extensions/roles/xaction/PhabricatorRoleStatusTransaction.php', + 'PhabricatorRoleSubRoleWarningController' => 'extensions/roles/controller/PhabricatorRoleSubRoleWarningController.php', + 'PhabricatorRoleSubRolesController' => 'extensions/roles/controller/PhabricatorRoleSubRolesController.php', + 'PhabricatorRoleSubRolesProfileMenuItem' => 'extensions/roles/menuitem/PhabricatorRoleSubRolesProfileMenuItem.php', + 'PhabricatorRoleSubtypeDatasource' => 'extensions/roles/typeahead/PhabricatorRoleSubtypeDatasource.php', + 'PhabricatorRoleSubtypesConfigType' => 'extensions/roles/config/PhabricatorRoleSubtypesConfigType.php', + 'PhabricatorRoleTagsAddedField' => 'extensions/roles/herald/PhabricatorRoleTagsAddedField.php', + 'PhabricatorRoleTagsField' => 'extensions/roles/herald/PhabricatorRoleTagsField.php', + 'PhabricatorRoleTagsRemovedField' => 'extensions/roles/herald/PhabricatorRoleTagsRemovedField.php', + 'PhabricatorRoleTestDataGenerator' => 'extensions/roles/lipsum/PhabricatorRoleTestDataGenerator.php', + 'PhabricatorRoleTransaction' => 'extensions/roles/storage/PhabricatorRoleTransaction.php', + 'PhabricatorRoleTransactionEditor' => 'extensions/roles/editor/PhabricatorRoleTransactionEditor.php', + 'PhabricatorRoleTransactionQuery' => 'extensions/roles/query/PhabricatorRoleTransactionQuery.php', + 'PhabricatorRoleTransactionType' => 'extensions/roles/xaction/PhabricatorRoleTransactionType.php', + 'PhabricatorRoleTrigger' => 'extensions/roles/storage/PhabricatorRoleTrigger.php', + 'PhabricatorRoleTriggerAddRolesRule' => 'extensions/roles/trigger/PhabricatorRoleTriggerAddRolesRule.php', + 'PhabricatorRoleTriggerController' => 'extensions/roles/controller/trigger/PhabricatorRoleTriggerController.php', + 'PhabricatorRoleTriggerCorruptionException' => 'extensions/roles/exception/PhabricatorRoleTriggerCorruptionException.php', + 'PhabricatorRoleTriggerEditController' => 'extensions/roles/controller/trigger/PhabricatorRoleTriggerEditController.php', + 'PhabricatorRoleTriggerEditor' => 'extensions/roles/editor/PhabricatorRoleTriggerEditor.php', + 'PhabricatorRoleTriggerInvalidRule' => 'extensions/roles/trigger/PhabricatorRoleTriggerInvalidRule.php', + 'PhabricatorRoleTriggerListController' => 'extensions/roles/controller/trigger/PhabricatorRoleTriggerListController.php', + 'PhabricatorRoleTriggerManiphestOwnerRule' => 'extensions/roles/trigger/PhabricatorRoleTriggerManiphestOwnerRule.php', + 'PhabricatorRoleTriggerManiphestPriorityRule' => 'extensions/roles/trigger/PhabricatorRoleTriggerManiphestPriorityRule.php', + 'PhabricatorRoleTriggerManiphestStatusRule' => 'extensions/roles/trigger/PhabricatorRoleTriggerManiphestStatusRule.php', + 'PhabricatorRoleTriggerNameTransaction' => 'extensions/roles/xaction/trigger/PhabricatorRoleTriggerNameTransaction.php', + 'PhabricatorRoleTriggerPHIDType' => 'extensions/roles/phid/PhabricatorRoleTriggerPHIDType.php', + 'PhabricatorRoleTriggerPlaySoundRule' => 'extensions/roles/trigger/PhabricatorRoleTriggerPlaySoundRule.php', + 'PhabricatorRoleTriggerQuery' => 'extensions/roles/query/PhabricatorRoleTriggerQuery.php', + 'PhabricatorRoleTriggerRemoveRolesRule' => 'extensions/roles/trigger/PhabricatorRoleTriggerRemoveRolesRule.php', + 'PhabricatorRoleTriggerRule' => 'extensions/roles/trigger/PhabricatorRoleTriggerRule.php', + 'PhabricatorRoleTriggerRuleRecord' => 'extensions/roles/trigger/PhabricatorRoleTriggerRuleRecord.php', + 'PhabricatorRoleTriggerRulesetTransaction' => 'extensions/roles/xaction/trigger/PhabricatorRoleTriggerRulesetTransaction.php', + 'PhabricatorRoleTriggerSearchEngine' => 'extensions/roles/query/PhabricatorRoleTriggerSearchEngine.php', + 'PhabricatorRoleTriggerTransaction' => 'extensions/roles/storage/PhabricatorRoleTriggerTransaction.php', + 'PhabricatorRoleTriggerTransactionQuery' => 'extensions/roles/query/PhabricatorRoleTriggerTransactionQuery.php', + 'PhabricatorRoleTriggerTransactionType' => 'extensions/roles/xaction/trigger/PhabricatorRoleTriggerTransactionType.php', + 'PhabricatorRoleTriggerUnknownRule' => 'extensions/roles/trigger/PhabricatorRoleTriggerUnknownRule.php', + 'PhabricatorRoleTriggerUsage' => 'extensions/roles/storage/PhabricatorRoleTriggerUsage.php', + 'PhabricatorRoleTriggerUsageIndexEngineExtension' => 'extensions/roles/engineextension/PhabricatorRoleTriggerUsageIndexEngineExtension.php', + 'PhabricatorRoleTriggerViewController' => 'extensions/roles/controller/trigger/PhabricatorRoleTriggerViewController.php', + 'PhabricatorRoleTypeTransaction' => 'extensions/roles/xaction/PhabricatorRoleTypeTransaction.php', + 'PhabricatorRoleUIEventListener' => 'extensions/roles/events/PhabricatorRoleUIEventListener.php', + 'PhabricatorRoleUpdateController' => 'extensions/roles/controller/PhabricatorRoleUpdateController.php', + 'PhabricatorRoleUserFunctionDatasource' => 'extensions/roles/typeahead/PhabricatorRoleUserFunctionDatasource.php', + 'PhabricatorRoleUserListView' => 'extensions/roles/view/PhabricatorRoleUserListView.php', + 'PhabricatorRoleViewController' => 'extensions/roles/controller/PhabricatorRoleViewController.php', + 'PhabricatorRoleWatchController' => 'extensions/roles/controller/PhabricatorRoleWatchController.php', + 'PhabricatorRoleWatcherListView' => 'extensions/roles/view/PhabricatorRoleWatcherListView.php', + 'PhabricatorRoleWorkboardBackgroundColor' => 'extensions/roles/constants/PhabricatorRoleWorkboardBackgroundColor.php', + 'PhabricatorRoleWorkboardBackgroundTransaction' => 'extensions/roles/xaction/PhabricatorRoleWorkboardBackgroundTransaction.php', + 'PhabricatorRoleWorkboardProfileMenuItem' => 'extensions/roles/menuitem/PhabricatorRoleWorkboardProfileMenuItem.php', + 'PhabricatorRoleWorkboardTransaction' => 'extensions/roles/xaction/PhabricatorRoleWorkboardTransaction.php', + 'PhabricatorRolesAllPolicyRule' => 'extensions/roles/policyrule/PhabricatorRolesAllPolicyRule.php', + 'PhabricatorRolesAncestorsSearchEngineAttachment' => 'extensions/roles/engineextension/PhabsricatorRolesAncestorsSearchEngineAttachment.php', + 'PhabricatorRolesBasePolicyRule' => 'extensions/roles/policyrule/PhabricatorRolesBasePolicyRule.php', + 'PhabricatorRolesCurtainExtension' => 'extensions/roles/engineextension/PhabricatorRolesCurtainExtension.php', + 'PhabricatorRolesEditEngineExtension' => 'extensions/roles/engineextension/PhabricatorRolesEditEngineExtension.php', + 'PhabricatorRolesEditField' => 'applications/transactions/editfield/PhabricatorRolesEditField.php', + 'PhabricatorRolesExportEngineExtension' => 'infrastructure/export/engine/PhabricatorRolesExportEngineExtension.php', + 'PhabricatorRolesFulltextEngineExtension' => 'extensions/roles/engineextension/PhabricatorRolesFulltextEngineExtension.php', + 'PhabricatorRolesMailEngineExtension' => 'extensions/roles/engineextension/PhabricatorRolesMailEngineExtension.php', + 'PhabricatorRolesMembersSearchEngineAttachment' => 'extensions/roles/engineextension/PhabricatorRolesMembersSearchEngineAttachment.php', + 'PhabricatorRolesMembershipIndexEngineExtension' => 'extensions/roles/engineextension/PhabricatorRolesMembershipIndexEngineExtension.php', + 'PhabricatorRolesPolicyRule' => 'extensions/roles/policyrule/PhabricatorRolesPolicyRule.php', + 'PhabricatorRolesSearchEngineAttachment' => 'extensions/roles/engineextension/PhabricatorRolesSearchEngineAttachment.php', + 'PhabricatorRolesSearchEngineExtension' => 'extensions/roles/engineextension/PhabricatorRolesSearchEngineExtension.php', + 'PhabricatorRolesWatchersSearchEngineAttachment' => 'extensions/roles/engineextension/PhabricatorRolesWatchersSearchEngineAttachment.php', 'PhabricatorPronounSetting' => 'applications/settings/setting/PhabricatorPronounSetting.php', 'PhabricatorProtocolLog' => 'infrastructure/log/PhabricatorProtocolLog.php', 'PhabricatorPureChartFunction' => 'applications/fact/chart/PhabricatorPureChartFunction.php', 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/extensions/roles/__tests__/PhabricatorRoleCoreTestCase.php b/src/extensions/roles/__tests__/PhabricatorRoleCoreTestCase.php new file mode 100644 index 0000000000..bed027b439 --- /dev/null +++ b/src/extensions/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 PhabricatorBoardLayoutEngine()) + ->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 PhabricatorBoardLayoutEngine()) + ->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 (!strlen($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/extensions/roles/application/PhabricatorRoleApplication.php b/src/extensions/roles/application/PhabricatorRoleApplication.php new file mode 100644 index 0000000000..07a19f2f9f --- /dev/null +++ b/src/extensions/roles/application/PhabricatorRoleApplication.php @@ -0,0 +1,177 @@ + 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', + ), + '/tag/' => array( + '(?P[^/]+)/' => 'PhabricatorRoleViewController', + '(?P[^/]+)/board/' => 'PhabricatorRoleBoardViewController', + ), + ); + } + + 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/extensions/roles/capability/RoleCanLockRolesCapability.php b/src/extensions/roles/capability/RoleCanLockRolesCapability.php new file mode 100644 index 0000000000..e159d7b805 --- /dev/null +++ b/src/extensions/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/extensions/roles/chart/PhabricatorRoleBurndownChartEngine.php b/src/extensions/roles/chart/PhabricatorRoleBurndownChartEngine.php new file mode 100644 index 0000000000..7f0a2d61ad --- /dev/null +++ b/src/extensions/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/extensions/roles/command/ProjectAddRolesEmailCommand.php b/src/extensions/roles/command/ProjectAddRolesEmailCommand.php new file mode 100644 index 0000000000..cc72479e73 --- /dev/null +++ b/src/extensions/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/extensions/roles/conduit/RoleColumnSearchConduitAPIMethod.php b/src/extensions/roles/conduit/RoleColumnSearchConduitAPIMethod.php new file mode 100644 index 0000000000..c280aefd0b --- /dev/null +++ b/src/extensions/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/extensions/roles/conduit/RoleCreateConduitAPIMethod.php b/src/extensions/roles/conduit/RoleCreateConduitAPIMethod.php new file mode 100644 index 0000000000..629240c4da --- /dev/null +++ b/src/extensions/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/extensions/roles/conduit/RoleEditConduitAPIMethod.php b/src/extensions/roles/conduit/RoleEditConduitAPIMethod.php new file mode 100644 index 0000000000..2292341f7d --- /dev/null +++ b/src/extensions/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::normalizeRoleSlug($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/extensions/roles/conduit/RoleSearchConduitAPIMethod.php b/src/extensions/roles/conduit/RoleSearchConduitAPIMethod.php new file mode 100644 index 0000000000..df68bff9e4 --- /dev/null +++ b/src/extensions/roles/conduit/RoleSearchConduitAPIMethod.php @@ -0,0 +1,24 @@ + $query->getSlugMap(), + ); + } + +} diff --git a/src/extensions/roles/config/PhabricatorRoleColorsConfigType.php b/src/extensions/roles/config/PhabricatorRoleColorsConfigType.php new file mode 100644 index 0000000000..4d4cb2218f --- /dev/null +++ b/src/extensions/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/extensions/roles/config/PhabricatorRoleIconsConfigType.php b/src/extensions/roles/config/PhabricatorRoleIconsConfigType.php new file mode 100644 index 0000000000..84ea98e93e --- /dev/null +++ b/src/extensions/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/extensions/roles/constants/PhabricatorRoleWorkboardBackgroundColor.php b/src/extensions/roles/constants/PhabricatorRoleWorkboardBackgroundColor.php new file mode 100644 index 0000000000..e0f6fef747 --- /dev/null +++ b/src/extensions/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/extensions/roles/controller/PhabricatorRoleArchiveController.php b/src/extensions/roles/controller/PhabricatorRoleArchiveController.php new file mode 100644 index 0000000000..912e255945 --- /dev/null +++ b/src/extensions/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/extensions/roles/controller/PhabricatorRoleBoardBackgroundController.php b/src/extensions/roles/controller/PhabricatorRoleBoardBackgroundController.php new file mode 100644 index 0000000000..facdb5ee6a --- /dev/null +++ b/src/extensions/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/extensions/roles/controller/PhabricatorRoleBoardController.php b/src/extensions/roles/controller/PhabricatorRoleBoardController.php new file mode 100644 index 0000000000..6e325785df --- /dev/null +++ b/src/extensions/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 PhabricatorWorkboardViewState()) + ->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/extensions/roles/controller/PhabricatorRoleBoardDefaultController.php b/src/extensions/roles/controller/PhabricatorRoleBoardDefaultController.php new file mode 100644 index 0000000000..1b3beacbed --- /dev/null +++ b/src/extensions/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/extensions/roles/controller/PhabricatorRoleBoardDisableController.php b/src/extensions/roles/controller/PhabricatorRoleBoardDisableController.php new file mode 100644 index 0000000000..ae0bd9b914 --- /dev/null +++ b/src/extensions/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/extensions/roles/controller/PhabricatorRoleBoardFilterController.php b/src/extensions/roles/controller/PhabricatorRoleBoardFilterController.php new file mode 100644 index 0000000000..b22f55e0bd --- /dev/null +++ b/src/extensions/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/extensions/roles/controller/PhabricatorRoleBoardImportController.php b/src/extensions/roles/controller/PhabricatorRoleBoardImportController.php new file mode 100644 index 0000000000..b0c3281ae3 --- /dev/null +++ b/src/extensions/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/extensions/roles/controller/PhabricatorRoleBoardManageController.php b/src/extensions/roles/controller/PhabricatorRoleBoardManageController.php new file mode 100644 index 0000000000..23aaee082c --- /dev/null +++ b/src/extensions/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 PhabricatorBoardLayoutEngine()) + ->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('role-view-home') + ->addClass('role-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/extensions/roles/controller/PhabricatorRoleBoardReloadController.php b/src/extensions/roles/controller/PhabricatorRoleBoardReloadController.php new file mode 100644 index 0000000000..f8dd6c8502 --- /dev/null +++ b/src/extensions/roles/controller/PhabricatorRoleBoardReloadController.php @@ -0,0 +1,73 @@ +getViewer(); + + $response = $this->loadRole(); + if ($response) { + return $response; + } + + $order = $request->getStr('order'); + if (!strlen($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 PhabricatorBoardResponseEngine()) + ->setViewer($viewer) + ->setBoardPHID($board_phid) + ->setOrdering($ordering) + ->setObjects($objects) + ->setUpdatePHIDs($update_phids) + ->setVisiblePHIDs($visible_phids); + + return $engine->buildResponse(); + } + +} diff --git a/src/extensions/roles/controller/PhabricatorRoleBoardReorderController.php b/src/extensions/roles/controller/PhabricatorRoleBoardReorderController.php new file mode 100644 index 0000000000..9d0534a530 --- /dev/null +++ b/src/extensions/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/extensions/roles/controller/PhabricatorRoleBoardViewController.php b/src/extensions/roles/controller/PhabricatorRoleBoardViewController.php new file mode 100644 index 0000000000..0770398598 --- /dev/null +++ b/src/extensions/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 PhabricatorBoardRenderingEngine()) + ->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()] = + PhabricatorBoardResponseEngine::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('role-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/extensions/roles/controller/PhabricatorRoleColumnBulkEditController.php b/src/extensions/roles/controller/PhabricatorRoleColumnBulkEditController.php new file mode 100644 index 0000000000..931e29bd09 --- /dev/null +++ b/src/extensions/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/extensions/roles/controller/PhabricatorRoleColumnBulkMoveController.php b/src/extensions/roles/controller/PhabricatorRoleColumnBulkMoveController.php new file mode 100644 index 0000000000..be71cd4113 --- /dev/null +++ b/src/extensions/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 PhabricatorBoardLayoutEngine()) + ->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/extensions/roles/controller/PhabricatorRoleColumnDetailController.php b/src/extensions/roles/controller/PhabricatorRoleColumnDetailController.php new file mode 100644 index 0000000000..9f74f07f0d --- /dev/null +++ b/src/extensions/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('role-view-home') + ->addClass('role-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/extensions/roles/controller/PhabricatorRoleColumnEditController.php b/src/extensions/roles/controller/PhabricatorRoleColumnEditController.php new file mode 100644 index 0000000000..7d29819f8d --- /dev/null +++ b/src/extensions/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/extensions/roles/controller/PhabricatorRoleColumnHideController.php b/src/extensions/roles/controller/PhabricatorRoleColumnHideController.php new file mode 100644 index 0000000000..1557de6c0a --- /dev/null +++ b/src/extensions/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/extensions/roles/controller/PhabricatorRoleColumnRemoveTriggerController.php b/src/extensions/roles/controller/PhabricatorRoleColumnRemoveTriggerController.php new file mode 100644 index 0000000000..da0bcbf559 --- /dev/null +++ b/src/extensions/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/extensions/roles/controller/PhabricatorRoleColumnViewQueryController.php b/src/extensions/roles/controller/PhabricatorRoleColumnViewQueryController.php new file mode 100644 index 0000000000..f480cb21d7 --- /dev/null +++ b/src/extensions/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 PhabricatorBoardLayoutEngine()) + ->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/extensions/roles/controller/PhabricatorRoleController.php b/src/extensions/roles/controller/PhabricatorRoleController.php new file mode 100644 index 0000000000..d78e25b785 --- /dev/null +++ b/src/extensions/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::normalizeRoleSlug($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 PhabricatorBoardResponseEngine()) + ->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/extensions/roles/controller/PhabricatorRoleCoverController.php b/src/extensions/roles/controller/PhabricatorRoleCoverController.php new file mode 100644 index 0000000000..9eb4ca9aa5 --- /dev/null +++ b/src/extensions/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/extensions/roles/controller/PhabricatorRoleEditController.php b/src/extensions/roles/controller/PhabricatorRoleEditController.php new file mode 100644 index 0000000000..01fd652fcc --- /dev/null +++ b/src/extensions/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/extensions/roles/controller/PhabricatorRoleEditPictureController.php b/src/extensions/roles/controller/PhabricatorRoleEditPictureController.php new file mode 100644 index 0000000000..f56ddbd945 --- /dev/null +++ b/src/extensions/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/extensions/roles/controller/PhabricatorRoleListController.php b/src/extensions/roles/controller/PhabricatorRoleListController.php new file mode 100644 index 0000000000..e4595c88b4 --- /dev/null +++ b/src/extensions/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/extensions/roles/controller/PhabricatorRoleLockController.php b/src/extensions/roles/controller/PhabricatorRoleLockController.php new file mode 100644 index 0000000000..4e62492d3b --- /dev/null +++ b/src/extensions/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/extensions/roles/controller/PhabricatorRoleManageController.php b/src/extensions/roles/controller/PhabricatorRoleManageController.php new file mode 100644 index 0000000000..3987ec2f03 --- /dev/null +++ b/src/extensions/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('role-view-home') + ->addClass('role-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/extensions/roles/controller/PhabricatorRoleMembersAddController.php b/src/extensions/roles/controller/PhabricatorRoleMembersAddController.php new file mode 100644 index 0000000000..1940f6e2d3 --- /dev/null +++ b/src/extensions/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/extensions/roles/controller/PhabricatorRoleMembersRemoveController.php b/src/extensions/roles/controller/PhabricatorRoleMembersRemoveController.php new file mode 100644 index 0000000000..96fe1d94ff --- /dev/null +++ b/src/extensions/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/extensions/roles/controller/PhabricatorRoleMembersViewController.php b/src/extensions/roles/controller/PhabricatorRoleMembersViewController.php new file mode 100644 index 0000000000..afb4e8670e --- /dev/null +++ b/src/extensions/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('role-view-home') + ->addClass('role-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/extensions/roles/controller/PhabricatorRoleMenuItemController.php b/src/extensions/roles/controller/PhabricatorRoleMenuItemController.php new file mode 100644 index 0000000000..657f46ea68 --- /dev/null +++ b/src/extensions/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/extensions/roles/controller/PhabricatorRoleMoveController.php b/src/extensions/roles/controller/PhabricatorRoleMoveController.php new file mode 100644 index 0000000000..8fca5d2181 --- /dev/null +++ b/src/extensions/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 (!strlen($order)) { + $order = PhabricatorRoleColumnNaturalOrder::ORDERKEY; + } + + $ordering = PhabricatorRoleColumnOrder::getOrderByKey($order); + $ordering = id(clone $ordering) + ->setViewer($viewer); + + $edit_header = null; + $raw_header = $request->getStr('header'); + if (strlen($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 PhabricatorBoardLayoutEngine()) + ->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/extensions/roles/controller/PhabricatorRoleProfileController.php b/src/extensions/roles/controller/PhabricatorRoleProfileController.php new file mode 100644 index 0000000000..fc6ad73dab --- /dev/null +++ b/src/extensions/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('role-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('role-view-feed') + ->appendChild( + array( + $overheated_view, + $feed, + )); + + require_celerity_resource('project-view-css'); + + $home = id(new PHUITwoColumnView()) + ->setHeader($header) + ->addClass('role-view-home') + ->addClass('role-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('role-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/extensions/roles/controller/PhabricatorRoleReportsController.php b/src/extensions/roles/controller/PhabricatorRoleReportsController.php new file mode 100644 index 0000000000..92601cb1d5 --- /dev/null +++ b/src/extensions/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/extensions/roles/controller/PhabricatorRoleSilenceController.php b/src/extensions/roles/controller/PhabricatorRoleSilenceController.php new file mode 100644 index 0000000000..9259319efe --- /dev/null +++ b/src/extensions/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/extensions/roles/controller/PhabricatorRoleSubroleWarningController.php b/src/extensions/roles/controller/PhabricatorRoleSubroleWarningController.php new file mode 100644 index 0000000000..341c49d89a --- /dev/null +++ b/src/extensions/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/extensions/roles/controller/PhabricatorRoleSubrolesController.php b/src/extensions/roles/controller/PhabricatorRoleSubrolesController.php new file mode 100644 index 0000000000..04f4820540 --- /dev/null +++ b/src/extensions/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('role-view-home') + ->addClass('role-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/extensions/roles/controller/PhabricatorRoleUpdateController.php b/src/extensions/roles/controller/PhabricatorRoleUpdateController.php new file mode 100644 index 0000000000..ce08e17eb5 --- /dev/null +++ b/src/extensions/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/extensions/roles/controller/PhabricatorRoleViewController.php b/src/extensions/roles/controller/PhabricatorRoleViewController.php new file mode 100644 index 0000000000..e550e625fe --- /dev/null +++ b/src/extensions/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/extensions/roles/controller/PhabricatorRoleWatchController.php b/src/extensions/roles/controller/PhabricatorRoleWatchController.php new file mode 100644 index 0000000000..e11b8c18a1 --- /dev/null +++ b/src/extensions/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/extensions/roles/controller/trigger/PhabricatorRoleTriggerController.php b/src/extensions/roles/controller/trigger/PhabricatorRoleTriggerController.php new file mode 100644 index 0000000000..638f8de8de --- /dev/null +++ b/src/extensions/roles/controller/trigger/PhabricatorRoleTriggerController.php @@ -0,0 +1,16 @@ +addTextCrumb( + pht('Triggers'), + $this->getApplicationURI('trigger/')); + + return $crumbs; + } + +} diff --git a/src/extensions/roles/controller/trigger/PhabricatorRoleTriggerEditController.php b/src/extensions/roles/controller/trigger/PhabricatorRoleTriggerEditController.php new file mode 100644 index 0000000000..bd2e818fd6 --- /dev/null +++ b/src/extensions/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('role-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/extensions/roles/controller/trigger/PhabricatorRoleTriggerListController.php b/src/extensions/roles/controller/trigger/PhabricatorRoleTriggerListController.php new file mode 100644 index 0000000000..2d320bf5ce --- /dev/null +++ b/src/extensions/roles/controller/trigger/PhabricatorRoleTriggerListController.php @@ -0,0 +1,16 @@ +setController($this) + ->buildResponse(); + } + +} diff --git a/src/extensions/roles/controller/trigger/PhabricatorRoleTriggerViewController.php b/src/extensions/roles/controller/trigger/PhabricatorRoleTriggerViewController.php new file mode 100644 index 0000000000..39b2e736d4 --- /dev/null +++ b/src/extensions/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/extensions/roles/customfield/PhabricatorRoleConfiguredCustomField.php b/src/extensions/roles/customfield/PhabricatorRoleConfiguredCustomField.php new file mode 100644 index 0000000000..0bf214b440 --- /dev/null +++ b/src/extensions/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/extensions/roles/customfield/PhabricatorRoleStandardCustomField.php b/src/extensions/roles/customfield/PhabricatorRoleStandardCustomField.php new file mode 100644 index 0000000000..c113c8a7fa --- /dev/null +++ b/src/extensions/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::normalizeRoleSlug($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::normalizeRoleSlug($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/extensions/roles/editor/PhabricatorRoleTriggerEditor.php b/src/extensions/roles/editor/PhabricatorRoleTriggerEditor.php new file mode 100644 index 0000000000..9f273a73e2 --- /dev/null +++ b/src/extensions/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 PhabricatorWorkboardInterface)) { + 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() || $board->getHasSubroles()) { + $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() || $board->getHasSubroles()) { + $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/extensions/roles/engine/PhabricatorBoardRenderingEngine.php b/src/extensions/roles/engine/PhabricatorBoardRenderingEngine.php new file mode 100644 index 0000000000..7d36a4c89c --- /dev/null +++ b/src/extensions/roles/engine/PhabricatorBoardRenderingEngine.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/extensions/roles/engine/PhabricatorBoardResponseEngine.php b/src/extensions/roles/engine/PhabricatorBoardResponseEngine.php new file mode 100644 index 0000000000..3ad6bd1c07 --- /dev/null +++ b/src/extensions/roles/engine/PhabricatorBoardResponseEngine.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 PhabricatorBoardLayoutEngine()) + ->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 PhabricatorBoardRenderingEngine()) + ->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/extensions/roles/engine/PhabricatorRoleEditEngine.php b/src/extensions/roles/engine/PhabricatorRoleEditEngine.php new file mode 100644 index 0000000000..aef1f7e802 --- /dev/null +++ b/src/extensions/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/extensions/roles/engine/PhabricatorRoleProfileMenuEngine.php b/src/extensions/roles/engine/PhabricatorRoleProfileMenuEngine.php new file mode 100644 index 0000000000..c4f2ace998 --- /dev/null +++ b/src/extensions/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/extensions/roles/engineextension/PhabricatorBoardColumnsSearchEngineAttachment.php b/src/extensions/roles/engineextension/PhabricatorBoardColumnsSearchEngineAttachment.php new file mode 100644 index 0000000000..0cd92271b3 --- /dev/null +++ b/src/extensions/roles/engineextension/PhabricatorBoardColumnsSearchEngineAttachment.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 PhabricatorBoardLayoutEngine()) + ->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/extensions/roles/engineextension/PhabricatorRoleHovercardEngineExtension.php b/src/extensions/roles/engineextension/PhabricatorRoleHovercardEngineExtension.php new file mode 100644 index 0000000000..516624605c --- /dev/null +++ b/src/extensions/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/extensions/roles/engineextension/PhabricatorRoleTriggerUsageIndexEngineExtension.php b/src/extensions/roles/engineextension/PhabricatorRoleTriggerUsageIndexEngineExtension.php new file mode 100644 index 0000000000..303ffb8ee0 --- /dev/null +++ b/src/extensions/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/extensions/roles/engineextension/PhabricatorRolesAncestorsSearchEngineAttachment.php b/src/extensions/roles/engineextension/PhabricatorRolesAncestorsSearchEngineAttachment.php new file mode 100644 index 0000000000..d43643e7f9 --- /dev/null +++ b/src/extensions/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/extensions/roles/engineextension/PhabricatorRolesCurtainExtension.php b/src/extensions/roles/engineextension/PhabricatorRolesCurtainExtension.php new file mode 100644 index 0000000000..7ffa99e0d0 --- /dev/null +++ b/src/extensions/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 PhabricatorBoardLayoutEngine()) + ->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/extensions/roles/engineextension/PhabricatorRolesEditEngineExtension.php b/src/extensions/roles/engineextension/PhabricatorRolesEditEngineExtension.php new file mode 100644 index 0000000000..b76dc06190 --- /dev/null +++ b/src/extensions/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/extensions/roles/engineextension/PhabricatorRolesFulltextEngineExtension.php b/src/extensions/roles/engineextension/PhabricatorRolesFulltextEngineExtension.php new file mode 100644 index 0000000000..c7a46c76f0 --- /dev/null +++ b/src/extensions/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/extensions/roles/engineextension/PhabricatorRolesMailEngineExtension.php b/src/extensions/roles/engineextension/PhabricatorRolesMailEngineExtension.php new file mode 100644 index 0000000000..0f15f27009 --- /dev/null +++ b/src/extensions/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/extensions/roles/engineextension/PhabricatorRolesMembersSearchEngineAttachment.php b/src/extensions/roles/engineextension/PhabricatorRolesMembersSearchEngineAttachment.php new file mode 100644 index 0000000000..d509c98b92 --- /dev/null +++ b/src/extensions/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/extensions/roles/engineextension/PhabricatorRolesMembershipIndexEngineExtension.php b/src/extensions/roles/engineextension/PhabricatorRolesMembershipIndexEngineExtension.php new file mode 100644 index 0000000000..141d73001c --- /dev/null +++ b/src/extensions/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/extensions/roles/engineextension/PhabricatorRolesSearchEngineAttachment.php b/src/extensions/roles/engineextension/PhabricatorRolesSearchEngineAttachment.php new file mode 100644 index 0000000000..663f1f4bfd --- /dev/null +++ b/src/extensions/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/extensions/roles/engineextension/PhabricatorRolesSearchEngineExtension.php b/src/extensions/roles/engineextension/PhabricatorRolesSearchEngineExtension.php new file mode 100644 index 0000000000..96943f330a --- /dev/null +++ b/src/extensions/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/extensions/roles/engineextension/PhabricatorRolesWatchersSearchEngineAttachment.php b/src/extensions/roles/engineextension/PhabricatorRolesWatchersSearchEngineAttachment.php new file mode 100644 index 0000000000..d224523e48 --- /dev/null +++ b/src/extensions/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/extensions/roles/engineextension/RoleDatasourceEngineExtension.php b/src/extensions/roles/engineextension/RoleDatasourceEngineExtension.php new file mode 100644 index 0000000000..a4caa42fb7 --- /dev/null +++ b/src/extensions/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/extensions/roles/events/PhabricatorRoleUIEventListener.php b/src/extensions/roles/events/PhabricatorRoleUIEventListener.php new file mode 100644 index 0000000000..659ebf33ce --- /dev/null +++ b/src/extensions/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 PhabricatorBoardLayoutEngine()) + ->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/extensions/roles/exception/PhabricatorRoleTriggerCorruptionException.php b/src/extensions/roles/exception/PhabricatorRoleTriggerCorruptionException.php new file mode 100644 index 0000000000..77911d81d5 --- /dev/null +++ b/src/extensions/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/extensions/roles/herald/HeraldRolesField.php b/src/extensions/roles/herald/HeraldRolesField.php new file mode 100644 index 0000000000..52243f879b --- /dev/null +++ b/src/extensions/roles/herald/HeraldRolesField.php @@ -0,0 +1,18 @@ +getPHID(), + PhabricatorRoleObjectHasRoleEdgeType::EDGECONST); + } + +} diff --git a/src/extensions/roles/herald/PhabricatorRoleAddHeraldAction.php b/src/extensions/roles/herald/PhabricatorRoleAddHeraldAction.php new file mode 100644 index 0000000000..8db6e179fe --- /dev/null +++ b/src/extensions/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/extensions/roles/herald/PhabricatorRoleHeraldAction.php b/src/extensions/roles/herald/PhabricatorRoleHeraldAction.php new file mode 100644 index 0000000000..cdb0ffee0b --- /dev/null +++ b/src/extensions/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/extensions/roles/herald/PhabricatorRoleHeraldAdapter.php b/src/extensions/roles/herald/PhabricatorRoleHeraldAdapter.php new file mode 100644 index 0000000000..aa38d0d00f --- /dev/null +++ b/src/extensions/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/extensions/roles/herald/PhabricatorRoleHeraldFieldGroup.php b/src/extensions/roles/herald/PhabricatorRoleHeraldFieldGroup.php new file mode 100644 index 0000000000..10c1754247 --- /dev/null +++ b/src/extensions/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/extensions/roles/herald/PhabricatorRoleTagsAddedField.php b/src/extensions/roles/herald/PhabricatorRoleTagsAddedField.php new file mode 100644 index 0000000000..ae9489a58a --- /dev/null +++ b/src/extensions/roles/herald/PhabricatorRoleTagsAddedField.php @@ -0,0 +1,23 @@ +getRoleTagsTransaction(); + if (!$xaction) { + return array(); + } + + $record = PhabricatorEdgeChangeRecord::newFromTransaction($xaction); + + return $record->getAddedPHIDs(); + } + +} diff --git a/src/extensions/roles/herald/PhabricatorRoleTagsField.php b/src/extensions/roles/herald/PhabricatorRoleTagsField.php new file mode 100644 index 0000000000..ffc14df4ab --- /dev/null +++ b/src/extensions/roles/herald/PhabricatorRoleTagsField.php @@ -0,0 +1,27 @@ +getAppliedEdgeTransactionOfType( + PhabricatorRoleObjectHasRoleEdgeType::EDGECONST); + } + +} diff --git a/src/extensions/roles/herald/PhabricatorRoleTagsRemovedField.php b/src/extensions/roles/herald/PhabricatorRoleTagsRemovedField.php new file mode 100644 index 0000000000..c16b5f4f96 --- /dev/null +++ b/src/extensions/roles/herald/PhabricatorRoleTagsRemovedField.php @@ -0,0 +1,23 @@ +getRoleTagsTransaction(); + if (!$xaction) { + return array(); + } + + $record = PhabricatorEdgeChangeRecord::newFromTransaction($xaction); + + return $record->getRemovedPHIDs(); + } + +} diff --git a/src/extensions/roles/icon/PhabricatorRoleDropEffect.php b/src/extensions/roles/icon/PhabricatorRoleDropEffect.php new file mode 100644 index 0000000000..d291cd8590 --- /dev/null +++ b/src/extensions/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/extensions/roles/icon/PhabricatorRoleIconSet.php b/src/extensions/roles/icon/PhabricatorRoleIconSet.php new file mode 100644 index 0000000000..3a3e3d17e4 --- /dev/null +++ b/src/extensions/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-umbrella', + 'name' => pht('Art'), + 'image' => 'v3/umbrella.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/extensions/roles/interface/PhabricatorColumnProxyInterface.php b/src/extensions/roles/interface/PhabricatorColumnProxyInterface.php new file mode 100644 index 0000000000..4e3c882d35 --- /dev/null +++ b/src/extensions/roles/interface/PhabricatorColumnProxyInterface.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/extensions/roles/lipsum/PhabricatorRoleTestDataGenerator.php b/src/extensions/roles/lipsum/PhabricatorRoleTestDataGenerator.php new file mode 100644 index 0000000000..8a6e1fd3b5 --- /dev/null +++ b/src/extensions/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/extensions/roles/mail/RoleReplyHandler.php b/src/extensions/roles/mail/RoleReplyHandler.php new file mode 100644 index 0000000000..ebeab472e1 --- /dev/null +++ b/src/extensions/roles/mail/RoleReplyHandler.php @@ -0,0 +1,21 @@ +getMenuItemProperty('name'); + + if (strlen($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/extensions/roles/menuitem/PhabricatorRoleManageProfileMenuItem.php b/src/extensions/roles/menuitem/PhabricatorRoleManageProfileMenuItem.php new file mode 100644 index 0000000000..880893878b --- /dev/null +++ b/src/extensions/roles/menuitem/PhabricatorRoleManageProfileMenuItem.php @@ -0,0 +1,73 @@ +getMenuItemProperty('name'); + + if (strlen($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/extensions/roles/menuitem/PhabricatorRoleMembersProfileMenuItem.php b/src/extensions/roles/menuitem/PhabricatorRoleMembersProfileMenuItem.php new file mode 100644 index 0000000000..2a8db6ae78 --- /dev/null +++ b/src/extensions/roles/menuitem/PhabricatorRoleMembersProfileMenuItem.php @@ -0,0 +1,63 @@ +getMenuItemProperty('name'); + + if (strlen($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/extensions/roles/menuitem/PhabricatorRolePictureProfileMenuItem.php b/src/extensions/roles/menuitem/PhabricatorRolePictureProfileMenuItem.php new file mode 100644 index 0000000000..06f320a030 --- /dev/null +++ b/src/extensions/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/extensions/roles/menuitem/PhabricatorRolePointsProfileMenuItem.php b/src/extensions/roles/menuitem/PhabricatorRolePointsProfileMenuItem.php new file mode 100644 index 0000000000..2333269d05 --- /dev/null +++ b/src/extensions/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/extensions/roles/menuitem/PhabricatorRoleReportsProfileMenuItem.php b/src/extensions/roles/menuitem/PhabricatorRoleReportsProfileMenuItem.php new file mode 100644 index 0000000000..feaa8950a6 --- /dev/null +++ b/src/extensions/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 (strlen($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/extensions/roles/menuitem/PhabricatorRoleSubrolesProfileMenuItem.php b/src/extensions/roles/menuitem/PhabricatorRoleSubrolesProfileMenuItem.php new file mode 100644 index 0000000000..d3e321b064 --- /dev/null +++ b/src/extensions/roles/menuitem/PhabricatorRoleSubrolesProfileMenuItem.php @@ -0,0 +1,70 @@ +isMilestone()) { + return false; + } + + return true; + } + + public function getDisplayName( + PhabricatorProfileMenuItemConfiguration $config) { + $name = $config->getMenuItemProperty('name'); + + if (strlen($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/extensions/roles/menuitem/PhabricatorRoleWorkboardProfileMenuItem.php b/src/extensions/roles/menuitem/PhabricatorRoleWorkboardProfileMenuItem.php new file mode 100644 index 0000000000..7ce42667b5 --- /dev/null +++ b/src/extensions/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 (strlen($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/extensions/roles/order/PhabricatorRoleColumnAuthorOrder.php b/src/extensions/roles/order/PhabricatorRoleColumnAuthorOrder.php new file mode 100644 index 0000000000..127e69996d --- /dev/null +++ b/src/extensions/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/extensions/roles/order/PhabricatorRoleColumnCreatedOrder.php b/src/extensions/roles/order/PhabricatorRoleColumnCreatedOrder.php new file mode 100644 index 0000000000..59a8d5ad46 --- /dev/null +++ b/src/extensions/roles/order/PhabricatorRoleColumnCreatedOrder.php @@ -0,0 +1,35 @@ +getDateCreated(), + -1 * (int)$object->getID(), + ); + } + +} diff --git a/src/extensions/roles/order/PhabricatorRoleColumnHeader.php b/src/extensions/roles/order/PhabricatorRoleColumnHeader.php new file mode 100644 index 0000000000..9af4c7e7aa --- /dev/null +++ b/src/extensions/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/extensions/roles/order/PhabricatorRoleColumnNaturalOrder.php b/src/extensions/roles/order/PhabricatorRoleColumnNaturalOrder.php new file mode 100644 index 0000000000..05556cf3bf --- /dev/null +++ b/src/extensions/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/extensions/roles/order/PhabricatorRoleColumnOwnerOrder.php b/src/extensions/roles/order/PhabricatorRoleColumnOwnerOrder.php new file mode 100644 index 0000000000..abc72b4ebb --- /dev/null +++ b/src/extensions/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/extensions/roles/order/PhabricatorRoleColumnPointsOrder.php b/src/extensions/roles/order/PhabricatorRoleColumnPointsOrder.php new file mode 100644 index 0000000000..61debfd3d5 --- /dev/null +++ b/src/extensions/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/extensions/roles/order/PhabricatorRoleColumnPriorityOrder.php b/src/extensions/roles/order/PhabricatorRoleColumnPriorityOrder.php new file mode 100644 index 0000000000..632fbfc0cd --- /dev/null +++ b/src/extensions/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/extensions/roles/order/PhabricatorRoleColumnStatusOrder.php b/src/extensions/roles/order/PhabricatorRoleColumnStatusOrder.php new file mode 100644 index 0000000000..1a8c19a31c --- /dev/null +++ b/src/extensions/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/extensions/roles/order/PhabricatorRoleColumnTitleOrder.php b/src/extensions/roles/order/PhabricatorRoleColumnTitleOrder.php new file mode 100644 index 0000000000..1bbceea5a4 --- /dev/null +++ b/src/extensions/roles/order/PhabricatorRoleColumnTitleOrder.php @@ -0,0 +1,34 @@ +getTitle(), + ); + } + +} diff --git a/src/extensions/roles/phid/PhabricatorRoleColumnPHIDType.php b/src/extensions/roles/phid/PhabricatorRoleColumnPHIDType.php new file mode 100644 index 0000000000..a9956bab01 --- /dev/null +++ b/src/extensions/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/extensions/roles/phid/PhabricatorRoleRolePHIDType.php b/src/extensions/roles/phid/PhabricatorRoleRolePHIDType.php new file mode 100644 index 0000000000..4d514f5742 --- /dev/null +++ b/src/extensions/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 (strlen($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/extensions/roles/phid/PhabricatorRoleTriggerPHIDType.php b/src/extensions/roles/phid/PhabricatorRoleTriggerPHIDType.php new file mode 100644 index 0000000000..767d0c1373 --- /dev/null +++ b/src/extensions/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/extensions/roles/policyrule/PhabricatorRoleMembersPolicyRule.php b/src/extensions/roles/policyrule/PhabricatorRoleMembersPolicyRule.php new file mode 100644 index 0000000000..5ac39e8f3a --- /dev/null +++ b/src/extensions/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/extensions/roles/policyrule/PhabricatorRolesAllPolicyRule.php b/src/extensions/roles/policyrule/PhabricatorRolesAllPolicyRule.php new file mode 100644 index 0000000000..25c9b1c5b6 --- /dev/null +++ b/src/extensions/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/extensions/roles/policyrule/PhabricatorRolesBasePolicyRule.php b/src/extensions/roles/policyrule/PhabricatorRolesBasePolicyRule.php new file mode 100644 index 0000000000..13add00d85 --- /dev/null +++ b/src/extensions/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/extensions/roles/policyrule/PhabricatorRolesPolicyRule.php b/src/extensions/roles/policyrule/PhabricatorRolesPolicyRule.php new file mode 100644 index 0000000000..b34d5a6727 --- /dev/null +++ b/src/extensions/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/extensions/roles/query/PhabricatorRoleColumnPositionQuery.php b/src/extensions/roles/query/PhabricatorRoleColumnPositionQuery.php new file mode 100644 index 0000000000..3c0584d7da --- /dev/null +++ b/src/extensions/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/extensions/roles/query/PhabricatorRoleColumnQuery.php b/src/extensions/roles/query/PhabricatorRoleColumnQuery.php new file mode 100644 index 0000000000..007d40ab81 --- /dev/null +++ b/src/extensions/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 PhabricatorColumnProxyInterface)) { + $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/extensions/roles/query/PhabricatorRoleColumnSearchEngine.php b/src/extensions/roles/query/PhabricatorRoleColumnSearchEngine.php new file mode 100644 index 0000000000..48fc63dd87 --- /dev/null +++ b/src/extensions/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/extensions/roles/query/PhabricatorRoleColumnTransactionQuery.php b/src/extensions/roles/query/PhabricatorRoleColumnTransactionQuery.php new file mode 100644 index 0000000000..5e22685172 --- /dev/null +++ b/src/extensions/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/extensions/roles/query/PhabricatorRoleSearchEngine.php b/src/extensions/roles/query/PhabricatorRoleSearchEngine.php new file mode 100644 index 0000000000..0e16aacde7 --- /dev/null +++ b/src/extensions/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 (strlen($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/extensions/roles/query/PhabricatorRoleTransactionQuery.php b/src/extensions/roles/query/PhabricatorRoleTransactionQuery.php new file mode 100644 index 0000000000..49335eb6ee --- /dev/null +++ b/src/extensions/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/extensions/roles/query/PhabricatorRoleTriggerSearchEngine.php b/src/extensions/roles/query/PhabricatorRoleTriggerSearchEngine.php new file mode 100644 index 0000000000..0cc009f3eb --- /dev/null +++ b/src/extensions/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/extensions/roles/query/PhabricatorRoleTriggerTransactionQuery.php b/src/extensions/roles/query/PhabricatorRoleTriggerTransactionQuery.php new file mode 100644 index 0000000000..8465f38c57 --- /dev/null +++ b/src/extensions/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/extensions/roles/remarkup/__tests__/RoleRemarkupRuleTestCase.php b/src/extensions/roles/remarkup/__tests__/RoleRemarkupRuleTestCase.php new file mode 100644 index 0000000000..346805b714 --- /dev/null +++ b/src/extensions/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/extensions/roles/search/PhabricatorRoleFerretEngine.php b/src/extensions/roles/search/PhabricatorRoleFerretEngine.php new file mode 100644 index 0000000000..11daadc37c --- /dev/null +++ b/src/extensions/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/extensions/roles/searchfield/PhabricatorRoleSearchField.php b/src/extensions/roles/searchfield/PhabricatorRoleSearchField.php new file mode 100644 index 0000000000..96030b9b36 --- /dev/null +++ b/src/extensions/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/extensions/roles/state/PhabricatorWorkboardViewState.php b/src/extensions/roles/state/PhabricatorWorkboardViewState.php new file mode 100644 index 0000000000..b40fbb7b49 --- /dev/null +++ b/src/extensions/roles/state/PhabricatorWorkboardViewState.php @@ -0,0 +1,291 @@ +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'); + } + + if (strlen($request->getURIData('queryKey'))) { + $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 (strlen($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 (strlen($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 PhabricatorBoardLayoutEngine()) + ->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/extensions/roles/storage/PhabricatorRole.php b/src/extensions/roles/storage/PhabricatorRole.php new file mode 100644 index 0000000000..afabf50500 --- /dev/null +++ b/src/extensions/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 (!strlen($this->getPHID())) { + $this->setPHID($this->generatePHID()); + } + + if (!strlen($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(), + ); + } + + +/* -( PhabricatorColumnProxyInterface )------------------------------------ */ + + + public function getProxyColumnName() { + 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/extensions/roles/storage/PhabricatorRoleColumn.php b/src/extensions/roles/storage/PhabricatorRoleColumn.php new file mode 100644 index 0000000000..cbd17d13d1 --- /dev/null +++ b/src/extensions/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 (strlen($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/extensions/roles/storage/PhabricatorRoleColumnPosition.php b/src/extensions/roles/storage/PhabricatorRoleColumnPosition.php new file mode 100644 index 0000000000..6605b53892 --- /dev/null +++ b/src/extensions/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/extensions/roles/storage/PhabricatorRoleColumnTransaction.php b/src/extensions/roles/storage/PhabricatorRoleColumnTransaction.php new file mode 100644 index 0000000000..1c791a17e9 --- /dev/null +++ b/src/extensions/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/extensions/roles/storage/PhabricatorRoleSlug.php b/src/extensions/roles/storage/PhabricatorRoleSlug.php new file mode 100644 index 0000000000..3c097275a1 --- /dev/null +++ b/src/extensions/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/extensions/roles/storage/PhabricatorRoleTransaction.php b/src/extensions/roles/storage/PhabricatorRoleTransaction.php new file mode 100644 index 0000000000..b5e69a9e16 --- /dev/null +++ b/src/extensions/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/extensions/roles/storage/PhabricatorRoleTrigger.php b/src/extensions/roles/storage/PhabricatorRoleTrigger.php new file mode 100644 index 0000000000..d8a983eb1a --- /dev/null +++ b/src/extensions/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 (strlen($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/extensions/roles/storage/PhabricatorRoleTriggerTransaction.php b/src/extensions/roles/storage/PhabricatorRoleTriggerTransaction.php new file mode 100644 index 0000000000..9bfaafc15e --- /dev/null +++ b/src/extensions/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/extensions/roles/trigger/PhabricatorRoleTriggerAddRolesRule.php b/src/extensions/roles/trigger/PhabricatorRoleTriggerAddRolesRule.php new file mode 100644 index 0000000000..69aeb599c4 --- /dev/null +++ b/src/extensions/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/extensions/roles/trigger/PhabricatorRoleTriggerInvalidRule.php b/src/extensions/roles/trigger/PhabricatorRoleTriggerInvalidRule.php new file mode 100644 index 0000000000..0acccf96ba --- /dev/null +++ b/src/extensions/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/extensions/roles/trigger/PhabricatorRoleTriggerManiphestOwnerRule.php b/src/extensions/roles/trigger/PhabricatorRoleTriggerManiphestOwnerRule.php new file mode 100644 index 0000000000..a111db2d14 --- /dev/null +++ b/src/extensions/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/extensions/roles/trigger/PhabricatorRoleTriggerManiphestPriorityRule.php b/src/extensions/roles/trigger/PhabricatorRoleTriggerManiphestPriorityRule.php new file mode 100644 index 0000000000..750fc7bee2 --- /dev/null +++ b/src/extensions/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/extensions/roles/trigger/PhabricatorRoleTriggerManiphestStatusRule.php b/src/extensions/roles/trigger/PhabricatorRoleTriggerManiphestStatusRule.php new file mode 100644 index 0000000000..7d870fbc39 --- /dev/null +++ b/src/extensions/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/extensions/roles/trigger/PhabricatorRoleTriggerPlaySoundRule.php b/src/extensions/roles/trigger/PhabricatorRoleTriggerPlaySoundRule.php new file mode 100644 index 0000000000..6a30b80578 --- /dev/null +++ b/src/extensions/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/extensions/roles/trigger/PhabricatorRoleTriggerRemoveProjectsRule.php b/src/extensions/roles/trigger/PhabricatorRoleTriggerRemoveProjectsRule.php new file mode 100644 index 0000000000..06d92324c4 --- /dev/null +++ b/src/extensions/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/extensions/roles/trigger/PhabricatorRoleTriggerRule.php b/src/extensions/roles/trigger/PhabricatorRoleTriggerRule.php new file mode 100644 index 0000000000..f466c49dda --- /dev/null +++ b/src/extensions/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/extensions/roles/trigger/PhabricatorRoleTriggerRuleRecord.php b/src/extensions/roles/trigger/PhabricatorRoleTriggerRuleRecord.php new file mode 100644 index 0000000000..9b27add0f7 --- /dev/null +++ b/src/extensions/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/extensions/roles/trigger/PhabricatorRoleTriggerUnknownRule.php b/src/extensions/roles/trigger/PhabricatorRoleTriggerUnknownRule.php new file mode 100644 index 0000000000..1056b4e827 --- /dev/null +++ b/src/extensions/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/extensions/roles/typeahead/PhabricatorRoleDatasource.php b/src/extensions/roles/typeahead/PhabricatorRoleDatasource.php new file mode 100644 index 0000000000..53c82593d8 --- /dev/null +++ b/src/extensions/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 (!strlen($slug)) { + foreach ($role->getSlugs() as $slug_object) { + $slug = $slug_object->getSlug(); + if (strlen($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 (strlen($slug)) { + $role_result->setAutocomplete('#'.$slug); + } + + $role_result->setImageURI($role->getProfileImageURI()); + + if ($is_browse) { + $role_result->addAttribute($role->getDisplayIconName()); + + $description = idx($descriptions, $phid); + if (strlen($description)) { + $summary = PhabricatorMarkupEngine::summarizeSentence($description); + $role_result->addAttribute($summary); + } + } + + $results[] = $role_result; + } + + return $results; + } + +} diff --git a/src/extensions/roles/typeahead/PhabricatorRoleLogicalAncestorDatasource.php b/src/extensions/roles/typeahead/PhabricatorRoleLogicalAncestorDatasource.php new file mode 100644 index 0000000000..f60e386770 --- /dev/null +++ b/src/extensions/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/extensions/roles/typeahead/PhabricatorRoleLogicalDatasource.php b/src/extensions/roles/typeahead/PhabricatorRoleLogicalDatasource.php new file mode 100644 index 0000000000..0059801f4e --- /dev/null +++ b/src/extensions/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/extensions/roles/typeahead/PhabricatorRoleLogicalOrNotDatasource.php b/src/extensions/roles/typeahead/PhabricatorRoleLogicalOrNotDatasource.php new file mode 100644 index 0000000000..bcf64ad7c0 --- /dev/null +++ b/src/extensions/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/extensions/roles/typeahead/PhabricatorRoleLogicalUserDatasource.php b/src/extensions/roles/typeahead/PhabricatorRoleLogicalUserDatasource.php new file mode 100644 index 0000000000..beede77552 --- /dev/null +++ b/src/extensions/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/extensions/roles/typeahead/PhabricatorRoleLogicalViewerDatasource.php b/src/extensions/roles/typeahead/PhabricatorRoleLogicalViewerDatasource.php new file mode 100644 index 0000000000..ff050f7e15 --- /dev/null +++ b/src/extensions/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/extensions/roles/typeahead/PhabricatorRoleMembersDatasource.php b/src/extensions/roles/typeahead/PhabricatorRoleMembersDatasource.php new file mode 100644 index 0000000000..73b11e52a3 --- /dev/null +++ b/src/extensions/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/extensions/roles/typeahead/PhabricatorRoleNoRolesDatasource.php b/src/extensions/roles/typeahead/PhabricatorRoleNoRolesDatasource.php new file mode 100644 index 0000000000..7f27efca3f --- /dev/null +++ b/src/extensions/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/extensions/roles/typeahead/PhabricatorRoleOrUserDatasource.php b/src/extensions/roles/typeahead/PhabricatorRoleOrUserDatasource.php new file mode 100644 index 0000000000..687b3d30c1 --- /dev/null +++ b/src/extensions/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/extensions/roles/typeahead/PhabricatorRoleUserFunctionDatasource.php b/src/extensions/roles/typeahead/PhabricatorRoleUserFunctionDatasource.php new file mode 100644 index 0000000000..a613daa5d5 --- /dev/null +++ b/src/extensions/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/extensions/roles/view/PhabricatorRoleCardView.php b/src/extensions/roles/view/PhabricatorRoleCardView.php new file mode 100644 index 0000000000..ae86e998de --- /dev/null +++ b/src/extensions/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[] = 'role-card-view'; + + $color = $this->role->getColor(); + $classes[] = 'role-card-'.$color; + + return array( + 'class' => implode(' ', $classes), + ); + } + + protected function getTagContent() { + + $role = $this->role; + $viewer = $this->viewer; + require_celerity_resource('role-card-view-css'); + + $icon = $role->getDisplayIconIcon(); + $icon_name = $role->getDisplayIconName(); + $tag = id(new PHUITagView()) + ->setIcon($icon) + ->setName($icon_name) + ->addClass('role-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' => 'role-card-inner', + ), + array( + $header, + $description, + )); + + return $card; + } + +} diff --git a/src/extensions/roles/view/PhabricatorRoleListView.php b/src/extensions/roles/view/PhabricatorRoleListView.php new file mode 100644 index 0000000000..8e20cbfbb8 --- /dev/null +++ b/src/extensions/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/extensions/roles/view/PhabricatorRoleMemberListView.php b/src/extensions/roles/view/PhabricatorRoleMemberListView.php new file mode 100644 index 0000000000..f5fc2ee3a4 --- /dev/null +++ b/src/extensions/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/extensions/roles/view/PhabricatorRoleUserListView.php b/src/extensions/roles/view/PhabricatorRoleUserListView.php new file mode 100644 index 0000000000..907dc2f64e --- /dev/null +++ b/src/extensions/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/extensions/roles/view/PhabricatorRoleWatcherListView.php b/src/extensions/roles/view/PhabricatorRoleWatcherListView.php new file mode 100644 index 0000000000..a812c3fde3 --- /dev/null +++ b/src/extensions/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/extensions/roles/view/ProjectBoardTaskCard.php b/src/extensions/roles/view/ProjectBoardTaskCard.php new file mode 100644 index 0000000000..c1be9b2b57 --- /dev/null +++ b/src/extensions/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-role-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/extensions/roles/xaction/PhabricatorRoleColorTransaction.php b/src/extensions/roles/xaction/PhabricatorRoleColorTransaction.php new file mode 100644 index 0000000000..f8e6a318a0 --- /dev/null +++ b/src/extensions/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/extensions/roles/xaction/PhabricatorRoleFilterTransaction.php b/src/extensions/roles/xaction/PhabricatorRoleFilterTransaction.php new file mode 100644 index 0000000000..a5ebc78da5 --- /dev/null +++ b/src/extensions/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/extensions/roles/xaction/PhabricatorRoleIconTransaction.php b/src/extensions/roles/xaction/PhabricatorRoleIconTransaction.php new file mode 100644 index 0000000000..9f39a68f77 --- /dev/null +++ b/src/extensions/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/extensions/roles/xaction/PhabricatorRoleImageTransaction.php b/src/extensions/roles/xaction/PhabricatorRoleImageTransaction.php new file mode 100644 index 0000000000..b449ba5083 --- /dev/null +++ b/src/extensions/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/extensions/roles/xaction/PhabricatorRoleLockTransaction.php b/src/extensions/roles/xaction/PhabricatorRoleLockTransaction.php new file mode 100644 index 0000000000..eab4e90887 --- /dev/null +++ b/src/extensions/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/extensions/roles/xaction/PhabricatorRoleMilestoneTransaction.php b/src/extensions/roles/xaction/PhabricatorRoleMilestoneTransaction.php new file mode 100644 index 0000000000..dd8fd0e1b0 --- /dev/null +++ b/src/extensions/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/extensions/roles/xaction/PhabricatorRoleNameTransaction.php b/src/extensions/roles/xaction/PhabricatorRoleNameTransaction.php new file mode 100644 index 0000000000..fc1d33bb05 --- /dev/null +++ b/src/extensions/roles/xaction/PhabricatorRoleNameTransaction.php @@ -0,0 +1,112 @@ +getName(); + } + + public function applyInternalEffects($object, $value) { + $object->setName($value); + if (!$this->getEditor()->getIsMilestone()) { + $object->setPrimarySlug(/*PhabricatorSlug::normalizeRoleSlug*/($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::isValidRoleSlug($name)) { + // $errors[] = $this->newInvalidError( + // pht('Role names must contain at least one letter or number.')); + //} + + $slug = /*PhabricatorSlug::normalizeRoleSlug*/($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/extensions/roles/xaction/PhabricatorRoleParentTransaction.php b/src/extensions/roles/xaction/PhabricatorRoleParentTransaction.php new file mode 100644 index 0000000000..f0dcbdd009 --- /dev/null +++ b/src/extensions/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/extensions/roles/xaction/PhabricatorRoleSlugsTransaction.php b/src/extensions/roles/xaction/PhabricatorRoleSlugsTransaction.php new file mode 100644 index 0000000000..7531d49d9a --- /dev/null +++ b/src/extensions/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/extensions/roles/xaction/PhabricatorRoleSortTransaction.php b/src/extensions/roles/xaction/PhabricatorRoleSortTransaction.php new file mode 100644 index 0000000000..46117278a7 --- /dev/null +++ b/src/extensions/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/extensions/roles/xaction/PhabricatorRoleStatusTransaction.php b/src/extensions/roles/xaction/PhabricatorRoleStatusTransaction.php new file mode 100644 index 0000000000..4c4598a8e4 --- /dev/null +++ b/src/extensions/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/extensions/roles/xaction/PhabricatorRoleTransactionType.php b/src/extensions/roles/xaction/PhabricatorRoleTransactionType.php new file mode 100644 index 0000000000..79d0b7adb7 --- /dev/null +++ b/src/extensions/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/extensions/roles/xaction/PhabricatorRoleWorkboardBackgroundTransaction.php b/src/extensions/roles/xaction/PhabricatorRoleWorkboardBackgroundTransaction.php new file mode 100644 index 0000000000..de3f69a649 --- /dev/null +++ b/src/extensions/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/extensions/roles/xaction/PhabricatorRoleWorkboardTransaction.php b/src/extensions/roles/xaction/PhabricatorRoleWorkboardTransaction.php new file mode 100644 index 0000000000..29d055d84c --- /dev/null +++ b/src/extensions/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/extensions/roles/xaction/column/PhabricatorRoleColumnLimitTransaction.php b/src/extensions/roles/xaction/column/PhabricatorRoleColumnLimitTransaction.php new file mode 100644 index 0000000000..6f6082031d --- /dev/null +++ b/src/extensions/roles/xaction/column/PhabricatorRoleColumnLimitTransaction.php @@ -0,0 +1,63 @@ +getPointLimit(); + } + + public function generateNewValue($object, $value) { + if (strlen($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/extensions/roles/xaction/column/PhabricatorRoleColumnNameTransaction.php b/src/extensions/roles/xaction/column/PhabricatorRoleColumnNameTransaction.php new file mode 100644 index 0000000000..481d18a618 --- /dev/null +++ b/src/extensions/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 (!strlen($old)) { + return pht( + '%s named this column %s.', + $this->renderAuthor(), + $this->renderNewValue()); + } else if (strlen($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/extensions/roles/xaction/column/PhabricatorRoleColumnStatusTransaction.php b/src/extensions/roles/xaction/column/PhabricatorRoleColumnStatusTransaction.php new file mode 100644 index 0000000000..5d41b5a88d --- /dev/null +++ b/src/extensions/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/extensions/roles/xaction/column/PhabricatorRoleColumnTransactionType.php b/src/extensions/roles/xaction/column/PhabricatorRoleColumnTransactionType.php new file mode 100644 index 0000000000..e0b5bb3203 --- /dev/null +++ b/src/extensions/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/extensions/roles/xaction/trigger/PhabricatorRoleTriggerNameTransaction.php b/src/extensions/roles/xaction/trigger/PhabricatorRoleTriggerNameTransaction.php new file mode 100644 index 0000000000..10139caae0 --- /dev/null +++ b/src/extensions/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/extensions/roles/xaction/trigger/PhabricatorRoleTriggerRulesetTransaction.php b/src/extensions/roles/xaction/trigger/PhabricatorRoleTriggerRulesetTransaction.php new file mode 100644 index 0000000000..dbf39c7a8a --- /dev/null +++ b/src/extensions/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/extensions/roles/xaction/trigger/PhabricatorRoleTriggerTransactionType.php b/src/extensions/roles/xaction/trigger/PhabricatorRoleTriggerTransactionType.php new file mode 100644 index 0000000000..2d74ac842d --- /dev/null +++ b/src/extensions/roles/xaction/trigger/PhabricatorRoleTriggerTransactionType.php @@ -0,0 +1,4 @@ + v_vec[ii]) { + return 1; + } + + if (u_vec[ii] < v_vec[ii]) { + return -1; + } + } + + return 0; + }, + + start: function() { + this._setupDragHandlers(); + + // TODO: This is temporary code to make it easier to debug this workflow + // by pressing the "R" key. + var on_reload = JX.bind(this, this._reloadCards); + new JX.KeyboardShortcut('R', 'Reload Card State (Prototype)') + .setHandler(on_reload) + .register(); + + var board_phid = this.getPHID(); + + JX.Stratcom.listen('aphlict-server-message', null, function(e) { + var message = e.getData(); + + if (message.type != 'workboards') { + return; + } + + // Check if this update notification is about the currently visible + // board. If it is, update the board state. + + var found_board = false; + for (var ii = 0; ii < message.subscribers.length; ii++) { + var subscriber_phid = message.subscribers[ii]; + if (subscriber_phid === board_phid) { + found_board = true; + break; + } + } + + if (found_board) { + on_reload(); + } + }); + + JX.Stratcom.listen('aphlict-reconnect', null, function(e) { + on_reload(); + }); + + for (var k in this._columns) { + this._columns[k].redraw(); + } + }, + + _buildColumns: function() { + var nodes = JX.DOM.scry(this.getRoot(), 'ul', 'role-column'); + + this._columns = {}; + for (var ii = 0; ii < nodes.length; ii++) { + var node = nodes[ii]; + var data = JX.Stratcom.getData(node); + var phid = data.columnPHID; + + this._columns[phid] = new JX.WorkboardColumn(this, phid, node); + } + + var on_over = JX.bind(this, this._showTriggerPreview); + var on_out = JX.bind(this, this._hideTriggerPreview); + JX.Stratcom.listen('mouseover', 'trigger-preview', on_over); + JX.Stratcom.listen('mouseout', 'trigger-preview', on_out); + + var on_move = JX.bind(this, this._dimPreview); + JX.Stratcom.listen('mousemove', null, on_move); + }, + + _dimPreview: function(e) { + var p = this._previewPositionVector; + if (!p) { + return; + } + + // When the mouse cursor gets near the drop preview element, fade it + // out so you can see through it. We can't do this with ":hover" because + // we disable cursor events. + + var cursor = JX.$V(e); + var margin = 64; + + var near_x = (cursor.x > (p.x - margin)); + var near_y = (cursor.y > (p.y - margin)); + var should_dim = (near_x && near_y); + + this._setPreviewDimState(should_dim); + }, + + _setPreviewDimState: function(is_dim) { + if (is_dim === this._previewDimState) { + return; + } + + this._previewDimState = is_dim; + var node = this._getDropPreviewNode(); + JX.DOM.alterClass(node, 'workboard-drop-preview-fade', is_dim); + }, + + _showTriggerPreview: function(e) { + if (this._disablePreview) { + return; + } + + var target = e.getTarget(); + var node = e.getNode('trigger-preview'); + + if (target !== node) { + return; + } + + var phid = JX.Stratcom.getData(node).columnPHID; + var column = this._columns[phid]; + + // Bail out if we don't know anything about this column. + if (!column) { + return; + } + + if (phid === this._previewPHID) { + return; + } + + this._previewPHID = phid; + + var effects = column.getDropEffects(); + + var triggers = []; + for (var ii = 0; ii < effects.length; ii++) { + if (effects[ii].getIsTriggerEffect()) { + triggers.push(effects[ii]); + } + } + + if (triggers.length) { + var header = column.getTriggerPreviewEffect(); + triggers = [header].concat(triggers); + } + + this._showEffects(triggers); + }, + + _hideTriggerPreview: function(e) { + if (this._disablePreview) { + return; + } + + var target = e.getTarget(); + + if (target !== e.getNode('trigger-preview')) { + return; + } + + this._removeTriggerPreview(); + }, + + _removeTriggerPreview: function() { + this._showEffects([]); + this._previewPHID = null; + }, + + _beginDrag: function() { + this._disablePreview = true; + this._showEffects([]); + }, + + _endDrag: function() { + this._disablePreview = false; + }, + + _setupDragHandlers: function() { + var columns = this.getColumns(); + + var order_template = this.getOrderTemplate(this.getOrder()); + var has_headers = order_template.getHasHeaders(); + var can_reorder = order_template.getCanReorder(); + + var lists = []; + for (var k in columns) { + var column = columns[k]; + + var list = new JX.DraggableList('draggable-card', column.getRoot()) + .setOuterContainer(this.getRoot()) + .setFindItemsHandler(JX.bind(column, column.getDropTargetNodes)) + .setCanDragX(true) + .setHasInfiniteHeight(true) + .setIsDropTargetHandler(JX.bind(column, column.setIsDropTarget)); + + var default_handler = list.getGhostHandler(); + list.setGhostHandler( + JX.bind(column, column.handleDragGhost, default_handler)); + + // The "compare handler" locks cards into a specific position in the + // column. + list.setCompareHandler(JX.bind(column, column.compareHandler)); + + // If the view has group headers, we lock cards into the right position + // when moving them between columns, but not within a column. + if (has_headers) { + list.setCompareOnMove(true); + } + + // If we can't reorder cards, we always lock them into their current + // position. + if (!can_reorder) { + list.setCompareOnMove(true); + list.setCompareOnReorder(true); + } + + list.setTargetChangeHandler(JX.bind(this, this._didChangeDropTarget)); + + list.listen('didDrop', JX.bind(this, this._onmovecard, list)); + + list.listen('didBeginDrag', JX.bind(this, this._beginDrag)); + list.listen('didEndDrag', JX.bind(this, this._endDrag)); + + lists.push(list); + } + + for (var ii = 0; ii < lists.length; ii++) { + lists[ii].setGroup(lists); + } + }, + + _didChangeDropTarget: function(src_list, src_node, dst_list, dst_node) { + if (!dst_list) { + // The card is being dragged into a dead area, like the left menu. + this._showEffects([]); + return; + } + + if (dst_node === false) { + // The card is being dragged over itself, so dropping it won't + // affect anything. + this._showEffects([]); + return; + } + + var src_phid = JX.Stratcom.getData(src_list.getRootNode()).columnPHID; + var dst_phid = JX.Stratcom.getData(dst_list.getRootNode()).columnPHID; + + var src_column = this.getColumn(src_phid); + var dst_column = this.getColumn(dst_phid); + + var effects = []; + if (src_column !== dst_column) { + effects = effects.concat(dst_column.getDropEffects()); + } + + var context = this._getDropContext(dst_node); + if (context.headerKey) { + var header = this.getHeaderTemplate(context.headerKey); + effects = effects.concat(header.getDropEffects()); + } + + var card_phid = JX.Stratcom.getData(src_node).objectPHID; + var card = src_column.getCard(card_phid); + + var visible = []; + for (var ii = 0; ii < effects.length; ii++) { + if (effects[ii].isEffectVisibleForCard(card)) { + visible.push(effects[ii]); + } + } + effects = visible; + + this._showEffects(effects); + }, + + _showEffects: function(effects) { + var node = this._getDropPreviewNode(); + + if (!effects.length) { + JX.DOM.remove(node); + this._previewPositionVector = null; + return; + } + + var items = []; + for (var ii = 0; ii < effects.length; ii++) { + var effect = effects[ii]; + items.push(effect.newNode()); + } + + JX.DOM.setContent(this._getDropPreviewListNode(), items); + document.body.appendChild(node); + + // Undim the drop preview element if it was previously dimmed. + this._setPreviewDimState(false); + this._previewPositionVector = JX.$V(node); + }, + + _getDropPreviewNode: function() { + if (!this._dropPreviewNode) { + var attributes = { + className: 'workboard-drop-preview' + }; + + var content = [ + this._getDropPreviewListNode() + ]; + + this._dropPreviewNode = JX.$N('div', attributes, content); + } + + return this._dropPreviewNode; + }, + + _getDropPreviewListNode: function() { + if (!this._dropPreviewListNode) { + var attributes = {}; + this._dropPreviewListNode = JX.$N('ul', attributes); + } + + return this._dropPreviewListNode; + }, + + _findCardsInColumn: function(column_node) { + return JX.DOM.scry(column_node, 'li', 'role-card'); + }, + + _getDropContext: function(after_node, item) { + var header_key; + var after_phids = []; + var before_phids = []; + + // We're going to send an "afterPHID" and a "beforePHID" if the card + // was dropped immediately adjacent to another card. If a card was + // dropped before or after a header, we don't send a PHID for the card + // on the other side of the header. + + // If the view has headers, we always send the header the card was + // dropped under. + + var after_data; + var after_card = after_node; + while (after_card) { + after_data = JX.Stratcom.getData(after_card); + + if (after_data.headerKey) { + break; + } + + if (after_data.objectPHID) { + after_phids.push(after_data.objectPHID); + } + + after_card = after_card.previousSibling; + } + + if (item) { + var before_data; + var before_card = item.nextSibling; + while (before_card) { + before_data = JX.Stratcom.getData(before_card); + + if (before_data.headerKey) { + break; + } + + if (before_data.objectPHID) { + before_phids.push(before_data.objectPHID); + } + + before_card = before_card.nextSibling; + } + } + + var header_data; + var header_node = after_node; + while (header_node) { + header_data = JX.Stratcom.getData(header_node); + if (header_data.headerKey) { + break; + } + header_node = header_node.previousSibling; + } + + if (header_data) { + header_key = header_data.headerKey; + } + + return { + headerKey: header_key, + afterPHIDs: after_phids, + beforePHIDs: before_phids + }; + }, + + _onmovecard: function(list, item, after_node, src_list) { + list.lock(); + JX.DOM.alterClass(item, 'drag-sending', true); + + var src_phid = JX.Stratcom.getData(src_list.getRootNode()).columnPHID; + var dst_phid = JX.Stratcom.getData(list.getRootNode()).columnPHID; + + var item_phid = JX.Stratcom.getData(item).objectPHID; + var data = { + objectPHID: item_phid, + columnPHID: dst_phid, + order: this.getOrder() + }; + + var context = this._getDropContext(after_node, item); + data.afterPHIDs = context.afterPHIDs.join(','); + data.beforePHIDs = context.beforePHIDs.join(','); + + if (context.headerKey) { + var properties = this.getHeaderTemplate(context.headerKey) + .getEditProperties(); + data.header = JX.JSON.stringify(properties); + } + + var visible_phids = []; + var column = this.getColumn(dst_phid); + for (var object_phid in column.getCards()) { + visible_phids.push(object_phid); + } + + data.visiblePHIDs = visible_phids.join(','); + + // If the user cancels the workflow (for example, by hitting an MFA + // prompt that they click "Cancel" on), put the card back where it was + // and reset the UI state. + var on_revert = JX.bind( + this, + this._revertCard, + list, + item, + src_phid, + dst_phid); + + var after_phid = null; + if (data.afterPHIDs.length) { + after_phid = data.afterPHIDs[0]; + } + + var onupdate = JX.bind( + this, + this._oncardupdate, + list, + src_phid, + dst_phid, + after_phid); + + new JX.Workflow(this.getController().getMoveURI(), data) + .setHandler(onupdate) + .setCloseHandler(on_revert) + .start(); + }, + + _revertCard: function(list, item, src_phid, dst_phid) { + JX.DOM.alterClass(item, 'drag-sending', false); + + var src_column = this.getColumn(src_phid); + var dst_column = this.getColumn(dst_phid); + + src_column.markForRedraw(); + dst_column.markForRedraw(); + this._redrawColumns(); + + list.unlock(); + }, + + _oncardupdate: function(list, src_phid, dst_phid, after_phid, response) { + this.updateCard(response); + + var sounds = response.sounds || []; + for (var ii = 0; ii < sounds.length; ii++) { + JX.Sound.queue(sounds[ii]); + } + + list.unlock(); + }, + + updateCard: function(response) { + var columns = this.getColumns(); + var column_phid; + var card_phid; + var card_data; + + // The server may send us a full or partial update for a card. If we've + // received a full update, we're going to redraw the entire card and may + // need to change which columns it appears in. + + // For a partial update, we've just received supplemental sorting or + // property information and do not need to perform a full redraw. + + // When we reload card state, edit a card, or move a card, we get a full + // update for the card. + + // Ween we move a card in a column, we may get a partial update for other + // visible cards in the column. + + + // Figure out which columns each card now appears in. For cards that + // have received a full update, we'll use this map to move them into + // the correct columns. + var update_map = {}; + for (column_phid in response.columnMaps) { + var target_column = this.getColumn(column_phid); + + if (!target_column) { + // If the column isn't visible, don't try to add a card to it. + continue; + } + + var column_map = response.columnMaps[column_phid]; + + for (var ii = 0; ii < column_map.length; ii++) { + card_phid = column_map[ii]; + if (!update_map[card_phid]) { + update_map[card_phid] = {}; + } + update_map[card_phid][column_phid] = true; + } + } + + // Process card removals. These are cases where the client still sees + // a particular card on a board but it has been removed on the server. + for (card_phid in response.cards) { + card_data = response.cards[card_phid]; + + if (!card_data.remove) { + continue; + } + + for (column_phid in columns) { + var column = columns[column_phid]; + + var card = column.getCard(card_phid); + if (card) { + column.removeCard(card_phid); + column.markForRedraw(); + } + } + } + + // Process partial updates for cards. This is supplemental data which + // we can just merge in without any special handling. + for (card_phid in response.cards) { + card_data = response.cards[card_phid]; + + if (card_data.remove) { + continue; + } + + var card_template = this.getCardTemplate(card_phid); + + if (card_data.nodeHTMLTemplate) { + card_template.setNodeHTMLTemplate(card_data.nodeHTMLTemplate); + } + + var order; + for (order in card_data.vectors) { + card_template.setSortVector(order, card_data.vectors[order]); + } + + for (order in card_data.headers) { + card_template.setHeaderKey(order, card_data.headers[order]); + } + + for (var key in card_data.properties) { + card_template.setObjectProperty(key, card_data.properties[key]); + } + } + + // Process full updates for cards which we have a full update for. This + // may involve moving them between columns. + for (card_phid in response.cards) { + card_data = response.cards[card_phid]; + + if (!card_data.update) { + continue; + } + + for (column_phid in columns) { + var column = columns[column_phid]; + var card = column.getCard(card_phid); + + if (card) { + card.redraw(); + column.markForRedraw(); + } + + // Compare the server state to the client state, and add or remove + // cards on the client as necessary to synchronize them. + + if (update_map[card_phid] && update_map[card_phid][column_phid]) { + if (!card) { + column.newCard(card_phid); + column.markForRedraw(); + } + } else { + if (card) { + column.removeCard(card_phid); + column.markForRedraw(); + } + } + } + } + + var column_maps = response.columnMaps; + var natural_column; + for (var natural_phid in column_maps) { + natural_column = this.getColumn(natural_phid); + if (!natural_column) { + // Our view of the board may be out of date, so we might get back + // information about columns that aren't visible. Just ignore the + // position information for any columns we aren't displaying on the + // client. + continue; + } + + natural_column.setNaturalOrder(column_maps[natural_phid]); + } + + var headers = response.headers; + for (var jj = 0; jj < headers.length; jj++) { + var header = headers[jj]; + + this.getHeaderTemplate(header.key) + .setOrder(header.order) + .setNodeHTMLTemplate(header.template) + .setVector(header.vector) + .setEditProperties(header.editProperties); + } + + this._redrawColumns(); + }, + + _redrawColumns: function() { + var columns = this.getColumns(); + for (var k in columns) { + if (columns[k].isMarkedForRedraw()) { + columns[k].redraw(); + } + } + }, + + _reloadCards: function() { + var state = {}; + + var columns = this.getColumns(); + for (var column_phid in columns) { + var cards = columns[column_phid].getCards(); + for (var card_phid in cards) { + state[card_phid] = this.getCardTemplate(card_phid).getVersion(); + } + } + + var data = { + state: JX.JSON.stringify(state), + order: this.getOrder() + }; + + var on_reload = JX.bind(this, this._onReloadResponse); + + new JX.Request(this.getController().getReloadURI(), on_reload) + .setData(data) + .send(); + }, + + _onReloadResponse: function(response) { + this.updateCard(response); + } + + } + +}); diff --git a/webroot/rsrc/js/application/roles/WorkboardCard.js b/webroot/rsrc/js/application/roles/WorkboardCard.js new file mode 100644 index 0000000000..4a3be2a51d --- /dev/null +++ b/webroot/rsrc/js/application/roles/WorkboardCard.js @@ -0,0 +1,79 @@ +/** + * @provides javelin-workboard-card + * @requires javelin-install + * @javelin + */ + +JX.install('WorkboardCard', { + + construct: function(column, phid) { + this._column = column; + this._phid = phid; + }, + + members: { + _column: null, + _phid: null, + _root: null, + + getPHID: function() { + return this._phid; + }, + + getColumn: function() { + return this._column; + }, + + setColumn: function(column) { + this._column = column; + }, + + getProperties: function() { + return this.getColumn().getBoard() + .getCardTemplate(this.getPHID()) + .getObjectProperties(); + }, + + getPoints: function() { + return this.getProperties().points; + }, + + getStatus: function() { + return this.getProperties().status; + }, + + getNode: function() { + if (!this._root) { + var phid = this.getPHID(); + + var root = this.getColumn().getBoard() + .getCardTemplate(phid) + .newNode(); + + JX.Stratcom.getData(root).objectPHID = phid; + + this._root = root; + } + + return this._root; + }, + + isWorkboardHeader: function() { + return false; + }, + + redraw: function() { + var old_node = this._root; + this._root = null; + var new_node = this.getNode(); + + if (old_node && old_node.parentNode) { + JX.DOM.replace(old_node, new_node); + } + + return this; + } + + } + +}); diff --git a/webroot/rsrc/js/application/roles/WorkboardCardTemplate.js b/webroot/rsrc/js/application/roles/WorkboardCardTemplate.js new file mode 100644 index 0000000000..e3387a4e18 --- /dev/null +++ b/webroot/rsrc/js/application/roles/WorkboardCardTemplate.js @@ -0,0 +1,69 @@ +/** + * @provides javelin-workboard-card-template + * @requires javelin-install + * @javelin + */ + +JX.install('WorkboardCardTemplate', { + + construct: function(phid) { + this._phid = phid; + this._vectors = {}; + this._headerKeys = {}; + + this.setObjectProperties({}); + }, + + properties: { + objectProperties: null + }, + + members: { + _phid: null, + _html: null, + _vectors: null, + _headerKeys: null, + + getPHID: function() { + return this._phid; + }, + + getVersion: function() { + // TODO: For now, just return a constant version number. + return 1; + }, + + setNodeHTMLTemplate: function(html) { + this._html = html; + return this; + }, + + setSortVector: function(order, vector) { + this._vectors[order] = vector; + return this; + }, + + getSortVector: function(order) { + return this._vectors[order]; + }, + + setHeaderKey: function(order, key) { + this._headerKeys[order] = key; + return this; + }, + + getHeaderKey: function(order) { + return this._headerKeys[order]; + }, + + newNode: function() { + return JX.$H(this._html).getFragment().firstChild; + }, + + setObjectProperty: function(key, value) { + this.getObjectProperties()[key] = value; + return this; + } + } + +}); diff --git a/webroot/rsrc/js/application/roles/WorkboardColumn.js b/webroot/rsrc/js/application/roles/WorkboardColumn.js new file mode 100644 index 0000000000..c63e733c1c --- /dev/null +++ b/webroot/rsrc/js/application/roles/WorkboardColumn.js @@ -0,0 +1,486 @@ +/** + * @provides javelin-workboard-column + * @requires javelin-install + * javelin-workboard-card + * javelin-workboard-header + * @javelin + */ + +JX.install('WorkboardColumn', { + + construct: function(board, phid, root) { + this._board = board; + this._phid = phid; + this._root = root; + + this._panel = JX.DOM.findAbove(root, 'div', 'workpanel'); + this._pointsNode = JX.DOM.find(this._panel, 'span', 'column-points'); + + this._pointsContentNode = JX.DOM.find( + this._panel, + 'span', + 'column-points-content'); + + this._cards = {}; + this._headers = {}; + this._objects = []; + this._naturalOrder = []; + this._dropEffects = []; + }, + + properties: { + triggerPreviewEffect: null + }, + + members: { + _phid: null, + _root: null, + _board: null, + _cards: null, + _headers: null, + _naturalOrder: null, + _orderVectors: null, + _panel: null, + _pointsNode: null, + _pointsContentNode: null, + _dirty: true, + _objects: null, + _dropEffects: null, + + getPHID: function() { + return this._phid; + }, + + getRoot: function() { + return this._root; + }, + + getCards: function() { + return this._cards; + }, + + _getObjects: function() { + return this._objects; + }, + + getCard: function(phid) { + return this._cards[phid]; + }, + + getBoard: function() { + return this._board; + }, + + setNaturalOrder: function(order) { + this._naturalOrder = order; + this._orderVectors = null; + return this; + }, + + setDropEffects: function(effects) { + this._dropEffects = effects; + return this; + }, + + getDropEffects: function() { + return this._dropEffects; + }, + + getPointsNode: function() { + return this._pointsNode; + }, + + getPointsContentNode: function() { + return this._pointsContentNode; + }, + + getWorkpanelNode: function() { + return this._panel; + }, + + newCard: function(phid) { + var card = new JX.WorkboardCard(this, phid); + + this._cards[phid] = card; + this._naturalOrder.push(phid); + this._orderVectors = null; + + return card; + }, + + removeCard: function(phid) { + var card = this._cards[phid]; + delete this._cards[phid]; + + for (var ii = 0; ii < this._naturalOrder.length; ii++) { + if (this._naturalOrder[ii] == phid) { + this._naturalOrder.splice(ii, 1); + this._orderVectors = null; + break; + } + } + + return card; + }, + + addCard: function(card, after) { + var phid = card.getPHID(); + + card.setColumn(this); + this._cards[phid] = card; + + var index = 0; + + if (after) { + for (var ii = 0; ii < this._naturalOrder.length; ii++) { + if (this._naturalOrder[ii] == after) { + index = ii + 1; + break; + } + } + } + + if (index > this._naturalOrder.length) { + this._naturalOrder.push(phid); + } else { + this._naturalOrder.splice(index, 0, phid); + } + + this._orderVectors = null; + + return this; + }, + + getDropTargetNodes: function() { + var objects = this._getObjects(); + + var nodes = []; + for (var ii = 0; ii < objects.length; ii++) { + var object = objects[ii]; + nodes.push(object.getNode()); + } + + return nodes; + }, + + getCardPHIDs: function() { + return JX.keys(this.getCards()); + }, + + getPointLimit: function() { + return JX.Stratcom.getData(this.getRoot()).pointLimit; + }, + + markForRedraw: function() { + this._dirty = true; + }, + + isMarkedForRedraw: function() { + return this._dirty; + }, + + getHeader: function(key) { + if (!this._headers[key]) { + this._headers[key] = new JX.WorkboardHeader(this, key); + } + return this._headers[key]; + }, + + handleDragGhost: function(default_handler, ghost, node) { + // If the column has headers, don't let the user drag a card above + // the topmost header: for example, you can't change a task to have + // a priority higher than the highest possible priority. + + if (this._hasColumnHeaders()) { + if (!node) { + return false; + } + } + + return default_handler(ghost, node); + }, + + _hasColumnHeaders: function() { + var board = this.getBoard(); + var order = board.getOrder(); + + return board.getOrderTemplate(order).getHasHeaders(); + }, + + redraw: function() { + var board = this.getBoard(); + var order = board.getOrder(); + + var list = this._getCardsSortedByKey(order); + + var ii; + var objects = []; + + var has_headers = this._hasColumnHeaders(); + var header_keys = []; + var seen_headers = {}; + if (has_headers) { + var header_templates = board.getHeaderTemplatesForOrder(order); + for (var k in header_templates) { + header_keys.push(header_templates[k].getHeaderKey()); + } + header_keys.reverse(); + } + + var header_key; + var next; + for (ii = 0; ii < list.length; ii++) { + var card = list[ii]; + + // If a column has a "High" priority card and a "Low" priority card, + // we need to add the "Normal" header in between them. This allows + // you to change priority to "Normal" even if there are no "Normal" + // cards in a column. + + if (has_headers) { + header_key = board.getCardTemplate(card.getPHID()) + .getHeaderKey(order); + + if (!seen_headers[header_key]) { + while (header_keys.length) { + next = header_keys.pop(); + + var header = this.getHeader(next); + objects.push(header); + seen_headers[header_key] = true; + + if (next === header_key) { + break; + } + } + } + } + + objects.push(card); + } + + // Add any leftover headers at the bottom of the column which don't have + // any cards in them. In particular, empty columns don't have any cards + // but should still have headers. + + while (header_keys.length) { + next = header_keys.pop(); + + if (seen_headers[next]) { + continue; + } + + objects.push(this.getHeader(next)); + } + + this._objects = objects; + + var content = []; + for (ii = 0; ii < this._objects.length; ii++) { + var object = this._objects[ii]; + + var node = object.getNode(); + content.push(node); + } + + JX.DOM.setContent(this.getRoot(), content); + + this._redrawFrame(); + + this._dirty = false; + }, + + compareHandler: function(src_list, src_node, dst_list, dst_node) { + var board = this.getBoard(); + var order = board.getOrder(); + + var u_vec = this._getNodeOrderVector(src_node, order); + var v_vec = this._getNodeOrderVector(dst_node, order); + + return board.compareVectors(u_vec, v_vec); + }, + + _getNodeOrderVector: function(node, order) { + var board = this.getBoard(); + var data = JX.Stratcom.getData(node); + + if (data.objectPHID) { + return this._getOrderVector(data.objectPHID, order); + } + + return board.getHeaderTemplate(data.headerKey).getVector(); + }, + + setIsDropTarget: function(is_target) { + var node = this.getWorkpanelNode(); + JX.DOM.alterClass(node, 'workboard-column-drop-target', is_target); + }, + + _getCardsSortedByKey: function(order) { + var cards = this.getCards(); + + var list = []; + for (var k in cards) { + list.push(cards[k]); + } + + list.sort(JX.bind(this, this._sortCards, order)); + + return list; + }, + + _sortCards: function(order, u, v) { + var board = this.getBoard(); + var u_vec = this._getOrderVector(u.getPHID(), order); + var v_vec = this._getOrderVector(v.getPHID(), order); + + return board.compareVectors(u_vec, v_vec); + }, + + _getOrderVector: function(phid, order) { + var board = this.getBoard(); + + if (!this._orderVectors) { + this._orderVectors = {}; + } + + if (!this._orderVectors[order]) { + var cards = this.getCards(); + var vectors = {}; + + for (var k in cards) { + var card_phid = cards[k].getPHID(); + var vector = board.getCardTemplate(card_phid) + .getSortVector(order); + + vectors[card_phid] = [].concat(vector); + + // Push a "card" type, so cards always sort after headers; headers + // have a "0" in this position. + vectors[card_phid].push(1); + } + + for (var ii = 0; ii < this._naturalOrder.length; ii++) { + var natural_phid = this._naturalOrder[ii]; + if (vectors[natural_phid]) { + vectors[natural_phid].push(ii); + } + } + + this._orderVectors[order] = vectors; + } + + if (!this._orderVectors[order][phid]) { + // In this case, we're comparing a card being dragged in from another + // column to the cards already in this column. We're just going to + // build a temporary vector for it. + var incoming_vector = board.getCardTemplate(phid) + .getSortVector(order); + incoming_vector = [].concat(incoming_vector); + + // Add a "card" type to sort this after headers. + incoming_vector.push(1); + + // Add a "0" for the natural ordering to put this on top. A new card + // has no natural ordering on a column it isn't part of yet. + incoming_vector.push(0); + + return incoming_vector; + } + + return this._orderVectors[order][phid]; + }, + + _redrawFrame: function() { + var cards = this.getCards(); + var board = this.getBoard(); + + var points = {}; + var count = 0; + var decimal_places = 0; + for (var phid in cards) { + var card = cards[phid]; + + var card_points; + if (board.getPointsEnabled()) { + card_points = card.getPoints(); + } else { + card_points = 1; + } + + if (card_points !== null) { + var status = card.getStatus(); + if (!points[status]) { + points[status] = 0; + } + points[status] += card_points; + + // Count the number of decimal places in the point value with the + // most decimal digits. We'll use the same precision when rendering + // the point sum. This avoids rounding errors and makes the display + // a little more consistent. + var parts = card_points.toString().split('.'); + if (parts[1]) { + decimal_places = Math.max(decimal_places, parts[1].length); + } + } + + count++; + } + + var total_points = 0; + for (var k in points) { + total_points += points[k]; + } + total_points = total_points.toFixed(decimal_places); + + var limit = this.getPointLimit(); + + var display_value; + if (limit !== null && limit !== 0) { + display_value = total_points + ' / ' + limit; + } else { + display_value = total_points; + } + + if (board.getPointsEnabled()) { + display_value = count + ' | ' + display_value; + } + + var over_limit = ((limit !== null) && (total_points > limit)); + + var content_node = this.getPointsContentNode(); + var points_node = this.getPointsNode(); + + JX.DOM.setContent(content_node, display_value); + + // Only put the "empty" style on the column (which just adds some empty + // space so it's easier to drop cards into an empty column) if it has no + // cards and no headers. + + var is_empty = + (!this.getCardPHIDs().length) && + (!this._hasColumnHeaders()); + + var panel = JX.DOM.findAbove(this.getRoot(), 'div', 'workpanel'); + JX.DOM.alterClass(panel, 'role-panel-empty', is_empty); + + + JX.DOM.alterClass(panel, 'role-panel-over-limit', over_limit); + + var color_map = { + 'phui-tag-disabled': (total_points === 0), + 'phui-tag-blue': (total_points > 0 && !over_limit), + 'phui-tag-red': (over_limit) + }; + + for (var c in color_map) { + JX.DOM.alterClass(points_node, c, !!color_map[c]); + } + + JX.DOM.show(points_node); + } + + } + +}); diff --git a/webroot/rsrc/js/application/roles/WorkboardController.js b/webroot/rsrc/js/application/roles/WorkboardController.js new file mode 100644 index 0000000000..3b7bfe11cf --- /dev/null +++ b/webroot/rsrc/js/application/roles/WorkboardController.js @@ -0,0 +1,201 @@ +/** + * @provides javelin-workboard-controller + * @requires javelin-install + * javelin-dom + * javelin-util + * javelin-vector + * javelin-stratcom + * javelin-workflow + * phabricator-drag-and-drop-file-upload + * javelin-workboard-board + * @javelin + */ + +JX.install('WorkboardController', { + + construct: function() { + this._boards = {}; + }, + + properties: { + uploadURI: null, + coverURI: null, + moveURI: null, + reloadURI: null, + chunkThreshold: null + }, + + members: { + _boards: null, + + _panOrigin: null, + _panNode: null, + _panX: null, + + start: function() { + this._setupCoverImageHandlers(); + this._setupPanHandlers(); + this._setupEditHandlers(); + + return this; + }, + + newBoard: function(phid, node) { + var board = new JX.WorkboardBoard(this, phid, node); + this._boards[phid] = board; + return board; + }, + + _getBoard: function(board_phid) { + return this._boards[board_phid]; + }, + + _setupCoverImageHandlers: function() { + if (!JX.PhabricatorDragAndDropFileUpload.isSupported()) { + return; + } + + var drop = new JX.PhabricatorDragAndDropFileUpload('role-card') + .setURI(this.getUploadURI()) + .setChunkThreshold(this.getChunkThreshold()); + + drop.listen('didBeginDrag', function(node) { + JX.DOM.alterClass(node, 'phui-workcard-upload-target', true); + }); + + drop.listen('didEndDrag', function(node) { + JX.DOM.alterClass(node, 'phui-workcard-upload-target', false); + }); + + drop.listen('didUpload', JX.bind(this, this._oncoverupload)); + + drop.start(); + }, + + _oncoverupload: function(file) { + var node = file.getTargetNode(); + + var board = this._getBoardFromNode(node); + + var column_node = JX.DOM.findAbove(node, 'ul', 'role-column'); + var column_phid = JX.Stratcom.getData(column_node).columnPHID; + var column = board.getColumn(column_phid); + + var data = { + boardPHID: board.getPHID(), + objectPHID: JX.Stratcom.getData(node).objectPHID, + filePHID: file.getPHID(), + visiblePHIDs: column.getCardPHIDs() + }; + + new JX.Workflow(this.getCoverURI(), data) + .setHandler(JX.bind(board, board.updateCard)) + .start(); + }, + + _getBoardFromNode: function(node) { + var board_node = JX.DOM.findAbove(node, 'div', 'jx-workboard'); + var board_phid = JX.Stratcom.getData(board_node).boardPHID; + return this._getBoard(board_phid); + }, + + _setupPanHandlers: function() { + var mousedown = JX.bind(this, this._onpanmousedown); + var mousemove = JX.bind(this, this._onpanmousemove); + var mouseup = JX.bind(this, this._onpanmouseup); + + JX.Stratcom.listen('mousedown', 'workboard-shadow', mousedown); + JX.Stratcom.listen('mousemove', null, mousemove); + JX.Stratcom.listen('mouseup', null, mouseup); + }, + + _onpanmousedown: function(e) { + if (!JX.Device.isDesktop()) { + return; + } + + if (e.getNode('workpanel')) { + return; + } + + if (JX.Stratcom.pass()) { + return; + } + + e.kill(); + + this._panOrigin = JX.$V(e); + this._panNode = e.getNode('workboard-shadow'); + this._panX = this._panNode.scrollLeft; + }, + + _onpanmousemove: function(e) { + if (!this._panOrigin) { + return; + } + + var cursor = JX.$V(e); + this._panNode.scrollLeft = this._panX + (this._panOrigin.x - cursor.x); + }, + + _onpanmouseup: function() { + this._panOrigin = null; + }, + + _setupEditHandlers: function() { + var onadd = JX.bind(this, this._onaddcard); + var onedit = JX.bind(this, this._oneditcard); + + JX.Stratcom.listen('click', 'column-add-task', onadd); + JX.Stratcom.listen('click', 'edit-role-card', onedit); + }, + + _onaddcard: function(e) { + // We want the 'boards-dropdown-menu' behavior to see this event and + // close the dropdown, but don't want to follow the link. + e.prevent(); + + var column_data = e.getNodeData('column-add-task'); + var column_phid = column_data.columnPHID; + + var board_phid = column_data.boardPHID; + var board = this._getBoard(board_phid); + var column = board.getColumn(column_phid); + + var request_data = { + responseType: 'card', + columnPHID: column.getPHID(), + roles: column_data.rolePHID, + visiblePHIDs: column.getCardPHIDs(), + order: board.getOrder() + }; + + new JX.Workflow(column_data.createURI, request_data) + .setHandler(JX.bind(board, board.updateCard)) + .start(); + }, + + _oneditcard: function(e) { + e.kill(); + + var column_node = e.getNode('role-column'); + var column_phid = JX.Stratcom.getData(column_node).columnPHID; + + var board = this._getBoardFromNode(column_node); + var column = board.getColumn(column_phid); + + var request_data = { + responseType: 'card', + columnPHID: column.getPHID(), + visiblePHIDs: column.getCardPHIDs(), + order: board.getOrder() + }; + + new JX.Workflow(e.getNode('tag:a').href, request_data) + .setHandler(JX.bind(board, board.updateCard)) + .start(); + } + + } + +}); diff --git a/webroot/rsrc/js/application/roles/WorkboardDropEffect.js b/webroot/rsrc/js/application/roles/WorkboardDropEffect.js new file mode 100644 index 0000000000..0c729fc517 --- /dev/null +++ b/webroot/rsrc/js/application/roles/WorkboardDropEffect.js @@ -0,0 +1,73 @@ +/** + * @provides javelin-workboard-drop-effect + * @requires javelin-install + * javelin-dom + * @javelin + */ + +JX.install('WorkboardDropEffect', { + + properties: { + icon: null, + color: null, + content: null, + isTriggerEffect: false, + isHeader: false, + conditions: [] + }, + + statics: { + newFromDictionary: function(map) { + return new JX.WorkboardDropEffect() + .setIcon(map.icon) + .setColor(map.color) + .setContent(JX.$H(map.content)) + .setIsTriggerEffect(map.isTriggerEffect) + .setIsHeader(map.isHeader) + .setConditions(map.conditions || []); + } + }, + + members: { + newNode: function() { + var icon = new JX.PHUIXIconView() + .setIcon(this.getIcon()) + .setColor(this.getColor()) + .getNode(); + + var attributes = {}; + + if (this.getIsHeader()) { + attributes.className = 'workboard-drop-preview-header'; + } + + return JX.$N('li', attributes, [icon, this.getContent()]); + }, + + isEffectVisibleForCard: function(card) { + var conditions = this.getConditions(); + + var properties = card.getProperties(); + for (var ii = 0; ii < conditions.length; ii++) { + var condition = conditions[ii]; + + var field = properties[condition.field]; + var value = condition.value; + + var result = true; + switch (condition.operator) { + case '!=': + result = (field !== value); + break; + } + + if (!result) { + return false; + } + } + + return true; + } + + } +}); diff --git a/webroot/rsrc/js/application/roles/WorkboardHeader.js b/webroot/rsrc/js/application/roles/WorkboardHeader.js new file mode 100644 index 0000000000..a0cbfc13c7 --- /dev/null +++ b/webroot/rsrc/js/application/roles/WorkboardHeader.js @@ -0,0 +1,48 @@ +/** + * @provides javelin-workboard-header + * @requires javelin-install + * @javelin + */ + +JX.install('WorkboardHeader', { + + construct: function(column, header_key) { + this._column = column; + this._headerKey = header_key; + }, + + members: { + _root: null, + _column: null, + _headerKey: null, + + getColumn: function() { + return this._column; + }, + + getHeaderKey: function() { + return this._headerKey; + }, + + getNode: function() { + if (!this._root) { + var header_key = this.getHeaderKey(); + + var root = this.getColumn().getBoard() + .getHeaderTemplate(header_key) + .newNode(); + + JX.Stratcom.getData(root).headerKey = header_key; + + this._root = root; + } + + return this._root; + }, + + isWorkboardHeader: function() { + return true; + } + } + +}); diff --git a/webroot/rsrc/js/application/roles/WorkboardHeaderTemplate.js b/webroot/rsrc/js/application/roles/WorkboardHeaderTemplate.js new file mode 100644 index 0000000000..d64a56dd29 --- /dev/null +++ b/webroot/rsrc/js/application/roles/WorkboardHeaderTemplate.js @@ -0,0 +1,40 @@ +/** + * @provides javelin-workboard-header-template + * @requires javelin-install + * @javelin + */ + +JX.install('WorkboardHeaderTemplate', { + + construct: function(header_key) { + this._headerKey = header_key; + }, + + properties: { + template: null, + order: null, + vector: null, + editProperties: null, + dropEffects: [] + }, + + members: { + _headerKey: null, + _html: null, + + getHeaderKey: function() { + return this._headerKey; + }, + + setNodeHTMLTemplate: function(html) { + this._html = html; + return this; + }, + + newNode: function() { + return JX.$H(this._html).getFragment().firstChild; + } + + } + +}); diff --git a/webroot/rsrc/js/application/roles/WorkboardOrderTemplate.js b/webroot/rsrc/js/application/roles/WorkboardOrderTemplate.js new file mode 100644 index 0000000000..083dc78b50 --- /dev/null +++ b/webroot/rsrc/js/application/roles/WorkboardOrderTemplate.js @@ -0,0 +1,27 @@ +/** + * @provides javelin-workboard-order-template + * @requires javelin-install + * @javelin + */ + +JX.install('WorkboardOrderTemplate', { + + construct: function(order) { + this._orderKey = order; + }, + + properties: { + hasHeaders: false, + canReorder: false + }, + + members: { + _orderKey: null, + + getOrderKey: function() { + return this._orderKey; + } + + } + +}); diff --git a/webroot/rsrc/js/application/roles/behavior-reorder-columns.js b/webroot/rsrc/js/application/roles/behavior-reorder-columns.js new file mode 100644 index 0000000000..0125f4d0e3 --- /dev/null +++ b/webroot/rsrc/js/application/roles/behavior-reorder-columns.js @@ -0,0 +1,58 @@ +/** + * @provides javelin-behavior-reorder-columns + * @requires javelin-behavior + * javelin-stratcom + * javelin-workflow + * javelin-dom + * phabricator-draggable-list + */ + +JX.behavior('reorder-columns', function(config) { + + var root = JX.$(config.listID); + + var list = new JX.DraggableList('board-column', root) + .setFindItemsHandler(function() { + return JX.DOM.scry(root, 'li', 'board-column'); + }); + + list.listen('didDrop', function(node) { + var nodes = list.findItems(); + + var node_data = JX.Stratcom.getData(node); + + // Find the column sequence of the previous node. + var sequence = null; + var data; + for (var ii = 0; ii < nodes.length; ii++) { + data = JX.Stratcom.getData(nodes[ii]); + if (data.columnPHID === node_data.columnPHID) { + break; + } + sequence = data.columnSequence; + } + + list.lock(); + JX.DOM.alterClass(node, 'drag-sending', true); + + var parameters = { + columnPHID: node_data.columnPHID, + sequence: (sequence === null) ? 0 : (parseInt(sequence, 10) + 1) + }; + + new JX.Workflow(config.reorderURI, parameters) + .setHandler(function(r) { + + // Adjust metadata for the new sequence numbers. + for (var ii = 0; ii < nodes.length; ii++) { + var data = JX.Stratcom.getData(nodes[ii]); + data.columnSequence = r.sequenceMap[data.columnPHID]; + } + + list.unlock(); + JX.DOM.alterClass(node, 'drag-sending', false); + }) + .start(); + }); + +}); diff --git a/webroot/rsrc/js/application/roles/behavior-role-boards.js b/webroot/rsrc/js/application/roles/behavior-role-boards.js new file mode 100644 index 0000000000..a4919856d4 --- /dev/null +++ b/webroot/rsrc/js/application/roles/behavior-role-boards.js @@ -0,0 +1,182 @@ +/** + * @provides javelin-behavior-role-boards + * @requires javelin-behavior + * javelin-dom + * javelin-util + * javelin-vector + * javelin-stratcom + * javelin-workflow + * javelin-workboard-controller + * javelin-workboard-drop-effect + */ + +JX.behavior('role-boards', function(config, statics) { + + function update_statics(update_config) { + statics.boardID = update_config.boardID; + statics.rolePHID = update_config.rolePHID; + statics.order = update_config.order; + statics.moveURI = update_config.moveURI; + } + + function setup() { + JX.Stratcom.listen('click', 'boards-dropdown-menu', function(e) { + var data = e.getNodeData('boards-dropdown-menu'); + if (data.menu) { + return; + } + + e.kill(); + + var list = JX.$H(data.items).getFragment().firstChild; + + var button = e.getNode('boards-dropdown-menu'); + data.menu = new JX.PHUIXDropdownMenu(button); + data.menu.setContent(list); + data.menu.open(); + }); + + JX.Stratcom.listen( + 'quicksand-redraw', + null, + function (e) { + var data = e.getData(); + if (!data.newResponse.boardConfig) { + return; + } + var new_config; + if (data.fromServer) { + new_config = data.newResponse.boardConfig; + statics.boardConfigCache[data.newResponseID] = new_config; + } else { + new_config = statics.boardConfigCache[data.newResponseID]; + statics.boardID = new_config.boardID; + } + update_statics(new_config); + }); + + return true; + } + + if (!statics.setup) { + update_statics(config); + var current_page_id = JX.Quicksand.getCurrentPageID(); + statics.boardConfigCache = {}; + statics.boardConfigCache[current_page_id] = config; + statics.setup = setup(); + } + + if (!statics.workboard) { + statics.workboard = new JX.WorkboardController() + .setUploadURI(config.uploadURI) + .setCoverURI(config.coverURI) + .setMoveURI(config.moveURI) + .setReloadURI(config.reloadURI) + .setChunkThreshold(config.chunkThreshold) + .start(); + } + + var board_phid = config.rolePHID; + var board_node = JX.$(config.boardID); + + var board = statics.workboard.newBoard(board_phid, board_node) + .setOrder(config.order) + .setPointsEnabled(config.pointsEnabled); + + var templates = config.templateMap; + for (var k in templates) { + board.getCardTemplate(k) + .setNodeHTMLTemplate(templates[k]); + } + + var ii; + var jj; + var effects; + + for (ii = 0; ii < config.columnTemplates.length; ii++) { + var spec = config.columnTemplates[ii]; + + var column = board.getColumn(spec.columnPHID); + + effects = []; + for (jj = 0; jj < spec.effects.length; jj++) { + effects.push( + JX.WorkboardDropEffect.newFromDictionary( + spec.effects[jj])); + } + column.setDropEffects(effects); + + for (jj = 0; jj < spec.cardPHIDs.length; jj++) { + column.newCard(spec.cardPHIDs[jj]); + } + + if (spec.triggerPreviewEffect) { + column.setTriggerPreviewEffect( + JX.WorkboardDropEffect.newFromDictionary( + spec.triggerPreviewEffect)); + } + } + + var order_maps = config.orderMaps; + for (var object_phid in order_maps) { + var order_card = board.getCardTemplate(object_phid); + for (var order_key in order_maps[object_phid]) { + order_card.setSortVector(order_key, order_maps[object_phid][order_key]); + } + } + + var property_maps = config.propertyMaps; + for (var property_phid in property_maps) { + board.getCardTemplate(property_phid) + .setObjectProperties(property_maps[property_phid]); + } + + var headers = config.headers; + for (ii = 0; ii < headers.length; ii++) { + var header = headers[ii]; + + effects = []; + for (jj = 0; jj < header.effects.length; jj++) { + effects.push( + JX.WorkboardDropEffect.newFromDictionary( + header.effects[jj])); + } + + board.getHeaderTemplate(header.key) + .setOrder(header.order) + .setNodeHTMLTemplate(header.template) + .setVector(header.vector) + .setEditProperties(header.editProperties) + .setDropEffects(effects); + } + + var orders = config.orders; + for (ii = 0; ii < orders.length; ii++) { + var order = orders[ii]; + + board.getOrderTemplate(order.orderKey) + .setHasHeaders(order.hasHeaders) + .setCanReorder(order.canReorder); + } + + var header_keys = config.headerKeys; + for (var header_phid in header_keys) { + board.getCardTemplate(header_phid) + .setHeaderKey(config.order, header_keys[header_phid]); + } + + board.start(); + + // In Safari, we can only play sounds that we've already loaded, and we can + // only load them in response to an explicit user interaction like a click. + var sounds = config.preloadSounds; + var listener = JX.Stratcom.listen('mousedown', null, function() { + for (var ii = 0; ii < sounds.length; ii++) { + JX.Sound.load(sounds[ii]); + } + + // Remove this callback once it has run once. + listener.remove(); + }); + +}); diff --git a/webroot/rsrc/js/application/roles/behavior-role-create.js b/webroot/rsrc/js/application/roles/behavior-role-create.js new file mode 100644 index 0000000000..8c529a3f14 --- /dev/null +++ b/webroot/rsrc/js/application/roles/behavior-role-create.js @@ -0,0 +1,26 @@ +/** + * @provides javelin-behavior-role-create + * @requires javelin-behavior + * javelin-dom + * javelin-stratcom + * javelin-workflow + */ + +JX.behavior('role-create', function(config) { + + JX.Stratcom.listen( + 'click', + 'role-create', + function(e) { + JX.Workflow.newFromLink(e.getTarget()) + .setHandler(function(r) { + var node = JX.$(config.tokenizerID); + var tokenizer = JX.Stratcom.getData(node).tokenizer; + tokenizer.addToken(r.phid, r.name); + }) + .start(); + + e.kill(); + }); + +}); From aff768d779ede8dddd337b01ae4d2e6917818edf Mon Sep 17 00:00:00 2001 From: Tom Date: Mon, 1 Nov 2021 16:34:16 +0100 Subject: [PATCH 07/62] T9586: first version of time tracker --- .gitignore | 3 - resources/celerity/map.php | 3132 ++--- resources/sql/quickstart.sql | 17 + src/__phutil_library_map__.php | 4 + src/aphront/response/AphrontResponse.php | 2 +- .../application/TimeTrackerApplication.php | 54 + .../application/TimeTrackerDAO.php | 8 + .../application/TimeTrackerStorageManager.php | 45 + .../application/TimeTrackerTimeUtils.php | 33 + .../timetracker/components/TimeTracker.php | 33 + .../components/TimeTrackerMainPanel.php | 68 + .../components/TimeTrackerPanelType.php | 7 + .../TimeTrackerSelectDateFormComponent.php | 65 + .../components/TimeTrackerSummaryPanel.php | 126 + .../TimeTrackerRenderController.php | 83 + .../TimeTrackerDayDetailsRequestHandler.php | 66 + .../TimeTrackerMainPanelRequestHandler.php | 109 + .../TimeTrackerRequestHandler.php | 5 + .../TimeTrackerSummaryPanelRequestHandler.php | 77 + .../images/ui-icons_444444_256x240.png | Bin 0 -> 7090 bytes .../images/ui-icons_555555_256x240.png | Bin 0 -> 7074 bytes .../images/ui-icons_777620_256x240.png | Bin 0 -> 4618 bytes .../images/ui-icons_777777_256x240.png | Bin 0 -> 7111 bytes .../images/ui-icons_cc0000_256x240.png | Bin 0 -> 4618 bytes .../images/ui-icons_ffffff_256x240.png | Bin 0 -> 6487 bytes .../css/application/timetracker/jquery-ui.css | 695 + .../timetracker/chart/Chart.min.js | 6515 +++++++++ .../timetracker/chart/jquery.min.js | 7 + .../timetracker/chart/show-graph.js | 34 + .../js/application/timetracker/jquery-ui.js | 2165 +++ .../rsrc/js/application/timetracker/jquery.js | 10998 ++++++++++++++++ .../js/application/timetracker/timetracker.js | 11 + 32 files changed, 22802 insertions(+), 1560 deletions(-) create mode 100644 src/extensions/timetracker/application/TimeTrackerApplication.php create mode 100644 src/extensions/timetracker/application/TimeTrackerDAO.php create mode 100644 src/extensions/timetracker/application/TimeTrackerStorageManager.php create mode 100644 src/extensions/timetracker/application/TimeTrackerTimeUtils.php create mode 100644 src/extensions/timetracker/components/TimeTracker.php create mode 100644 src/extensions/timetracker/components/TimeTrackerMainPanel.php create mode 100644 src/extensions/timetracker/components/TimeTrackerPanelType.php create mode 100644 src/extensions/timetracker/components/TimeTrackerSelectDateFormComponent.php create mode 100644 src/extensions/timetracker/components/TimeTrackerSummaryPanel.php create mode 100644 src/extensions/timetracker/controller/TimeTrackerRenderController.php create mode 100644 src/extensions/timetracker/requesthandlers/TimeTrackerDayDetailsRequestHandler.php create mode 100644 src/extensions/timetracker/requesthandlers/TimeTrackerMainPanelRequestHandler.php create mode 100644 src/extensions/timetracker/requesthandlers/TimeTrackerRequestHandler.php create mode 100644 src/extensions/timetracker/requesthandlers/TimeTrackerSummaryPanelRequestHandler.php create mode 100644 webroot/rsrc/css/application/timetracker/images/ui-icons_444444_256x240.png create mode 100644 webroot/rsrc/css/application/timetracker/images/ui-icons_555555_256x240.png create mode 100644 webroot/rsrc/css/application/timetracker/images/ui-icons_777620_256x240.png create mode 100644 webroot/rsrc/css/application/timetracker/images/ui-icons_777777_256x240.png create mode 100644 webroot/rsrc/css/application/timetracker/images/ui-icons_cc0000_256x240.png create mode 100644 webroot/rsrc/css/application/timetracker/images/ui-icons_ffffff_256x240.png create mode 100644 webroot/rsrc/css/application/timetracker/jquery-ui.css create mode 100644 webroot/rsrc/js/application/timetracker/chart/Chart.min.js create mode 100644 webroot/rsrc/js/application/timetracker/chart/jquery.min.js create mode 100644 webroot/rsrc/js/application/timetracker/chart/show-graph.js create mode 100644 webroot/rsrc/js/application/timetracker/jquery-ui.js create mode 100644 webroot/rsrc/js/application/timetracker/jquery.js create mode 100644 webroot/rsrc/js/application/timetracker/timetracker.js diff --git a/.gitignore b/.gitignore index d9f44b6bab..45dc0c5ee9 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/ diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 61a349bcad..4578387818 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -7,277 +7,284 @@ */ return array( 'names' => array( - 'conpherence.pkg.css' => '0e3cf785', - 'conpherence.pkg.js' => '020aebcf', - 'core.pkg.css' => '00a2e7f4', - 'core.pkg.js' => 'd2de90d9', - 'dark-console.pkg.js' => '187792c2', - 'differential.pkg.css' => 'ffb69e3d', - 'differential.pkg.js' => '8deec4cd', - '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' => '06fe569e', + 'core.pkg.js' => 'e41d1fc7', + 'dark-console.pkg.js' => 'f835403b', + 'differential.pkg.css' => 'd66e894a', + 'differential.pkg.js' => 'c0e0b7bf', + '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/releeph/releeph-core.css' => 'f81ff2db', - 'rsrc/css/application/releeph/releeph-preview-branch.css' => '22db5c07', - 'rsrc/css/application/releeph/releeph-request-differential-create-dialog.css' => '0ac1ea31', - 'rsrc/css/application/releeph/releeph-request-typeahead.css' => 'bce37359', - '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' => '5f752bdb', - '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/releeph/releeph-core.css' => 'af2da316', + 'rsrc/css/application/releeph/releeph-preview-branch.css' => '46755566', + 'rsrc/css/application/releeph/releeph-request-differential-create-dialog.css' => 'f91c659d', + 'rsrc/css/application/releeph/releeph-request-typeahead.css' => 'e991846d', + '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' => '70809291', + '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' => '4b575805', + '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', @@ -360,1038 +367,996 @@ '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/releeph/releeph-preview-branch.js' => '75184d68', - 'rsrc/js/application/releeph/releeph-request-state-change.js' => '9f081f05', - 'rsrc/js/application/releeph/releeph-request-typeahead.js' => 'aa3a100c', - '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' => '7ad020a5', - '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' => 'e47cb8ba', + '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/releeph/releeph-preview-branch.js' => 'a3506522', + 'rsrc/js/application/releeph/releeph-request-state-change.js' => '8a6d9716', + 'rsrc/js/application/releeph/releeph-request-typeahead.js' => 'c9844507', + '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' => 'a09e9a20', + 'rsrc/js/application/timetracker/chart/jquery.min.js' => '81da9b0e', + 'rsrc/js/application/timetracker/chart/show-graph.js' => 'db6258d5', + 'rsrc/js/application/timetracker/jquery-ui.js' => '1df4aaaf', + 'rsrc/js/application/timetracker/jquery.js' => '0c8323ad', + 'rsrc/js/application/timetracker/timetracker.js' => 'fcfdcbc0', + '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' => 'f96f6131', + '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' => '7ad020a5', - '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-releeph-preview-branch' => '75184d68', - 'javelin-behavior-releeph-request-state-change' => '9f081f05', - 'javelin-behavior-releeph-request-typeahead' => 'aa3a100c', - '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' => '5f752bdb', - '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', - 'releeph-core' => 'f81ff2db', - 'releeph-preview-branch' => '22db5c07', - 'releeph-request-differential-create-dialog' => '0ac1ea31', - 'releeph-request-typeahead-css' => 'bce37359', - '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' => 'a09e9a20', + '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' => 'f96f6131', + '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-releeph-preview-branch' => 'a3506522', + 'javelin-behavior-releeph-request-state-change' => '8a6d9716', + 'javelin-behavior-releeph-request-typeahead' => 'c9844507', + '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' => 'e47cb8ba', + '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' => '0c8323ad', + 'jquery-ui-css' => '70809291', + 'jquery-ui-js' => '1df4aaaf', + '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' => '4b575805', + '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', + 'releeph-core' => 'af2da316', + 'releeph-preview-branch' => '46755566', + 'releeph-request-differential-create-dialog' => 'f91c659d', + 'releeph-request-typeahead-css' => 'e991846d', + 'setup-issue-css' => '539e7d71', + 'show-graph' => 'db6258d5', + 'sprite-login-css' => '511a7d30', + 'sprite-tokens-css' => '8fd81966', + 'syntax-default-css' => 'f1d67c7e', + 'syntax-highlighting-css' => '66087f6a', + 'timetracker-js' => 'fcfdcbc0', + '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', + '39c77ea0' => array( + 'javelin-behavior', + 'javelin-dom', + '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( - 'javelin-behavior', + '4b0b997a' => array( + 'javelin-install', + 'javelin-util', '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', + '4bead335' => array( + 'javelin-install', ), - '42c44e8b' => array( + '4c29854b' => array( 'javelin-behavior', - 'javelin-workflow', - 'javelin-json', + 'javelin-stratcom', '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', @@ -1405,312 +1370,237 @@ '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', - ), - 54262396 => 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', + '5f013b5e' => array( + 'javelin-behavior', + 'javelin-stratcom', 'javelin-dom', ), - '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', - ), - '72960bc1' => array( - 'javelin-install', - 'javelin-reactor', - 'javelin-util', - 'javelin-reactor-node-calmer', - ), - '73ecc1f8' => array( + '763dd0b2' => array( 'javelin-behavior', - 'javelin-behavior-device', + 'javelin-dom', + 'javelin-util', + 'javelin-workflow', 'javelin-stratcom', - 'phabricator-tooltip', ), - 74446546 => array( - 'javelin-behavior', + '78e64734' => array( + 'javelin-install', 'javelin-dom', ), - '75184d68' => array( + '79b71246' => array( 'javelin-behavior', 'javelin-dom', - 'javelin-uri', - 'javelin-request', - ), - '78bc5d94' => array( - 'javelin-behavior', - 'javelin-uri', - 'phabricator-notification', - ), - '78f811c9' => array( - 'javelin-install', + 'javelin-stratcom', ), - '7930776a' => array( + '79e07647' => array( 'javelin-install', - 'javelin-dom', + 'javelin-util', + 'javelin-request', + 'javelin-router', ), - '7ad020a5' => array( + '7a17e183' => array( 'javelin-behavior', 'javelin-dom', - 'phabricator-drag-and-drop-file-upload', - 'phabricator-textareautils', - ), - '7b139193' => array( - 'javelin-behavior', 'javelin-stratcom', - 'javelin-workflow', - 'javelin-dom', ), - '7c4d8998' => array( - 'javelin-install', - 'javelin-dom', + '7a61e28c' => array( + 'phuix-icon-view', ), - '80bff3af' => array( + '7b5bd01c' => array( 'javelin-install', - 'javelin-typeahead-source', + 'javelin-util', ), - '81debc48' => array( + '7dbf81e3' => array( + 'javelin-color', 'javelin-install', 'javelin-util', - 'javelin-stratcom', - 'javelin-dom', - 'javelin-vector', ), - '8207abf9' => array( - 'javelin-dom', + '7dcfc62b' => array( + 'javelin-behavior', + 'javelin-uri', + 'phabricator-keyboard-shortcut', ), - 83754533 => array( + '7e67b544' => array( 'javelin-install', 'javelin-util', 'javelin-dom', 'javelin-vector', ), - '84e6891f' => array( - 'javelin-install', - 'javelin-stratcom', - 'javelin-util', - 'javelin-behavior', - 'javelin-json', + '7fa6d478' => array( 'javelin-dom', - 'javelin-resource', - 'javelin-routable', - ), - '84f82dad' => array( + 'javelin-util', + 'javelin-stratcom', 'javelin-install', + 'javelin-aphlict', + 'javelin-workflow', + 'javelin-router', + 'javelin-behavior-device', + 'javelin-vector', ), - '87428eb2' => array( + '8084fa7b' => array( 'javelin-behavior', - 'javelin-diffusion-locate-file-source', + 'javelin-stratcom', 'javelin-dom', - 'javelin-typeahead', - 'javelin-uri', ), - '876506b6' => array( - 'javelin-view', + 81770869 => array( 'javelin-install', - 'javelin-dom', - ), - '89a1ae3a' => array( - 'javelin-dom', 'javelin-util', 'javelin-stratcom', - 'javelin-install', + 'javelin-dom', + 'javelin-vector', ), - '8ac32fd9' => array( + '81f19452' => array( 'javelin-behavior', 'javelin-stratcom', - 'javelin-workflow', + 'javelin-util', 'javelin-dom', - 'phabricator-draggable-list', + 'javelin-request', + 'phabricator-keyboard-shortcut', + 'phabricator-darklog', + 'phabricator-darkmessage', ), - '8b5c7d65' => array( + '8233f53b' => array( 'javelin-behavior', - 'javelin-stratcom', - 'javelin-dom', - 'phabricator-busy', ), - '8badee71' => array( - 'javelin-install', + '823998c6' => array( + 'javelin-magical-init', 'javelin-util', + ), + '82cb3153' => array( + 'javelin-behavior', 'javelin-dom', - 'javelin-typeahead-normalizer', + 'javelin-util', + 'javelin-request', + 'phabricator-shaped-request', ), - '8c2ed2bf' => array( + '8312ac01' => array( 'javelin-behavior', 'javelin-dom', 'javelin-util', @@ -1724,219 +1614,212 @@ '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( + '8a6d9716' => array( 'javelin-behavior', - 'multirow-row-manager', 'javelin-dom', + 'javelin-stratcom', + 'javelin-workflow', 'javelin-util', - 'phabricator-prefab', - 'javelin-json', + 'phabricator-keyboard-shortcut', ), - '94243d89' => array( - 'javelin-install', + '8bf10492' => array( + 'javelin-behavior', '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', ), - '9f081f05' => array( + '92f45381' => array( 'javelin-behavior', 'javelin-dom', - 'javelin-stratcom', - 'javelin-workflow', - 'javelin-util', - 'phabricator-keyboard-shortcut', ), - 'a17b84f1' => array( + '95a14f58' => array( 'javelin-behavior', 'javelin-dom', - 'javelin-workflow', - ), - 'a241536a' => array( - 'javelin-install', + 'javelin-util', + 'phabricator-shaped-request', ), - 'a2ab19be' => array( + '96f0778d' => array( 'javelin-behavior', 'javelin-dom', 'javelin-util', - 'javelin-stratcom', - 'javelin-workflow', - 'phabricator-draggable-list', + 'multirow-row-manager', + 'javelin-json', + 'phuix-form-control-view', ), - 'a4356cde' => array( - 'javelin-install', + '9729be58' => array( + 'javelin-request', + 'javelin-behavior', 'javelin-dom', - 'javelin-vector', + 'javelin-router', 'javelin-util', + 'phabricator-busy', ), - 'a43ae2ae' => array( - 'javelin-install', - 'javelin-dom', + '98c0c9d1' => array( + 'javelin-behavior', + 'javelin-behavior-device', 'javelin-stratcom', - 'javelin-vector', - ), - 'a4aa75c4' => array( - 'phui-button-css', - 'phui-button-simple-css', + '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', ), - 'aa371860' => array( + 'a0632844' => array( + 'trigger-rule-control', + ), + 'a0b836b0' => array( 'javelin-behavior', 'javelin-stratcom', - 'javelin-workflow', 'javelin-dom', - 'phabricator-draggable-list', ), - 'aa3a100c' => array( + 'a3506522' => array( 'javelin-behavior', 'javelin-dom', - 'javelin-typeahead', - 'javelin-typeahead-ondemand-source', - 'javelin-dom', + 'javelin-uri', + 'javelin-request', ), - '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', @@ -1944,177 +1827,170 @@ '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', - ), - 'b58d1a2a' => array( - 'javelin-behavior', - 'javelin-behavior-device', 'javelin-stratcom', - 'javelin-vector', - 'javelin-dom', - 'javelin-magical-init', + 'javelin-json', + 'phabricator-prefab', ), - 'b5e9bff9' => array( + 'b44cb8be' => array( 'javelin-behavior', - 'javelin-stratcom', 'javelin-dom', + 'javelin-uri', + 'javelin-mask', + 'phabricator-drag-and-drop-file-upload', ), - 'b7b73831' => array( + 'b48f5e2c' => array( 'javelin-behavior', 'javelin-dom', 'javelin-util', - 'phabricator-shaped-request', - ), - 'b86ef6c2' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-stratcom', - 'phabricator-tooltip', - 'phabricator-diff-changeset-list', - 'phabricator-diff-changeset', - 'phuix-formation-view', - ), - 'b86f297f' => array( - 'javelin-behavior', - 'javelin-stratcom', 'javelin-workflow', - 'javelin-dom', - 'phabricator-draggable-list', + 'javelin-stratcom', + 'conpherence-thread-manager', ), - 'b9109f8f' => array( + 'b6ae7ef5' => array( 'javelin-behavior', 'javelin-uri', 'phabricator-notification', ), - 'b9d0c2f3' => array( + 'b6b17353' => array( 'javelin-install', - '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', - ), - 'c2c500a7' => array( + 'b7c340b9' => array( 'javelin-install', 'javelin-dom', - 'phuix-button-view', ), - 'c3703a16' => array( + 'bae4d536' => array( 'javelin-behavior', - 'javelin-aphlict', - 'phabricator-phtize', 'javelin-dom', ), - 'c3d24e63' => array( + 'bb4c7566' => array( 'javelin-install', - 'javelin-workboard-card', - 'javelin-workboard-header', ), - 'c687e867' => array( + 'bc69e075' => array( + 'phui-oi-list-view-css', + ), + 'bcb1469f' => array( 'javelin-behavior', 'javelin-dom', + 'javelin-request', + ), + 'bdac8af0' => array( + 'javelin-behavior', 'javelin-stratcom', - 'javelin-workflow', - 'javelin-fx', - 'javelin-util', + 'javelin-dom', + 'javelin-json', + 'phabricator-draggable-list', ), - 'c68f183f' => array( + 'bf9b167e' => array( + 'phui-button-css', + ), + 'c1810d04' => array( + 'multirow-row-manager', 'javelin-install', + 'path-typeahead', 'javelin-dom', + 'javelin-util', + 'phabricator-prefab', + 'phuix-form-control-view', ), - 'c715c123' => array( + 'c34271ef' => array( + 'javelin-install', + 'javelin-stratcom', + 'javelin-util', 'javelin-behavior', + 'javelin-json', 'javelin-dom', - 'javelin-util', + 'javelin-resource', + 'javelin-routable', + ), + 'c49be7c7' => array( + 'javelin-behavior', + 'javelin-stratcom', 'javelin-workflow', - 'javelin-json', + 'javelin-dom', + 'javelin-router', ), - 'cc2c5de5' => array( - 'javelin-install', - 'phuix-button-view', - 'phabricator-diff-tree-view', + 'c76477e6' => array( + 'herald-rule-editor', + 'javelin-behavior', ), - 'cef53b3e' => array( - 'javelin-install', + 'c829538d' => array( + 'javelin-behavior', + 'javelin-stratcom', + 'javelin-workflow', + 'javelin-dom', + 'phabricator-draggable-list', + ), + 'c9844507' => array( + 'javelin-behavior', + 'javelin-dom', + 'javelin-typeahead', + 'javelin-typeahead-ondemand-source', 'javelin-dom', - 'phuix-formation-column-view', - 'phuix-formation-flank-view', ), - 'cf32921f' => array( + 'cb1ed345' => array( 'javelin-behavior', 'javelin-dom', 'javelin-stratcom', ), - 'd12d214f' => array( - 'javelin-install', + 'cb3fa126' => array( + 'javelin-behavior', + 'javelin-typeahead-ondemand-source', + 'javelin-typeahead', 'javelin-dom', - 'javelin-json', - 'javelin-workflow', + 'javelin-uri', 'javelin-util', + 'javelin-stratcom', + 'phabricator-prefab', + 'phuix-icon-view', ), - 'd3799cb4' => array( + 'cb8ced97' => array( + 'javelin-behavior', + 'javelin-dom', + 'javelin-stratcom', + 'phabricator-tooltip', + 'phabricator-diff-changeset-list', + 'phabricator-diff-changeset', + 'phuix-formation-view', + ), + 'cda4ac41' => array( 'javelin-install', + 'javelin-reactornode', + 'javelin-util', + 'javelin-reactor', ), - 'd4cc2d2a' => array( + 'ce95eb52' => array( 'javelin-install', ), - 'd7d3ba75' => array( + 'd33517b0' => array( 'javelin-dom', 'javelin-util', 'javelin-stratcom', @@ -2128,116 +2004,260 @@ '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', ), - 'dae2d55b' => array( + 'd89b0a1b' => array( + 'javelin-install', + 'javelin-dom', + 'phuix-formation-column-view', + 'phuix-formation-flank-view', + ), + 'd8c8b363' => array( + 'multirow-row-manager', + 'trigger-rule', + ), + '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-vector', + 'javelin-uri', + ), + 'ddcf7994' => array( + 'javelin-install', + 'javelin-util', + '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', ), - 'e5bdb730' => array( + 'e08d87ef' => array( + 'javelin-behavior', + 'javelin-behavior-device', + 'javelin-stratcom', + 'javelin-vector', + 'javelin-dom', + 'javelin-magical-init', + ), + '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( + 'e47cb8ba' => array( + 'phui-chart-css', + 'd3', + 'javelin-chart-curtain-view', + 'javelin-chart-function-label', + ), + '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', + ), + 'e8754d9a' => array( + 'javelin-behavior', + 'javelin-stratcom', + 'javelin-workflow', + 'javelin-dom', + 'phabricator-draggable-list', ), - 'e9c80beb' => array( + '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', + ), + 'f1c80c78' => array( + 'owners-path-editor', + 'javelin-behavior', + ), + 'f20f2119' => array( + 'javelin-install', 'javelin-dom', + 'phabricator-notification', ), - 'f8c4e135' => array( + 'f5aa2659' => array( 'javelin-install', 'javelin-dom', - 'javelin-view-visitor', - 'javelin-util', ), - 'fa6f30b2' => array( + 'f6b9fd3a' => array( + 'phui-workcard-view-css', + ), + 'f96f6131' => array( 'javelin-behavior', 'javelin-dom', + 'phabricator-drag-and-drop-file-upload', + 'phabricator-textareautils', + ), + '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/quickstart.sql b/resources/sql/quickstart.sql index d5fbe601b6..15d9dbbe24 100644 --- a/resources/sql/quickstart.sql +++ b/resources/sql/quickstart.sql @@ -11349,3 +11349,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/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 51fb545c05..92305bb821 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -5285,6 +5285,10 @@ 'PhabricatorUIExample' => 'applications/uiexample/examples/PhabricatorUIExample.php', 'PhabricatorUIExampleRenderController' => 'applications/uiexample/controller/PhabricatorUIExampleRenderController.php', 'PhabricatorUIExamplesApplication' => 'applications/uiexample/application/PhabricatorUIExamplesApplication.php', + 'TimeTracker' => 'extensions/timetracker/components/TimeTracker.php', + 'TimeTrackerRenderController' => 'extensions/timetracker/controller/TimeTrackerRenderController.php', + 'TimeTrackerApplication' => 'extensions/timetracker/application/TimeTrackerApplication.php', + 'TimeTrackerRequestHandler' => 'extensions/timetracker/requesthandlers/TimeTrackerRequestHandler.php', 'PhabricatorURIExportField' => 'infrastructure/export/field/PhabricatorURIExportField.php', 'PhabricatorUSEnglishTranslation' => 'infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php', 'PhabricatorUnifiedDiffsSetting' => 'applications/settings/setting/PhabricatorUnifiedDiffsSetting.php', 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/extensions/timetracker/application/TimeTrackerApplication.php b/src/extensions/timetracker/application/TimeTrackerApplication.php new file mode 100644 index 0000000000..b229d79369 --- /dev/null +++ b/src/extensions/timetracker/application/TimeTrackerApplication.php @@ -0,0 +1,54 @@ + array( + '' => 'TimeTrackerRenderController', + 'view/(?P[^/]+)/' => 'TimeTrackerRenderController', + ), + ); + } + +} diff --git a/src/extensions/timetracker/application/TimeTrackerDAO.php b/src/extensions/timetracker/application/TimeTrackerDAO.php new file mode 100644 index 0000000000..e40bc38de3 --- /dev/null +++ b/src/extensions/timetracker/application/TimeTrackerDAO.php @@ -0,0 +1,8 @@ +getID(); + $numMinutesToTrack = $this->getNumMinutesToTrack($numHours, $numMinutes); + + $timestampWhenTrackedFor = strtotime($year . '-' . $month . '-' . $day); + + $dao = new TimeTrackerDAO(); + $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', + $numMinutesToTrack, time(), $userID, $timestampWhenTrackedFor); + + $dao->saveTransaction(); + unset($guard); + } + + public static function getNumMinutesTrackedToday($user) { + $todayTimestamp = TimeTrackerTimeUtils::getTodayTimestamp(); + $dao = new TimeTrackerDAO(); + $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; + } + + private function getNumMinutesToTrack($numHours, $numMinutes) { + return $numHours * 60 + $numMinutes; + } +} diff --git a/src/extensions/timetracker/application/TimeTrackerTimeUtils.php b/src/extensions/timetracker/application/TimeTrackerTimeUtils.php new file mode 100644 index 0000000000..f00eafe75f --- /dev/null +++ b/src/extensions/timetracker/application/TimeTrackerTimeUtils.php @@ -0,0 +1,33 @@ +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); + } +} \ No newline at end of file diff --git a/src/extensions/timetracker/components/TimeTracker.php b/src/extensions/timetracker/components/TimeTracker.php new file mode 100644 index 0000000000..9b006fdf0e --- /dev/null +++ b/src/extensions/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/extensions/timetracker/components/TimeTrackerMainPanel.php b/src/extensions/timetracker/components/TimeTrackerMainPanel.php new file mode 100644 index 0000000000..6a261e99dd --- /dev/null +++ b/src/extensions/timetracker/components/TimeTrackerMainPanel.php @@ -0,0 +1,68 @@ + + 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.
+ 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)'), + )); + + return phutil_tag_div('ml', array($view, $timeTrackingFormBox)); + } + + private function getTimeTrackingFormBox($user) { + $submit = id(new AphrontFormSubmitControl()); + $submit->setValue(pht('Save')); + + $dateFormComponent = id(new TimeTrackerSelectDateFormComponent()) + ->setUser($user) + ->setLabel(pht('Date (day/month/year):')); + + $form = id(new AphrontFormView()) + ->setUser($this->getRequest()->getUser()) + ->appendChild(id(new AphrontFormTextControl()) + ->setDisableAutocomplete(true) + ->setLabel(pht('Time:')) + ->setName('timeTracked') + ->setValue('')) + ->addHiddenInput('isSent', '1') + ->addHiddenInput('panelType', $this->getPanelType()) + ->appendChild($dateFormComponent) + ->appendChild($submit); + + $box = id(new PHUIObjectBoxView()) + ->setForm($form) + ->setHeader('Track your working time') + ->appendChild(id(new PHUIBoxView())); + + return $box; + } +} diff --git a/src/extensions/timetracker/components/TimeTrackerPanelType.php b/src/extensions/timetracker/components/TimeTrackerPanelType.php new file mode 100644 index 0000000000..67cb0795e6 --- /dev/null +++ b/src/extensions/timetracker/components/TimeTrackerPanelType.php @@ -0,0 +1,7 @@ +monthValue; + } + + private function getDayInputValue() { + return $this->dayValue; + } + + protected function getCustomControlClass() { + return 'aphront-form-control-text'; + } + + protected function renderInput() { + if (!$this->getUser()) { + throw new PhutilInvalidStateException('setUser'); + } + + $currentDay = TimeTrackerTimeUtils::getCurrentDay(); + $currentMonth = TimeTrackerTimeUtils::getCurrentMonth(); + $currentYear = TimeTrackerTimeUtils::getCurrentYear(); + + $amountOfDaysInCurrentMonth = date('t', mktime(0, 0, 0, $currentMonth, 1, $currentYear)); + + $days = range(1, $amountOfDaysInCurrentMonth); + $days = array_fuse($days); + + $months = range($currentMonth, $currentMonth); + $months = array_fuse($months); + + $years = range($currentYear, $currentYear); + $years = array_fuse($years); + + $monthsSelect = AphrontFormSelectControl::renderSelectTag( + $currentMonth, + $months, + array( + 'sigil' => 'month-input', + 'name' => 'month-input', + )); + + $daysSelect = AphrontFormSelectControl::renderSelectTag( + $currentDay, + $days, + array( + 'sigil' => 'day-input', + 'name' => 'day-input', + )); + + $yearsSelect = AphrontFormSelectControl::renderSelectTag( + $currentYear, + $years, + array( + 'sigil' => 'year-input', + 'name' => 'year-input', + )); + + return hsprintf('%s %s %s', $daysSelect, $monthsSelect, $yearsSelect); + } +} diff --git a/src/extensions/timetracker/components/TimeTrackerSummaryPanel.php b/src/extensions/timetracker/components/TimeTrackerSummaryPanel.php new file mode 100644 index 0000000000..5e8353322c --- /dev/null +++ b/src/extensions/timetracker/components/TimeTrackerSummaryPanel.php @@ -0,0 +1,126 @@ + + 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(); + + $elements = array(); + $elements[] = $dateRangeFormBox; + if ($chartBox != null) { + $elements[] = $chartBox; + } + if ($dayDetailsBox != null) { + $elements[] = $dayDetailsBox; + } + return phutil_tag_div('ml', $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')); + + $fromDateInput = (id(new AphrontFormTextControl()) + ->setLabel(pht('From:')) + ->setDisableAutocomplete(true) + ->setName('from') + ->setValue('') + ->setID('datepicker')); + + $toDateInput = (id(new AphrontFormTextControl()) + ->setLabel(pht('To:')) + ->setDisableAutocomplete(true) + ->setName('to') + ->setValue('') + ->setID('datepicker2')); + + $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) + ->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'); + + $content = phutil_safe_html(pht('

+ + + + ', $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 TimeTrackerDayDetailsRequestHandler)) { + return null; + } + + $detailsData = $requestHandler->getDetailsData(); + $totalTrackedTime = $requestHandler->getTotalTrackedTime(); + + $list = new PHUIStatusListView(); + + foreach ($detailsData as $row) { + $list->addItem(id(new PHUIStatusItemView()) + ->setIcon(PHUIStatusItemView::ICON_CLOCK, 'green', pht('')) + ->setTarget(pht($row['numMinutes'])) + ->setNote(pht('tracked ' . $row['realDateWhenTracked']))); + } + + $day = $_GET['day']; + $box = id(new PHUIObjectBoxView()) + ->setHeaderText('Tracked time history for ' . $day) + ->appendChild($list); + + return $box; + } +} diff --git a/src/extensions/timetracker/controller/TimeTrackerRenderController.php b/src/extensions/timetracker/controller/TimeTrackerRenderController.php new file mode 100644 index 0000000000..405927e231 --- /dev/null +++ b/src/extensions/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/extensions/timetracker/requesthandlers/TimeTrackerDayDetailsRequestHandler.php b/src/extensions/timetracker/requesthandlers/TimeTrackerDayDetailsRequestHandler.php new file mode 100644 index 0000000000..a5a05a9c60 --- /dev/null +++ b/src/extensions/timetracker/requesthandlers/TimeTrackerDayDetailsRequestHandler.php @@ -0,0 +1,66 @@ +getTimestampFromInput($request->getStr('day')); + $userID = $request->getUser()->getID(); + + $dao = new TimeTrackerDAO(); + $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', $userID, $dayTimestamp); + + $data = array(); + foreach ($result as $row) { + $this->totalTrackedTime += $row['numMinutes']; + $row['realDateWhenTracked'] = date("Y-m-d H:i:s", $row['realDateWhenTracked']); + $row['numMinutes'] = $this->numMinutesToString($row['numMinutes']); + $data[] = $row; + } + + $this->detailsData = $data; + $this->totalTrackedTime = $this->numMinutesToString($this->totalTrackedTime); + } + + public function getDetailsData() { + return $this->detailsData; + } + + public function getTotalTrackedTime() { + return $this->totalTrackedTime; + } + + 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); + } + + private function numMinutesToString($numMinutes) { + if ($numMinutes < 60) { + return $numMinutes . ' minutes'; + } + + $numHours = floor($numMinutes / 60); + $remainingMinutes = $numMinutes % 60; + + $str = $numHours . ' hours'; + if ($remainingMinutes > 0) { + $str .= ' ' . $remainingMinutes . ' minutes'; + } + return $str; + } +} diff --git a/src/extensions/timetracker/requesthandlers/TimeTrackerMainPanelRequestHandler.php b/src/extensions/timetracker/requesthandlers/TimeTrackerMainPanelRequestHandler.php new file mode 100644 index 0000000000..502d9b1691 --- /dev/null +++ b/src/extensions/timetracker/requesthandlers/TimeTrackerMainPanelRequestHandler.php @@ -0,0 +1,109 @@ +getStr('isSent') == '1'; + if ($isSent) { + $correctRequest = $this->parseTrackTimeRequest($request); + + if (!$correctRequest) { + echo 'incorrect request'; + } + else { + $day = $request->getStr('day-input'); + $month = $request->getStr('month-input'); + $year = $request->getStr('year-input'); + + $manager = new TimeTrackerStorageManager(); + $manager->trackTime($request->getUser(), $this->numHours, $this->numMinutes, $day, $month, $year); + } + } + } + + 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; + + $correctInput = true; + if ($isRange) { + $correctInput = $this->parseRange($timeTracked); + } + else { + $correctInput = $this->parseSingleTimeInput($timeTracked, $hasMinutes, $hasHours, $isNegative); + } + + $numMinutesToTrack = $this->numMinutes + $this->numHours * 60; + $numMinutesAlreadyTrackedToday = TimeTrackerStorageManager::getNumMinutesTrackedToday($request->getUser()); + + if ($numMinutesAlreadyTrackedToday + $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; + } + + public function getNumMinutes() { + return $this->numMinutes; + } + + public function getNumHours() { + return $this->numHours; + } +} diff --git a/src/extensions/timetracker/requesthandlers/TimeTrackerRequestHandler.php b/src/extensions/timetracker/requesthandlers/TimeTrackerRequestHandler.php new file mode 100644 index 0000000000..8a964ba67f --- /dev/null +++ b/src/extensions/timetracker/requesthandlers/TimeTrackerRequestHandler.php @@ -0,0 +1,5 @@ +getStr('isSent') == '1'; + if ($isSent) { + $fromTimestamp = $this->getTimestampFromInput($request->getStr('from')); + $toTimestamp = $this->getTimestampFromInput($request->getStr('to')); + $userID = $request->getUser()->getID(); + + $dao = new TimeTrackerDAO(); + $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; + } + + 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/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 0000000000000000000000000000000000000000..3d1b5f45d8ff39c5f06903decbe13876f16dd476 GIT binary patch literal 7090 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..6d7593c973cde2fdbb37252ce5fc7daf9b895a94 GIT binary patch literal 7074 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 dY3Jf(;$-I-{76d^ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..fede5688dbd366d3838dd29ec81ada387b3c3aeb GIT binary patch literal 7111 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 MidqU)a+VSQ2XSFRjsO4v literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..e23c3e3b4cb684c77dd4931057b80f3942ae9882 GIT binary patch literal 4618 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..fb914919feac46f0a06fe2b843ebee9931084027 GIT binary patch literal 6487 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 literal 0 HcmV?d00001 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/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("