diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 0117dc7801..ef1f97f1c7 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -1,2236 +1,2238 @@ array( - 'core.pkg.css' => '68d4f4fb', + 'core.pkg.css' => 'e2460e8f', 'core.pkg.js' => '3bbe23c6', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => '30602b8c', 'differential.pkg.js' => 'ebef29b1', 'diffusion.pkg.css' => '591664fa', 'diffusion.pkg.js' => '0115b37c', 'maniphest.pkg.css' => '68d4dd3d', 'maniphest.pkg.js' => '2f4f52c2', 'rsrc/css/aphront/aphront-bars.css' => '231ac33c', 'rsrc/css/aphront/dark-console.css' => '6378ef3d', 'rsrc/css/aphront/dialog-view.css' => '9b32db0a', 'rsrc/css/aphront/lightbox-attachment.css' => '7acac05d', 'rsrc/css/aphront/list-filter-view.css' => 'b2161041', 'rsrc/css/aphront/multi-column.css' => 'fd18389d', 'rsrc/css/aphront/notification.css' => '9c279160', 'rsrc/css/aphront/pager-view.css' => '2e3539af', 'rsrc/css/aphront/panel-view.css' => '8427b78d', 'rsrc/css/aphront/phabricator-nav-view.css' => '7aeaf435', 'rsrc/css/aphront/table-view.css' => '0b4cd283', 'rsrc/css/aphront/tokenizer.css' => '86a13f7f', 'rsrc/css/aphront/tooltip.css' => '7672b60f', 'rsrc/css/aphront/two-column.css' => '16ab3ad2', 'rsrc/css/aphront/typeahead-browse.css' => 'd8581d2c', 'rsrc/css/aphront/typeahead.css' => '0e403212', 'rsrc/css/application/almanac/almanac.css' => 'dbb9b3af', 'rsrc/css/application/auth/auth.css' => '44975d4b', 'rsrc/css/application/base/main-menu-view.css' => '663e3810', 'rsrc/css/application/base/notification-menu.css' => '3c9d8aa1', 'rsrc/css/application/base/phabricator-application-launch-view.css' => '16ca323f', 'rsrc/css/application/base/standard-page-view.css' => '61e68a55', 'rsrc/css/application/calendar/calendar-icon.css' => '98ce946d', 'rsrc/css/application/chatlog/chatlog.css' => '852140ff', 'rsrc/css/application/conduit/conduit-api.css' => '7bc725c4', 'rsrc/css/application/config/config-options.css' => '7fedf08b', 'rsrc/css/application/config/config-template.css' => '8e6c6fcd', 'rsrc/css/application/config/config-welcome.css' => '6abd79be', 'rsrc/css/application/config/setup-issue.css' => '22270af2', 'rsrc/css/application/config/unhandled-exception.css' => '37d4f9a2', 'rsrc/css/application/conpherence/durable-column.css' => '4331cbe9', 'rsrc/css/application/conpherence/menu.css' => 'f389e048', 'rsrc/css/application/conpherence/message-pane.css' => '5bb4b76d', 'rsrc/css/application/conpherence/notification.css' => '919974b6', 'rsrc/css/application/conpherence/transaction.css' => '42a457f6', 'rsrc/css/application/conpherence/update.css' => '1099a660', 'rsrc/css/application/conpherence/widget-pane.css' => '2af42ebe', 'rsrc/css/application/contentsource/content-source-view.css' => '4b8b05d4', 'rsrc/css/application/countdown/timer.css' => '86b7b0a0', 'rsrc/css/application/dashboard/dashboard.css' => '17937d22', 'rsrc/css/application/diff/inline-comment-summary.css' => 'eb5f8e8c', 'rsrc/css/application/differential/add-comment.css' => 'c47f8c40', 'rsrc/css/application/differential/changeset-view.css' => 'e19cfd6e', 'rsrc/css/application/differential/core.css' => '7ac3cabc', 'rsrc/css/application/differential/phui-inline-comment.css' => 'aa16f165', 'rsrc/css/application/differential/results-table.css' => '181aa9d9', 'rsrc/css/application/differential/revision-comment.css' => '14b8565a', 'rsrc/css/application/differential/revision-history.css' => '0e8eb855', 'rsrc/css/application/differential/revision-list.css' => 'f3c47d33', 'rsrc/css/application/differential/table-of-contents.css' => '63f3ef4a', 'rsrc/css/application/diffusion/diffusion-icons.css' => '9c5828da', 'rsrc/css/application/diffusion/diffusion-readme.css' => '2106ea08', 'rsrc/css/application/diffusion/diffusion-source.css' => '66fdf661', 'rsrc/css/application/feed/feed.css' => 'b513b5f4', 'rsrc/css/application/files/global-drag-and-drop.css' => '697324ad', 'rsrc/css/application/flag/flag.css' => '5337623f', 'rsrc/css/application/harbormaster/harbormaster.css' => '49d64eb4', 'rsrc/css/application/herald/herald-test.css' => '778b008e', 'rsrc/css/application/herald/herald.css' => '826075fa', 'rsrc/css/application/home/home.css' => 'e34bf140', 'rsrc/css/application/maniphest/batch-editor.css' => '8f380ebc', 'rsrc/css/application/maniphest/report.css' => 'f6931fdf', 'rsrc/css/application/maniphest/task-edit.css' => '8e23031b', 'rsrc/css/application/maniphest/task-summary.css' => 'ab2fc691', 'rsrc/css/application/objectselector/object-selector.css' => '029a133d', 'rsrc/css/application/owners/owners-path-editor.css' => '2f00933b', 'rsrc/css/application/paste/paste.css' => 'eb997ddd', 'rsrc/css/application/people/people-profile.css' => '25970776', 'rsrc/css/application/phame/phame.css' => '88bd4705', 'rsrc/css/application/pholio/pholio-edit.css' => '3ad9d1ee', 'rsrc/css/application/pholio/pholio-inline-comments.css' => '8e545e49', 'rsrc/css/application/pholio/pholio.css' => '95174bdd', 'rsrc/css/application/phortune/phortune-credit-card-form.css' => '8391eb02', 'rsrc/css/application/phortune/phortune.css' => '9149f103', 'rsrc/css/application/phrequent/phrequent.css' => 'ffc185ad', 'rsrc/css/application/phriction/phriction-document-css.css' => '0d16bc9a', 'rsrc/css/application/policy/policy-edit.css' => '815c66f7', 'rsrc/css/application/policy/policy-transaction-detail.css' => '82100a43', 'rsrc/css/application/policy/policy.css' => '957ea14c', 'rsrc/css/application/ponder/comments.css' => '6cdccea7', 'rsrc/css/application/ponder/feed.css' => 'e62615b6', 'rsrc/css/application/ponder/post.css' => '9d415218', 'rsrc/css/application/ponder/vote.css' => '8ed6ed8b', 'rsrc/css/application/profile/profile-view.css' => '1a20dcbf', 'rsrc/css/application/projects/project-icon.css' => 'c2ecb7f1', 'rsrc/css/application/releeph/releeph-core.css' => '9b3c5733', 'rsrc/css/application/releeph/releeph-preview-branch.css' => 'b7a6f4a5', 'rsrc/css/application/releeph/releeph-request-differential-create-dialog.css' => '8d8b92cd', 'rsrc/css/application/releeph/releeph-request-typeahead.css' => '667a48ae', 'rsrc/css/application/search/search-results.css' => '15c71110', 'rsrc/css/application/slowvote/slowvote.css' => '266df6a1', 'rsrc/css/application/tokens/tokens.css' => '3d0f239e', 'rsrc/css/application/uiexample/example.css' => '528b19de', 'rsrc/css/core/core.css' => 'aaea7a7a', 'rsrc/css/core/remarkup.css' => '07b7dc54', 'rsrc/css/core/syntax.css' => '9fd11da8', 'rsrc/css/core/z-index.css' => 'c4732d32', 'rsrc/css/diviner/diviner-shared.css' => '38813222', 'rsrc/css/font/font-awesome.css' => 'e2e712fe', 'rsrc/css/font/font-source-sans-pro.css' => '8906c07b', 'rsrc/css/font/phui-font-icon-base.css' => '3dad2ae3', 'rsrc/css/layout/phabricator-filetree-view.css' => 'fccf9f82', 'rsrc/css/layout/phabricator-hovercard-view.css' => 'dd9121a9', 'rsrc/css/layout/phabricator-side-menu-view.css' => 'c1db9e9c', 'rsrc/css/layout/phabricator-source-code-view.css' => '2ceee894', 'rsrc/css/phui/calendar/phui-calendar-day.css' => 'd1cf6f93', 'rsrc/css/phui/calendar/phui-calendar-list.css' => 'c1c7f338', 'rsrc/css/phui/calendar/phui-calendar-month.css' => '476be7e0', 'rsrc/css/phui/calendar/phui-calendar.css' => 'ccabe893', 'rsrc/css/phui/phui-action-header-view.css' => '89c497e7', 'rsrc/css/phui/phui-action-list.css' => '4f4d09f2', 'rsrc/css/phui/phui-action-panel.css' => '3ee9afd5', 'rsrc/css/phui/phui-box.css' => '7b3a2eed', 'rsrc/css/phui/phui-button.css' => 'de610129', 'rsrc/css/phui/phui-crumbs-view.css' => '594d719e', 'rsrc/css/phui/phui-document.css' => '94d5dcd8', 'rsrc/css/phui/phui-feed-story.css' => 'c9f3a0b5', 'rsrc/css/phui/phui-fontkit.css' => 'dd8ddf27', 'rsrc/css/phui/phui-form-view.css' => '808329f2', - 'rsrc/css/phui/phui-form.css' => 'f535f938', + 'rsrc/css/phui/phui-form.css' => '25876baf', 'rsrc/css/phui/phui-header-view.css' => '75aaf372', 'rsrc/css/phui/phui-icon.css' => 'bc766998', 'rsrc/css/phui/phui-image-mask.css' => '5a8b09c8', 'rsrc/css/phui/phui-info-panel.css' => '27ea50a1', 'rsrc/css/phui/phui-info-view.css' => 'c6f0aef8', 'rsrc/css/phui/phui-list.css' => '2e25ebfb', 'rsrc/css/phui/phui-object-box.css' => '7d160002', 'rsrc/css/phui/phui-object-item-list-view.css' => 'f3a22696', 'rsrc/css/phui/phui-pinboard-view.css' => 'eaab2b1b', 'rsrc/css/phui/phui-property-list-view.css' => '5b671934', 'rsrc/css/phui/phui-remarkup-preview.css' => '19ad512b', 'rsrc/css/phui/phui-spacing.css' => '042804d6', 'rsrc/css/phui/phui-status.css' => '888cedb8', 'rsrc/css/phui/phui-tag-view.css' => '402691cc', 'rsrc/css/phui/phui-text.css' => 'cf019f54', 'rsrc/css/phui/phui-timeline-view.css' => 'a85542c8', 'rsrc/css/phui/phui-workboard-view.css' => '5c4c53e9', 'rsrc/css/phui/phui-workpanel-view.css' => 'e495a5cc', 'rsrc/css/sprite-gradient.css' => '4bdb98a7', 'rsrc/css/sprite-login.css' => 'a3526809', 'rsrc/css/sprite-main-header.css' => '28d01b0b', 'rsrc/css/sprite-menu.css' => '9ef76324', 'rsrc/css/sprite-projects.css' => 'b0d9e24f', 'rsrc/css/sprite-tokens.css' => '1706b943', 'rsrc/externals/font/fontawesome/fontawesome-webfont.eot' => '5fb6fb0e', 'rsrc/externals/font/fontawesome/fontawesome-webfont.ttf' => 'a653cb11', 'rsrc/externals/font/fontawesome/fontawesome-webfont.woff' => '80526fc8', 'rsrc/externals/font/fontawesome/fontawesome-webfont.woff2' => '4924d54d', 'rsrc/externals/font/sourcesans/SourceSansPro-It.woff' => '3f21af52', 'rsrc/externals/font/sourcesans/SourceSansPro-It.woff2' => '30a7cf60', 'rsrc/externals/font/sourcesans/SourceSansPro-Regular.woff2' => 'e89b04b1', 'rsrc/externals/font/sourcesans/SourceSansPro-Semibold.woff' => '67cce9ea', 'rsrc/externals/font/sourcesans/SourceSansPro-Semibold.woff2' => 'ec2ed916', 'rsrc/externals/font/sourcesans/SourceSansPro-SemiboldIt.woff' => 'bd1ce81d', 'rsrc/externals/font/sourcesans/SourceSansPro-SemiboldIt.woff2' => 'd9928416', 'rsrc/externals/font/sourcesans/SourceSansPro.woff' => '3614608c', 'rsrc/externals/javelin/core/Event.js' => '85ea0626', 'rsrc/externals/javelin/core/Stratcom.js' => '6c53634d', 'rsrc/externals/javelin/core/__tests__/event-stop-and-kill.js' => '717554e4', 'rsrc/externals/javelin/core/__tests__/install.js' => 'c432ee85', 'rsrc/externals/javelin/core/__tests__/stratcom.js' => '88bf7313', 'rsrc/externals/javelin/core/__tests__/util.js' => 'e251703d', 'rsrc/externals/javelin/core/init.js' => '3010e992', 'rsrc/externals/javelin/core/init_node.js' => 'c234aded', 'rsrc/externals/javelin/core/install.js' => '05270951', 'rsrc/externals/javelin/core/util.js' => '93cc50d6', 'rsrc/externals/javelin/docs/Base.js' => '74676256', 'rsrc/externals/javelin/docs/onload.js' => 'e819c479', 'rsrc/externals/javelin/ext/fx/Color.js' => '7e41274a', 'rsrc/externals/javelin/ext/fx/FX.js' => '54b612ba', 'rsrc/externals/javelin/ext/reactor/core/DynVal.js' => 'f6555212', 'rsrc/externals/javelin/ext/reactor/core/Reactor.js' => '2b8de964', 'rsrc/externals/javelin/ext/reactor/core/ReactorNode.js' => '1ad0a787', 'rsrc/externals/javelin/ext/reactor/core/ReactorNodeCalmer.js' => '76f4ebed', 'rsrc/externals/javelin/ext/reactor/dom/RDOM.js' => 'c90a04fc', 'rsrc/externals/javelin/ext/view/HTMLView.js' => 'fe287620', 'rsrc/externals/javelin/ext/view/View.js' => '0f764c35', 'rsrc/externals/javelin/ext/view/ViewInterpreter.js' => 'f829edb3', 'rsrc/externals/javelin/ext/view/ViewPlaceholder.js' => '47830651', 'rsrc/externals/javelin/ext/view/ViewRenderer.js' => '6c2b09a2', 'rsrc/externals/javelin/ext/view/ViewVisitor.js' => 'efe49472', 'rsrc/externals/javelin/ext/view/__tests__/HTMLView.js' => 'f92d7bcb', 'rsrc/externals/javelin/ext/view/__tests__/View.js' => '6450b38b', 'rsrc/externals/javelin/ext/view/__tests__/ViewInterpreter.js' => '7a94d6a5', 'rsrc/externals/javelin/ext/view/__tests__/ViewRenderer.js' => '6ea96ac9', 'rsrc/externals/javelin/lib/Cookie.js' => '62dfea03', 'rsrc/externals/javelin/lib/DOM.js' => '147805fa', 'rsrc/externals/javelin/lib/History.js' => 'd4505101', 'rsrc/externals/javelin/lib/JSON.js' => '69adf288', 'rsrc/externals/javelin/lib/Leader.js' => '331b1611', 'rsrc/externals/javelin/lib/Mask.js' => '8a41885b', 'rsrc/externals/javelin/lib/Quicksand.js' => '4cebc641', 'rsrc/externals/javelin/lib/Request.js' => '94b750d2', 'rsrc/externals/javelin/lib/Resource.js' => '44959b73', 'rsrc/externals/javelin/lib/Routable.js' => 'b3e7d692', 'rsrc/externals/javelin/lib/Router.js' => '29274e2b', 'rsrc/externals/javelin/lib/Scrollbar.js' => '087e919c', 'rsrc/externals/javelin/lib/Sound.js' => '949c0fe5', 'rsrc/externals/javelin/lib/URI.js' => '6eff08aa', 'rsrc/externals/javelin/lib/Vector.js' => '2caa8fb8', 'rsrc/externals/javelin/lib/WebSocket.js' => 'e292eaf4', 'rsrc/externals/javelin/lib/Workflow.js' => '5b2e3e2b', 'rsrc/externals/javelin/lib/__tests__/Cookie.js' => '5ed109e8', 'rsrc/externals/javelin/lib/__tests__/DOM.js' => 'c984504b', 'rsrc/externals/javelin/lib/__tests__/JSON.js' => '837a7d68', 'rsrc/externals/javelin/lib/__tests__/URI.js' => '1e45fda9', 'rsrc/externals/javelin/lib/__tests__/behavior.js' => '1ea62783', 'rsrc/externals/javelin/lib/behavior.js' => '61cbc29a', 'rsrc/externals/javelin/lib/control/tokenizer/Tokenizer.js' => 'ab5f468d', 'rsrc/externals/javelin/lib/control/typeahead/Typeahead.js' => '70baed2f', 'rsrc/externals/javelin/lib/control/typeahead/normalizer/TypeaheadNormalizer.js' => 'e6e25838', 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadCompositeSource.js' => '503e17fd', 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadOnDemandSource.js' => '8b3fd187', 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadPreloadedSource.js' => '54f314a0', 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadSource.js' => '2818f5ce', 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadStaticSource.js' => '6c0e62fa', 'rsrc/externals/raphael/g.raphael.js' => '40dde778', 'rsrc/externals/raphael/g.raphael.line.js' => '40da039e', 'rsrc/externals/raphael/raphael.js' => '51ee6b43', 'rsrc/favicons/apple-touch-icon-120x120.png' => '43742962', 'rsrc/favicons/apple-touch-icon-152x152.png' => '669eaec3', 'rsrc/favicons/apple-touch-icon-76x76.png' => 'ecdef672', 'rsrc/favicons/favicon-128.png' => '47cdff03', 'rsrc/favicons/favicon-16x16.png' => 'ee2523ac', 'rsrc/favicons/favicon-32x32.png' => 'b6a8150e', 'rsrc/favicons/favicon-96x96.png' => '8f7ea177', 'rsrc/image/BFCFDA.png' => 'd5ec91f4', 'rsrc/image/actions/edit.png' => '2fc41442', 'rsrc/image/avatar.png' => '3eb28cd9', 'rsrc/image/checker_dark.png' => 'd8e65881', 'rsrc/image/checker_light.png' => 'a0155918', 'rsrc/image/checker_lighter.png' => 'd5da91b6', 'rsrc/image/darkload.gif' => '1ffd3ec6', 'rsrc/image/divot.png' => '94dded62', 'rsrc/image/examples/hero.png' => '979a86ae', 'rsrc/image/grippy_texture.png' => 'aca81e2f', 'rsrc/image/icon/fatcow/arrow_branch.png' => '2537c01c', 'rsrc/image/icon/fatcow/arrow_merge.png' => '21b660e0', 'rsrc/image/icon/fatcow/bullet_black.png' => 'ff190031', 'rsrc/image/icon/fatcow/bullet_orange.png' => 'e273e5bb', 'rsrc/image/icon/fatcow/bullet_red.png' => 'c0b75434', 'rsrc/image/icon/fatcow/calendar_edit.png' => '24632275', 'rsrc/image/icon/fatcow/document_black.png' => '45fe1c60', 'rsrc/image/icon/fatcow/flag_blue.png' => 'a01abb1d', 'rsrc/image/icon/fatcow/flag_finish.png' => '67825cee', 'rsrc/image/icon/fatcow/flag_ghost.png' => '20ca8783', 'rsrc/image/icon/fatcow/flag_green.png' => '7e0eaa7a', 'rsrc/image/icon/fatcow/flag_orange.png' => '9e73df66', 'rsrc/image/icon/fatcow/flag_pink.png' => '7e92f3b2', 'rsrc/image/icon/fatcow/flag_purple.png' => 'cc517522', 'rsrc/image/icon/fatcow/flag_red.png' => '04ec726f', 'rsrc/image/icon/fatcow/flag_yellow.png' => '73946fd4', 'rsrc/image/icon/fatcow/folder.png' => '95a435af', 'rsrc/image/icon/fatcow/folder_go.png' => '001cbc94', 'rsrc/image/icon/fatcow/key_question.png' => '52a0c26a', 'rsrc/image/icon/fatcow/link.png' => '7afd4d5e', 'rsrc/image/icon/fatcow/page_white_edit.png' => '39a2eed8', 'rsrc/image/icon/fatcow/page_white_link.png' => 'a90023c7', 'rsrc/image/icon/fatcow/page_white_put.png' => '08c95a0c', 'rsrc/image/icon/fatcow/page_white_text.png' => '1e1f79c3', 'rsrc/image/icon/fatcow/source/conduit.png' => '4ea01d2f', 'rsrc/image/icon/fatcow/source/email.png' => '9bab3239', 'rsrc/image/icon/fatcow/source/fax.png' => '04195e68', 'rsrc/image/icon/fatcow/source/mobile.png' => 'f1321264', 'rsrc/image/icon/fatcow/source/tablet.png' => '49396799', 'rsrc/image/icon/fatcow/source/web.png' => '136ccb5d', 'rsrc/image/icon/lightbox/close-2.png' => 'cc40e7c8', 'rsrc/image/icon/lightbox/close-hover-2.png' => 'fb5d6d9e', 'rsrc/image/icon/lightbox/left-arrow-2.png' => '8426133b', 'rsrc/image/icon/lightbox/left-arrow-hover-2.png' => '701e5ee3', 'rsrc/image/icon/lightbox/right-arrow-2.png' => '6d5519a0', 'rsrc/image/icon/lightbox/right-arrow-hover-2.png' => '3a04aa21', 'rsrc/image/icon/subscribe.png' => 'd03ed5a5', 'rsrc/image/icon/tango/attachment.png' => 'ecc8022e', 'rsrc/image/icon/tango/edit.png' => '929a1363', 'rsrc/image/icon/tango/go-down.png' => '96d95e43', 'rsrc/image/icon/tango/log.png' => 'b08cc63a', 'rsrc/image/icon/tango/upload.png' => '7bbb7984', 'rsrc/image/icon/unsubscribe.png' => '25725013', 'rsrc/image/lightblue-header.png' => '5c168b6d', 'rsrc/image/main_texture.png' => '29a2c5ad', 'rsrc/image/menu_texture.png' => '5a17580d', 'rsrc/image/people/harding.png' => '45aa614e', 'rsrc/image/people/jefferson.png' => 'afca0e53', 'rsrc/image/people/lincoln.png' => '9369126d', 'rsrc/image/people/mckinley.png' => 'fb8f16ce', 'rsrc/image/people/taft.png' => 'd7bc402c', 'rsrc/image/people/washington.png' => '40dd301c', 'rsrc/image/phrequent_active.png' => 'a466a8ed', 'rsrc/image/phrequent_inactive.png' => 'bfc15a69', 'rsrc/image/sprite-gradient.png' => 'ec15a417', 'rsrc/image/sprite-login-X2.png' => 'a15918f0', 'rsrc/image/sprite-login.png' => '8cee4f6e', 'rsrc/image/sprite-main-header.png' => '39419fa6', 'rsrc/image/sprite-menu-X2.png' => '1c90d7bc', 'rsrc/image/sprite-menu.png' => '619781ee', 'rsrc/image/sprite-projects-X2.png' => '8c91c839', 'rsrc/image/sprite-projects.png' => 'ef9dc9b5', 'rsrc/image/sprite-tokens-X2.png' => 'b4776580', 'rsrc/image/sprite-tokens.png' => '25b75533', 'rsrc/image/texture/card-gradient.png' => '815f26e8', 'rsrc/image/texture/dark-menu-hover.png' => '5fa7ece8', 'rsrc/image/texture/dark-menu.png' => '7e22296e', 'rsrc/image/texture/grip.png' => '719404f3', 'rsrc/image/texture/panel-header-gradient.png' => 'e3b8dcfe', 'rsrc/image/texture/phlnx-bg.png' => '8d819209', 'rsrc/image/texture/pholio-background.gif' => 'ba29239c', 'rsrc/image/texture/table_header.png' => '5c433037', 'rsrc/image/texture/table_header_hover.png' => '038ec3b9', 'rsrc/image/texture/table_header_tall.png' => 'd56b434f', 'rsrc/js/application/aphlict/Aphlict.js' => '5359e785', 'rsrc/js/application/aphlict/behavior-aphlict-dropdown.js' => '995ad707', 'rsrc/js/application/aphlict/behavior-aphlict-listen.js' => 'b1a59974', 'rsrc/js/application/aphlict/behavior-aphlict-status.js' => 'ea681761', 'rsrc/js/application/auth/behavior-persona-login.js' => '9414ff18', 'rsrc/js/application/calendar/behavior-day-view.js' => '5c46cff2', 'rsrc/js/application/calendar/behavior-event-all-day.js' => '38dcf3c8', + 'rsrc/js/application/calendar/behavior-recurring-edit.js' => '85c73ceb', 'rsrc/js/application/config/behavior-reorder-fields.js' => 'b6993408', 'rsrc/js/application/conpherence/ConpherenceThreadManager.js' => '01774ab2', 'rsrc/js/application/conpherence/behavior-drag-and-drop-photo.js' => 'cf86d16a', 'rsrc/js/application/conpherence/behavior-durable-column.js' => '16c695bf', 'rsrc/js/application/conpherence/behavior-menu.js' => '43b24a10', 'rsrc/js/application/conpherence/behavior-pontificate.js' => '21ba5861', 'rsrc/js/application/conpherence/behavior-quicksand-blacklist.js' => '7927a7d3', 'rsrc/js/application/conpherence/behavior-widget-pane.js' => '93568464', 'rsrc/js/application/countdown/timer.js' => 'e4cc26b3', 'rsrc/js/application/dashboard/behavior-dashboard-async-panel.js' => '469c0d9e', 'rsrc/js/application/dashboard/behavior-dashboard-move-panels.js' => '82439934', 'rsrc/js/application/dashboard/behavior-dashboard-query-panel-select.js' => '453c5375', 'rsrc/js/application/dashboard/behavior-dashboard-tab-panel.js' => 'd4eecc63', 'rsrc/js/application/differential/ChangesetViewManager.js' => '58562350', 'rsrc/js/application/differential/DifferentialInlineCommentEditor.js' => 'd4c87bf4', 'rsrc/js/application/differential/behavior-add-reviewers-and-ccs.js' => 'e10f8e18', 'rsrc/js/application/differential/behavior-comment-jump.js' => '4fdb476d', 'rsrc/js/application/differential/behavior-comment-preview.js' => 'b064af76', 'rsrc/js/application/differential/behavior-diff-radios.js' => 'e1ff79b1', 'rsrc/js/application/differential/behavior-dropdown-menus.js' => '2035b9cb', 'rsrc/js/application/differential/behavior-edit-inline-comments.js' => '037b59eb', 'rsrc/js/application/differential/behavior-keyboard-nav.js' => '2c426492', 'rsrc/js/application/differential/behavior-populate.js' => '8694b1df', 'rsrc/js/application/differential/behavior-show-field-details.js' => 'bba9eedf', 'rsrc/js/application/differential/behavior-toggle-files.js' => 'ca3f91eb', 'rsrc/js/application/differential/behavior-user-select.js' => 'a8d8459d', 'rsrc/js/application/diffusion/DiffusionLocateFileSource.js' => 'b42eddc7', 'rsrc/js/application/diffusion/behavior-audit-preview.js' => 'd835b03a', 'rsrc/js/application/diffusion/behavior-commit-branches.js' => 'bdaf4d04', 'rsrc/js/application/diffusion/behavior-commit-graph.js' => '9007c197', 'rsrc/js/application/diffusion/behavior-jump-to.js' => '73d09eef', 'rsrc/js/application/diffusion/behavior-load-blame.js' => '42126667', 'rsrc/js/application/diffusion/behavior-locate-file.js' => '6d3e1947', 'rsrc/js/application/diffusion/behavior-pull-lastmodified.js' => '2b228192', 'rsrc/js/application/doorkeeper/behavior-doorkeeper-tag.js' => 'e5822781', 'rsrc/js/application/files/behavior-icon-composer.js' => '8ef9ab58', 'rsrc/js/application/files/behavior-launch-icon-composer.js' => '48086888', 'rsrc/js/application/herald/HeraldRuleEditor.js' => '271ffdd7', 'rsrc/js/application/herald/PathTypeahead.js' => 'f7fc67ec', 'rsrc/js/application/herald/herald-rule-editor.js' => '7ebaeed3', 'rsrc/js/application/maniphest/behavior-batch-editor.js' => 'f5d1233b', 'rsrc/js/application/maniphest/behavior-batch-selector.js' => '7b98d7c5', 'rsrc/js/application/maniphest/behavior-line-chart.js' => '88f0c5b3', 'rsrc/js/application/maniphest/behavior-list-edit.js' => 'a9f88de2', 'rsrc/js/application/maniphest/behavior-subpriorityeditor.js' => '84845b5b', 'rsrc/js/application/maniphest/behavior-transaction-controls.js' => '44168bad', 'rsrc/js/application/maniphest/behavior-transaction-expand.js' => '5fefb143', 'rsrc/js/application/maniphest/behavior-transaction-preview.js' => '4c95d29e', 'rsrc/js/application/owners/OwnersPathEditor.js' => 'aa1733d0', 'rsrc/js/application/owners/owners-path-editor.js' => '7a68dda3', 'rsrc/js/application/passphrase/passphrase-credential-control.js' => '3cb0b2fc', 'rsrc/js/application/phame/phame-post-preview.js' => 'be807912', 'rsrc/js/application/pholio/behavior-pholio-mock-edit.js' => '246dc085', 'rsrc/js/application/pholio/behavior-pholio-mock-view.js' => 'fbe497e7', 'rsrc/js/application/phortune/behavior-stripe-payment-form.js' => '3f5d6dbf', 'rsrc/js/application/phortune/behavior-test-payment-form.js' => 'fc91ab6c', 'rsrc/js/application/phortune/phortune-credit-card-form.js' => '2290aeef', 'rsrc/js/application/policy/behavior-policy-control.js' => '9a340b3d', 'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '5e9f347c', 'rsrc/js/application/ponder/behavior-votebox.js' => '4e9b766b', 'rsrc/js/application/projects/behavior-project-boards.js' => 'ba4fa35c', 'rsrc/js/application/projects/behavior-project-create.js' => '065227cc', 'rsrc/js/application/projects/behavior-reorder-columns.js' => 'e1d25dfb', 'rsrc/js/application/releeph/releeph-preview-branch.js' => 'b2b4fbaf', 'rsrc/js/application/releeph/releeph-request-state-change.js' => 'a0b57eb8', 'rsrc/js/application/releeph/releeph-request-typeahead.js' => 'de2e896f', 'rsrc/js/application/repository/repository-crossreference.js' => 'bea81850', 'rsrc/js/application/search/behavior-reorder-queries.js' => 'e9581f08', 'rsrc/js/application/slowvote/behavior-slowvote-embed.js' => '887ad43f', 'rsrc/js/application/transactions/behavior-show-older-transactions.js' => 'dbbf48b6', 'rsrc/js/application/transactions/behavior-transaction-comment-form.js' => 'b23b49e6', 'rsrc/js/application/transactions/behavior-transaction-list.js' => '13c739ea', 'rsrc/js/application/typeahead/behavior-typeahead-browse.js' => '635de1ec', 'rsrc/js/application/typeahead/behavior-typeahead-search.js' => '93d0c9e3', 'rsrc/js/application/uiexample/JavelinViewExample.js' => 'd4a14807', 'rsrc/js/application/uiexample/ReactorButtonExample.js' => 'd19198c8', 'rsrc/js/application/uiexample/ReactorCheckboxExample.js' => '519705ea', 'rsrc/js/application/uiexample/ReactorFocusExample.js' => '40a6a403', 'rsrc/js/application/uiexample/ReactorInputExample.js' => '886fd850', 'rsrc/js/application/uiexample/ReactorMouseoverExample.js' => '47c794d8', 'rsrc/js/application/uiexample/ReactorRadioExample.js' => '988040b4', 'rsrc/js/application/uiexample/ReactorSelectExample.js' => 'a155550f', 'rsrc/js/application/uiexample/ReactorSendClassExample.js' => '1def2711', 'rsrc/js/application/uiexample/ReactorSendPropertiesExample.js' => 'b1f0ccee', 'rsrc/js/application/uiexample/busy-example.js' => '60479091', 'rsrc/js/application/uiexample/gesture-example.js' => '558829c2', 'rsrc/js/application/uiexample/notification-example.js' => '8ce821c5', 'rsrc/js/core/Busy.js' => '59a7976a', 'rsrc/js/core/DragAndDropFileUpload.js' => '07de8873', 'rsrc/js/core/DraggableList.js' => 'a16ec1c6', 'rsrc/js/core/FileUpload.js' => '477359c8', 'rsrc/js/core/Hovercard.js' => '14ac66f5', 'rsrc/js/core/KeyboardShortcut.js' => '1ae869f2', 'rsrc/js/core/KeyboardShortcutManager.js' => 'c1700f6f', 'rsrc/js/core/MultirowRowManager.js' => 'b5d57730', 'rsrc/js/core/Notification.js' => '0c6946e7', 'rsrc/js/core/Prefab.js' => '6920d200', 'rsrc/js/core/ShapedRequest.js' => '7cbe244b', 'rsrc/js/core/TextAreaUtils.js' => '5c93c52c', 'rsrc/js/core/Title.js' => 'df5e11d2', 'rsrc/js/core/ToolTip.js' => '1d298e3a', 'rsrc/js/core/behavior-active-nav.js' => 'e379b58e', 'rsrc/js/core/behavior-audio-source.js' => '59b251eb', 'rsrc/js/core/behavior-autofocus.js' => '7319e029', 'rsrc/js/core/behavior-choose-control.js' => '6153c708', 'rsrc/js/core/behavior-crop.js' => 'fa0f4fc2', 'rsrc/js/core/behavior-dark-console.js' => 'f411b6ae', 'rsrc/js/core/behavior-device.js' => 'a205cf28', 'rsrc/js/core/behavior-drag-and-drop-textarea.js' => '6d49590e', 'rsrc/js/core/behavior-error-log.js' => '6882e80a', 'rsrc/js/core/behavior-fancy-datepicker.js' => '510b5809', 'rsrc/js/core/behavior-file-tree.js' => '88236f00', 'rsrc/js/core/behavior-form.js' => '5c54cbf3', 'rsrc/js/core/behavior-gesture.js' => '3ab51e2c', 'rsrc/js/core/behavior-global-drag-and-drop.js' => 'c8e57404', 'rsrc/js/core/behavior-high-security-warning.js' => 'a464fe03', 'rsrc/js/core/behavior-history-install.js' => '7ee2b591', 'rsrc/js/core/behavior-hovercard.js' => 'f36e01af', 'rsrc/js/core/behavior-keyboard-pager.js' => 'a8da01f0', 'rsrc/js/core/behavior-keyboard-shortcuts.js' => 'd75709e6', 'rsrc/js/core/behavior-lightbox-attachments.js' => 'f8ba29d7', 'rsrc/js/core/behavior-line-linker.js' => '1499a8cb', 'rsrc/js/core/behavior-more.js' => 'a80d0378', 'rsrc/js/core/behavior-object-selector.js' => '49b73b36', 'rsrc/js/core/behavior-oncopy.js' => '2926fff2', 'rsrc/js/core/behavior-phabricator-nav.js' => '14d7a8b8', 'rsrc/js/core/behavior-phabricator-remarkup-assist.js' => '095ed313', 'rsrc/js/core/behavior-refresh-csrf.js' => '7814b593', 'rsrc/js/core/behavior-remarkup-preview.js' => 'f7379f45', 'rsrc/js/core/behavior-reorder-applications.js' => '76b9fc3e', 'rsrc/js/core/behavior-reveal-content.js' => '60821bc7', 'rsrc/js/core/behavior-scrollbar.js' => '834a1173', 'rsrc/js/core/behavior-search-typeahead.js' => '048330fa', 'rsrc/js/core/behavior-select-on-click.js' => '4e3e79a6', 'rsrc/js/core/behavior-time-typeahead.js' => '8bfbb401', 'rsrc/js/core/behavior-toggle-class.js' => '5d7c9f33', 'rsrc/js/core/behavior-tokenizer.js' => 'b3a4b884', 'rsrc/js/core/behavior-tooltip.js' => '3ee3408b', 'rsrc/js/core/behavior-watch-anchor.js' => '9f36c42d', 'rsrc/js/core/behavior-workflow.js' => '0a3f3021', 'rsrc/js/core/phtize.js' => 'd254d646', 'rsrc/js/phui/behavior-phui-dropdown-menu.js' => '54733475', 'rsrc/js/phui/behavior-phui-object-box-tabs.js' => '2bfa2836', 'rsrc/js/phuix/PHUIXActionListView.js' => 'b5c256b8', 'rsrc/js/phuix/PHUIXActionView.js' => '8cf6d262', 'rsrc/js/phuix/PHUIXDropdownMenu.js' => 'bd4c8dca', ), 'symbols' => array( 'almanac-css' => 'dbb9b3af', 'aphront-bars' => '231ac33c', 'aphront-dark-console-css' => '6378ef3d', 'aphront-dialog-view-css' => '9b32db0a', 'aphront-list-filter-view-css' => 'b2161041', 'aphront-multi-column-view-css' => 'fd18389d', 'aphront-pager-view-css' => '2e3539af', 'aphront-panel-view-css' => '8427b78d', 'aphront-table-view-css' => '0b4cd283', 'aphront-tokenizer-control-css' => '86a13f7f', 'aphront-tooltip-css' => '7672b60f', 'aphront-two-column-view-css' => '16ab3ad2', 'aphront-typeahead-control-css' => '0e403212', 'auth-css' => '44975d4b', 'calendar-icon-css' => '98ce946d', 'changeset-view-manager' => '58562350', 'conduit-api-css' => '7bc725c4', 'config-options-css' => '7fedf08b', 'config-welcome-css' => '6abd79be', 'conpherence-durable-column-view' => '4331cbe9', 'conpherence-menu-css' => 'f389e048', 'conpherence-message-pane-css' => '5bb4b76d', 'conpherence-notification-css' => '919974b6', 'conpherence-thread-manager' => '01774ab2', 'conpherence-transaction-css' => '42a457f6', 'conpherence-update-css' => '1099a660', 'conpherence-widget-pane-css' => '2af42ebe', 'differential-changeset-view-css' => 'e19cfd6e', 'differential-core-view-css' => '7ac3cabc', 'differential-inline-comment-editor' => 'd4c87bf4', 'differential-results-table-css' => '181aa9d9', 'differential-revision-add-comment-css' => 'c47f8c40', 'differential-revision-comment-css' => '14b8565a', 'differential-revision-history-css' => '0e8eb855', 'differential-revision-list-css' => 'f3c47d33', 'differential-table-of-contents-css' => '63f3ef4a', 'diffusion-icons-css' => '9c5828da', 'diffusion-readme-css' => '2106ea08', 'diffusion-source-css' => '66fdf661', 'diviner-shared-css' => '38813222', 'font-fontawesome' => 'e2e712fe', 'font-source-sans-pro' => '8906c07b', 'global-drag-and-drop-css' => '697324ad', 'harbormaster-css' => '49d64eb4', 'herald-css' => '826075fa', 'herald-rule-editor' => '271ffdd7', 'herald-test-css' => '778b008e', 'homepage-panel-css' => 'e34bf140', 'inline-comment-summary-css' => 'eb5f8e8c', 'javelin-aphlict' => '5359e785', 'javelin-behavior' => '61cbc29a', 'javelin-behavior-aphlict-dropdown' => '995ad707', 'javelin-behavior-aphlict-listen' => 'b1a59974', 'javelin-behavior-aphlict-status' => 'ea681761', 'javelin-behavior-aphront-basic-tokenizer' => 'b3a4b884', 'javelin-behavior-aphront-crop' => 'fa0f4fc2', 'javelin-behavior-aphront-drag-and-drop-textarea' => '6d49590e', 'javelin-behavior-aphront-form-disable-on-submit' => '5c54cbf3', 'javelin-behavior-aphront-more' => 'a80d0378', 'javelin-behavior-audio-source' => '59b251eb', 'javelin-behavior-audit-preview' => 'd835b03a', 'javelin-behavior-choose-control' => '6153c708', 'javelin-behavior-config-reorder-fields' => 'b6993408', 'javelin-behavior-conpherence-drag-and-drop-photo' => 'cf86d16a', 'javelin-behavior-conpherence-menu' => '43b24a10', 'javelin-behavior-conpherence-pontificate' => '21ba5861', 'javelin-behavior-conpherence-widget-pane' => '93568464', 'javelin-behavior-countdown-timer' => 'e4cc26b3', 'javelin-behavior-dark-console' => 'f411b6ae', 'javelin-behavior-dashboard-async-panel' => '469c0d9e', 'javelin-behavior-dashboard-move-panels' => '82439934', 'javelin-behavior-dashboard-query-panel-select' => '453c5375', 'javelin-behavior-dashboard-tab-panel' => 'd4eecc63', 'javelin-behavior-day-view' => '5c46cff2', 'javelin-behavior-device' => 'a205cf28', 'javelin-behavior-differential-add-reviewers-and-ccs' => 'e10f8e18', 'javelin-behavior-differential-comment-jump' => '4fdb476d', 'javelin-behavior-differential-diff-radios' => 'e1ff79b1', 'javelin-behavior-differential-dropdown-menus' => '2035b9cb', 'javelin-behavior-differential-edit-inline-comments' => '037b59eb', 'javelin-behavior-differential-feedback-preview' => 'b064af76', 'javelin-behavior-differential-keyboard-navigation' => '2c426492', 'javelin-behavior-differential-populate' => '8694b1df', 'javelin-behavior-differential-show-field-details' => 'bba9eedf', 'javelin-behavior-differential-toggle-files' => 'ca3f91eb', 'javelin-behavior-differential-user-select' => 'a8d8459d', 'javelin-behavior-diffusion-commit-branches' => 'bdaf4d04', 'javelin-behavior-diffusion-commit-graph' => '9007c197', 'javelin-behavior-diffusion-jump-to' => '73d09eef', 'javelin-behavior-diffusion-locate-file' => '6d3e1947', 'javelin-behavior-diffusion-pull-lastmodified' => '2b228192', 'javelin-behavior-doorkeeper-tag' => 'e5822781', 'javelin-behavior-durable-column' => '16c695bf', 'javelin-behavior-error-log' => '6882e80a', 'javelin-behavior-event-all-day' => '38dcf3c8', 'javelin-behavior-fancy-datepicker' => '510b5809', 'javelin-behavior-global-drag-and-drop' => 'c8e57404', 'javelin-behavior-herald-rule-editor' => '7ebaeed3', 'javelin-behavior-high-security-warning' => 'a464fe03', 'javelin-behavior-history-install' => '7ee2b591', 'javelin-behavior-icon-composer' => '8ef9ab58', 'javelin-behavior-launch-icon-composer' => '48086888', 'javelin-behavior-lightbox-attachments' => 'f8ba29d7', 'javelin-behavior-line-chart' => '88f0c5b3', 'javelin-behavior-load-blame' => '42126667', 'javelin-behavior-maniphest-batch-editor' => 'f5d1233b', 'javelin-behavior-maniphest-batch-selector' => '7b98d7c5', 'javelin-behavior-maniphest-list-editor' => 'a9f88de2', 'javelin-behavior-maniphest-subpriority-editor' => '84845b5b', 'javelin-behavior-maniphest-transaction-controls' => '44168bad', 'javelin-behavior-maniphest-transaction-expand' => '5fefb143', 'javelin-behavior-maniphest-transaction-preview' => '4c95d29e', 'javelin-behavior-owners-path-editor' => '7a68dda3', 'javelin-behavior-passphrase-credential-control' => '3cb0b2fc', 'javelin-behavior-persona-login' => '9414ff18', 'javelin-behavior-phabricator-active-nav' => 'e379b58e', 'javelin-behavior-phabricator-autofocus' => '7319e029', 'javelin-behavior-phabricator-busy-example' => '60479091', 'javelin-behavior-phabricator-file-tree' => '88236f00', 'javelin-behavior-phabricator-gesture' => '3ab51e2c', 'javelin-behavior-phabricator-gesture-example' => '558829c2', 'javelin-behavior-phabricator-hovercards' => 'f36e01af', 'javelin-behavior-phabricator-keyboard-pager' => 'a8da01f0', 'javelin-behavior-phabricator-keyboard-shortcuts' => 'd75709e6', 'javelin-behavior-phabricator-line-linker' => '1499a8cb', 'javelin-behavior-phabricator-nav' => '14d7a8b8', 'javelin-behavior-phabricator-notification-example' => '8ce821c5', 'javelin-behavior-phabricator-object-selector' => '49b73b36', 'javelin-behavior-phabricator-oncopy' => '2926fff2', 'javelin-behavior-phabricator-remarkup-assist' => '095ed313', 'javelin-behavior-phabricator-reveal-content' => '60821bc7', 'javelin-behavior-phabricator-search-typeahead' => '048330fa', 'javelin-behavior-phabricator-show-older-transactions' => 'dbbf48b6', 'javelin-behavior-phabricator-tooltips' => '3ee3408b', 'javelin-behavior-phabricator-transaction-comment-form' => 'b23b49e6', 'javelin-behavior-phabricator-transaction-list' => '13c739ea', 'javelin-behavior-phabricator-watch-anchor' => '9f36c42d', 'javelin-behavior-phame-post-preview' => 'be807912', 'javelin-behavior-pholio-mock-edit' => '246dc085', 'javelin-behavior-pholio-mock-view' => 'fbe497e7', 'javelin-behavior-phui-dropdown-menu' => '54733475', 'javelin-behavior-phui-object-box-tabs' => '2bfa2836', 'javelin-behavior-policy-control' => '9a340b3d', 'javelin-behavior-policy-rule-editor' => '5e9f347c', 'javelin-behavior-ponder-votebox' => '4e9b766b', 'javelin-behavior-project-boards' => 'ba4fa35c', 'javelin-behavior-project-create' => '065227cc', 'javelin-behavior-quicksand-blacklist' => '7927a7d3', + 'javelin-behavior-recurring-edit' => '85c73ceb', 'javelin-behavior-refresh-csrf' => '7814b593', 'javelin-behavior-releeph-preview-branch' => 'b2b4fbaf', 'javelin-behavior-releeph-request-state-change' => 'a0b57eb8', 'javelin-behavior-releeph-request-typeahead' => 'de2e896f', 'javelin-behavior-remarkup-preview' => 'f7379f45', 'javelin-behavior-reorder-applications' => '76b9fc3e', 'javelin-behavior-reorder-columns' => 'e1d25dfb', 'javelin-behavior-repository-crossreference' => 'bea81850', 'javelin-behavior-scrollbar' => '834a1173', 'javelin-behavior-search-reorder-queries' => 'e9581f08', 'javelin-behavior-select-on-click' => '4e3e79a6', 'javelin-behavior-slowvote-embed' => '887ad43f', 'javelin-behavior-stripe-payment-form' => '3f5d6dbf', 'javelin-behavior-test-payment-form' => 'fc91ab6c', 'javelin-behavior-time-typeahead' => '8bfbb401', 'javelin-behavior-toggle-class' => '5d7c9f33', 'javelin-behavior-typeahead-browse' => '635de1ec', 'javelin-behavior-typeahead-search' => '93d0c9e3', 'javelin-behavior-view-placeholder' => '47830651', 'javelin-behavior-workflow' => '0a3f3021', 'javelin-color' => '7e41274a', 'javelin-cookie' => '62dfea03', 'javelin-diffusion-locate-file-source' => 'b42eddc7', 'javelin-dom' => '147805fa', 'javelin-dynval' => 'f6555212', 'javelin-event' => '85ea0626', 'javelin-fx' => '54b612ba', 'javelin-history' => 'd4505101', 'javelin-install' => '05270951', 'javelin-json' => '69adf288', 'javelin-leader' => '331b1611', 'javelin-magical-init' => '3010e992', 'javelin-mask' => '8a41885b', 'javelin-quicksand' => '4cebc641', 'javelin-reactor' => '2b8de964', 'javelin-reactor-dom' => 'c90a04fc', 'javelin-reactor-node-calmer' => '76f4ebed', 'javelin-reactornode' => '1ad0a787', 'javelin-request' => '94b750d2', 'javelin-resource' => '44959b73', 'javelin-routable' => 'b3e7d692', 'javelin-router' => '29274e2b', 'javelin-scrollbar' => '087e919c', 'javelin-sound' => '949c0fe5', 'javelin-stratcom' => '6c53634d', 'javelin-tokenizer' => 'ab5f468d', 'javelin-typeahead' => '70baed2f', 'javelin-typeahead-composite-source' => '503e17fd', 'javelin-typeahead-normalizer' => 'e6e25838', 'javelin-typeahead-ondemand-source' => '8b3fd187', 'javelin-typeahead-preloaded-source' => '54f314a0', 'javelin-typeahead-source' => '2818f5ce', 'javelin-typeahead-static-source' => '6c0e62fa', 'javelin-uri' => '6eff08aa', 'javelin-util' => '93cc50d6', 'javelin-vector' => '2caa8fb8', 'javelin-view' => '0f764c35', 'javelin-view-html' => 'fe287620', 'javelin-view-interpreter' => 'f829edb3', 'javelin-view-renderer' => '6c2b09a2', 'javelin-view-visitor' => 'efe49472', 'javelin-websocket' => 'e292eaf4', 'javelin-workflow' => '5b2e3e2b', 'lightbox-attachment-css' => '7acac05d', 'maniphest-batch-editor' => '8f380ebc', 'maniphest-report-css' => 'f6931fdf', 'maniphest-task-edit-css' => '8e23031b', 'maniphest-task-summary-css' => 'ab2fc691', 'multirow-row-manager' => 'b5d57730', 'owners-path-editor' => 'aa1733d0', 'owners-path-editor-css' => '2f00933b', 'paste-css' => 'eb997ddd', 'path-typeahead' => 'f7fc67ec', 'people-profile-css' => '25970776', 'phabricator-action-list-view-css' => '4f4d09f2', 'phabricator-application-launch-view-css' => '16ca323f', 'phabricator-busy' => '59a7976a', 'phabricator-chatlog-css' => '852140ff', 'phabricator-content-source-view-css' => '4b8b05d4', 'phabricator-core-css' => 'aaea7a7a', 'phabricator-countdown-css' => '86b7b0a0', 'phabricator-dashboard-css' => '17937d22', 'phabricator-drag-and-drop-file-upload' => '07de8873', 'phabricator-draggable-list' => 'a16ec1c6', 'phabricator-fatal-config-template-css' => '8e6c6fcd', 'phabricator-feed-css' => 'b513b5f4', 'phabricator-file-upload' => '477359c8', 'phabricator-filetree-view-css' => 'fccf9f82', 'phabricator-flag-css' => '5337623f', 'phabricator-hovercard' => '14ac66f5', 'phabricator-hovercard-view-css' => 'dd9121a9', 'phabricator-keyboard-shortcut' => '1ae869f2', 'phabricator-keyboard-shortcut-manager' => 'c1700f6f', 'phabricator-main-menu-view' => '663e3810', 'phabricator-nav-view-css' => '7aeaf435', 'phabricator-notification' => '0c6946e7', 'phabricator-notification-css' => '9c279160', 'phabricator-notification-menu-css' => '3c9d8aa1', 'phabricator-object-selector-css' => '029a133d', 'phabricator-phtize' => 'd254d646', 'phabricator-prefab' => '6920d200', 'phabricator-profile-css' => '1a20dcbf', 'phabricator-remarkup-css' => '07b7dc54', 'phabricator-search-results-css' => '15c71110', 'phabricator-shaped-request' => '7cbe244b', 'phabricator-side-menu-view-css' => 'c1db9e9c', 'phabricator-slowvote-css' => '266df6a1', 'phabricator-source-code-view-css' => '2ceee894', 'phabricator-standard-page-view' => '61e68a55', 'phabricator-textareautils' => '5c93c52c', 'phabricator-title' => 'df5e11d2', 'phabricator-tooltip' => '1d298e3a', 'phabricator-ui-example-css' => '528b19de', 'phabricator-uiexample-javelin-view' => 'd4a14807', 'phabricator-uiexample-reactor-button' => 'd19198c8', 'phabricator-uiexample-reactor-checkbox' => '519705ea', 'phabricator-uiexample-reactor-focus' => '40a6a403', 'phabricator-uiexample-reactor-input' => '886fd850', 'phabricator-uiexample-reactor-mouseover' => '47c794d8', 'phabricator-uiexample-reactor-radio' => '988040b4', 'phabricator-uiexample-reactor-select' => 'a155550f', 'phabricator-uiexample-reactor-sendclass' => '1def2711', 'phabricator-uiexample-reactor-sendproperties' => 'b1f0ccee', 'phabricator-zindex-css' => 'c4732d32', 'phame-css' => '88bd4705', 'pholio-css' => '95174bdd', 'pholio-edit-css' => '3ad9d1ee', 'pholio-inline-comments-css' => '8e545e49', 'phortune-credit-card-form' => '2290aeef', 'phortune-credit-card-form-css' => '8391eb02', 'phortune-css' => '9149f103', 'phrequent-css' => 'ffc185ad', 'phriction-document-css' => '0d16bc9a', 'phui-action-header-view-css' => '89c497e7', 'phui-action-panel-css' => '3ee9afd5', 'phui-box-css' => '7b3a2eed', 'phui-button-css' => 'de610129', 'phui-calendar-css' => 'ccabe893', 'phui-calendar-day-css' => 'd1cf6f93', 'phui-calendar-list-css' => 'c1c7f338', 'phui-calendar-month-css' => '476be7e0', 'phui-crumbs-view-css' => '594d719e', 'phui-document-view-css' => '94d5dcd8', 'phui-feed-story-css' => 'c9f3a0b5', 'phui-font-icon-base-css' => '3dad2ae3', 'phui-fontkit-css' => 'dd8ddf27', - 'phui-form-css' => 'f535f938', + 'phui-form-css' => '25876baf', 'phui-form-view-css' => '808329f2', 'phui-header-view-css' => '75aaf372', 'phui-icon-view-css' => 'bc766998', 'phui-image-mask-css' => '5a8b09c8', 'phui-info-panel-css' => '27ea50a1', 'phui-info-view-css' => 'c6f0aef8', 'phui-inline-comment-view-css' => 'aa16f165', 'phui-list-view-css' => '2e25ebfb', 'phui-object-box-css' => '7d160002', 'phui-object-item-list-view-css' => 'f3a22696', 'phui-pinboard-view-css' => 'eaab2b1b', 'phui-property-list-view-css' => '5b671934', 'phui-remarkup-preview-css' => '19ad512b', 'phui-spacing-css' => '042804d6', 'phui-status-list-view-css' => '888cedb8', 'phui-tag-view-css' => '402691cc', 'phui-text-css' => 'cf019f54', 'phui-timeline-view-css' => 'a85542c8', 'phui-workboard-view-css' => '5c4c53e9', 'phui-workpanel-view-css' => 'e495a5cc', 'phuix-action-list-view' => 'b5c256b8', 'phuix-action-view' => '8cf6d262', 'phuix-dropdown-menu' => 'bd4c8dca', 'policy-css' => '957ea14c', 'policy-edit-css' => '815c66f7', 'policy-transaction-detail-css' => '82100a43', 'ponder-comment-table-css' => '6cdccea7', 'ponder-feed-view-css' => 'e62615b6', 'ponder-post-css' => '9d415218', 'ponder-vote-css' => '8ed6ed8b', 'project-icon-css' => 'c2ecb7f1', 'raphael-core' => '51ee6b43', 'raphael-g' => '40dde778', 'raphael-g-line' => '40da039e', 'releeph-core' => '9b3c5733', 'releeph-preview-branch' => 'b7a6f4a5', 'releeph-request-differential-create-dialog' => '8d8b92cd', 'releeph-request-typeahead-css' => '667a48ae', 'setup-issue-css' => '22270af2', 'sprite-gradient-css' => '4bdb98a7', 'sprite-login-css' => 'a3526809', 'sprite-main-header-css' => '28d01b0b', 'sprite-menu-css' => '9ef76324', 'sprite-projects-css' => 'b0d9e24f', 'sprite-tokens-css' => '1706b943', 'syntax-highlighting-css' => '9fd11da8', 'tokens-css' => '3d0f239e', 'typeahead-browse-css' => 'd8581d2c', 'unhandled-exception-css' => '37d4f9a2', ), 'requires' => array( '01774ab2' => array( 'javelin-dom', 'javelin-util', 'javelin-stratcom', 'javelin-install', 'javelin-aphlict', 'javelin-workflow', 'javelin-router', 'javelin-behavior-device', 'javelin-vector', ), '029a133d' => array( 'aphront-dialog-view-css', ), '037b59eb' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', 'javelin-util', 'javelin-vector', 'differential-inline-comment-editor', ), '048330fa' => array( 'javelin-behavior', 'javelin-typeahead-ondemand-source', 'javelin-typeahead', 'javelin-dom', 'javelin-uri', 'javelin-util', 'javelin-stratcom', 'phabricator-prefab', ), '05270951' => array( 'javelin-util', 'javelin-magical-init', ), '065227cc' => array( 'javelin-behavior', 'javelin-dom', 'javelin-stratcom', 'javelin-workflow', ), '07de8873' => array( 'javelin-install', 'javelin-util', 'javelin-request', 'javelin-dom', 'javelin-uri', 'phabricator-file-upload', ), '087e919c' => array( 'javelin-install', 'javelin-dom', 'javelin-stratcom', 'javelin-vector', ), '095ed313' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', 'phabricator-phtize', 'phabricator-textareautils', 'javelin-workflow', 'javelin-vector', ), '0a3f3021' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-workflow', 'javelin-dom', 'javelin-router', ), '0c6946e7' => array( 'javelin-install', 'javelin-dom', 'javelin-stratcom', 'javelin-util', 'phabricator-notification-css', ), '0f764c35' => array( 'javelin-install', 'javelin-util', ), '13c739ea' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-workflow', 'javelin-dom', 'javelin-uri', 'phabricator-textareautils', ), '147805fa' => array( 'javelin-magical-init', 'javelin-install', 'javelin-util', 'javelin-vector', 'javelin-stratcom', ), '1499a8cb' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', 'javelin-history', ), '14ac66f5' => array( 'javelin-install', 'javelin-dom', 'javelin-vector', 'javelin-request', 'javelin-uri', ), '14d7a8b8' => array( 'javelin-behavior', 'javelin-behavior-device', 'javelin-stratcom', 'javelin-dom', 'javelin-magical-init', 'javelin-vector', 'javelin-request', 'javelin-util', ), '16c695bf' => array( 'javelin-behavior', 'javelin-dom', 'javelin-stratcom', 'javelin-behavior-device', 'javelin-scrollbar', 'javelin-quicksand', 'phabricator-keyboard-shortcut', 'conpherence-thread-manager', ), '1ad0a787' => array( 'javelin-install', 'javelin-reactor', 'javelin-util', 'javelin-reactor-node-calmer', ), '1ae869f2' => array( 'javelin-install', 'javelin-util', 'phabricator-keyboard-shortcut-manager', ), '1d298e3a' => array( 'javelin-install', 'javelin-util', 'javelin-dom', 'javelin-vector', ), '1def2711' => array( 'javelin-install', 'javelin-dom', 'javelin-reactor-dom', ), '2035b9cb' => array( 'javelin-behavior', 'javelin-dom', 'javelin-util', 'javelin-stratcom', 'javelin-workflow', 'phuix-dropdown-menu', 'phuix-action-list-view', 'phuix-action-view', 'phabricator-phtize', 'changeset-view-manager', ), '21ba5861' => array( 'javelin-behavior', 'javelin-dom', 'javelin-util', 'javelin-workflow', 'javelin-stratcom', 'conpherence-thread-manager', ), '2290aeef' => array( 'javelin-install', 'javelin-dom', 'javelin-json', 'javelin-workflow', 'javelin-util', ), '246dc085' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', 'javelin-workflow', 'javelin-quicksand', 'phabricator-phtize', 'phabricator-drag-and-drop-file-upload', 'phabricator-draggable-list', ), '271ffdd7' => array( 'multirow-row-manager', 'javelin-install', 'javelin-util', 'javelin-dom', 'javelin-stratcom', 'javelin-json', 'phabricator-prefab', ), '2818f5ce' => array( 'javelin-install', 'javelin-util', 'javelin-dom', 'javelin-typeahead-normalizer', ), '2926fff2' => array( 'javelin-behavior', 'javelin-dom', ), '29274e2b' => array( 'javelin-install', 'javelin-util', ), '2b228192' => array( 'javelin-behavior', 'javelin-dom', 'javelin-util', 'javelin-workflow', 'javelin-json', ), '2b8de964' => array( 'javelin-install', 'javelin-util', ), '2bfa2836' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', ), '2c426492' => array( 'javelin-behavior', 'javelin-dom', 'javelin-stratcom', 'phabricator-keyboard-shortcut', ), '2caa8fb8' => array( 'javelin-install', 'javelin-event', ), '331b1611' => array( 'javelin-install', ), '3ab51e2c' => array( 'javelin-behavior', 'javelin-behavior-device', 'javelin-stratcom', 'javelin-vector', 'javelin-dom', 'javelin-magical-init', ), '3cb0b2fc' => array( 'javelin-behavior', 'javelin-dom', 'javelin-stratcom', 'javelin-workflow', 'javelin-util', 'javelin-uri', ), '3ee3408b' => array( 'javelin-behavior', 'javelin-behavior-device', 'javelin-stratcom', 'phabricator-tooltip', ), '3f5d6dbf' => array( 'javelin-behavior', 'javelin-dom', 'phortune-credit-card-form', ), '40a6a403' => array( 'javelin-install', 'javelin-dom', 'javelin-reactor-dom', ), 42126667 => array( 'javelin-behavior', 'javelin-dom', 'javelin-request', ), '43b24a10' => array( 'javelin-behavior', 'javelin-dom', 'javelin-util', 'javelin-stratcom', 'javelin-workflow', 'javelin-behavior-device', 'javelin-history', 'javelin-vector', 'javelin-scrollbar', 'phabricator-title', 'phabricator-shaped-request', 'conpherence-thread-manager', ), '44168bad' => array( 'javelin-behavior', 'javelin-dom', 'phabricator-prefab', ), '44959b73' => array( 'javelin-util', 'javelin-uri', 'javelin-install', ), '453c5375' => array( 'javelin-behavior', 'javelin-dom', ), '469c0d9e' => array( 'javelin-behavior', 'javelin-dom', 'javelin-workflow', ), '477359c8' => array( 'javelin-install', 'javelin-dom', 'phabricator-notification', ), 47830651 => array( 'javelin-behavior', 'javelin-dom', 'javelin-view-renderer', 'javelin-install', ), '47c794d8' => array( 'javelin-install', 'javelin-dom', 'javelin-reactor-dom', ), 48086888 => array( 'javelin-behavior', 'javelin-dom', 'javelin-workflow', ), '49b73b36' => array( 'javelin-behavior', 'javelin-dom', 'javelin-request', 'javelin-util', ), '4c95d29e' => array( 'javelin-behavior', 'javelin-dom', 'javelin-util', 'javelin-json', 'javelin-stratcom', 'phabricator-shaped-request', ), '4cebc641' => array( 'javelin-install', ), '4e3e79a6' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', ), '4e9b766b' => array( 'javelin-behavior', 'javelin-dom', 'javelin-util', 'javelin-stratcom', 'javelin-request', ), '4fdb476d' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', ), '503e17fd' => array( 'javelin-install', 'javelin-typeahead-source', 'javelin-util', ), '510b5809' => array( 'javelin-behavior', 'javelin-util', 'javelin-dom', 'javelin-stratcom', 'javelin-vector', ), '519705ea' => array( 'javelin-install', 'javelin-dom', 'javelin-reactor-dom', ), '5359e785' => array( 'javelin-install', 'javelin-util', 'javelin-websocket', 'javelin-leader', 'javelin-json', ), 54733475 => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', 'phuix-dropdown-menu', ), '54b612ba' => array( 'javelin-color', 'javelin-install', 'javelin-util', ), '54f314a0' => array( 'javelin-install', 'javelin-util', 'javelin-request', 'javelin-typeahead-source', ), '558829c2' => array( 'javelin-stratcom', 'javelin-behavior', 'javelin-vector', 'javelin-dom', ), 58562350 => array( 'javelin-dom', 'javelin-util', 'javelin-stratcom', 'javelin-install', 'javelin-workflow', 'javelin-router', 'javelin-behavior-device', 'javelin-vector', ), '59a7976a' => array( 'javelin-install', 'javelin-dom', 'javelin-fx', ), '59b251eb' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-vector', 'javelin-dom', ), '5b2e3e2b' => array( 'javelin-stratcom', 'javelin-request', 'javelin-dom', 'javelin-vector', 'javelin-install', 'javelin-util', 'javelin-mask', 'javelin-uri', 'javelin-routable', ), '5c54cbf3' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', ), '5c93c52c' => array( 'javelin-install', 'javelin-dom', 'javelin-vector', ), '5d7c9f33' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', ), '5e9f347c' => array( 'javelin-behavior', 'multirow-row-manager', 'javelin-dom', 'javelin-util', 'phabricator-prefab', 'javelin-json', ), '5fefb143' => array( 'javelin-behavior', 'javelin-dom', 'javelin-workflow', 'javelin-stratcom', ), 60479091 => array( 'phabricator-busy', 'javelin-behavior', ), '60821bc7' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', ), '6153c708' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', 'javelin-workflow', ), '61cbc29a' => array( 'javelin-magical-init', 'javelin-util', ), '62dfea03' => array( 'javelin-install', 'javelin-util', ), '635de1ec' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-workflow', 'javelin-dom', ), '6882e80a' => array( 'javelin-dom', ), '6920d200' => 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', ), '69adf288' => array( 'javelin-install', ), '6c0e62fa' => array( 'javelin-install', 'javelin-typeahead-source', ), '6c2b09a2' => array( 'javelin-install', 'javelin-util', ), '6c53634d' => array( 'javelin-install', 'javelin-event', 'javelin-util', 'javelin-magical-init', ), '6d3e1947' => array( 'javelin-behavior', 'javelin-diffusion-locate-file-source', 'javelin-dom', 'javelin-typeahead', 'javelin-uri', ), '6d49590e' => array( 'javelin-behavior', 'javelin-dom', 'phabricator-drag-and-drop-file-upload', 'phabricator-textareautils', ), '6eff08aa' => array( 'javelin-install', 'javelin-util', 'javelin-stratcom', ), '70baed2f' => array( 'javelin-install', 'javelin-dom', 'javelin-vector', 'javelin-util', ), '7319e029' => array( 'javelin-behavior', 'javelin-dom', ), '73d09eef' => array( 'javelin-behavior', 'javelin-vector', 'javelin-dom', ), '76b9fc3e' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-workflow', 'javelin-dom', 'phabricator-draggable-list', ), '76f4ebed' => array( 'javelin-install', 'javelin-reactor', 'javelin-util', ), '7814b593' => array( 'javelin-request', 'javelin-behavior', 'javelin-dom', 'javelin-router', 'javelin-util', 'phabricator-busy', ), '7927a7d3' => array( 'javelin-behavior', 'javelin-quicksand', ), '7a68dda3' => array( 'owners-path-editor', 'javelin-behavior', ), '7b98d7c5' => array( 'javelin-behavior', 'javelin-dom', 'javelin-stratcom', 'javelin-util', ), '7cbe244b' => array( 'javelin-install', 'javelin-util', 'javelin-request', 'javelin-router', ), '7e41274a' => array( 'javelin-install', ), '7ebaeed3' => array( 'herald-rule-editor', 'javelin-behavior', ), '7ee2b591' => array( 'javelin-behavior', 'javelin-history', ), 82439934 => array( 'javelin-behavior', 'javelin-dom', 'javelin-util', 'javelin-stratcom', 'javelin-workflow', 'phabricator-draggable-list', ), '834a1173' => array( 'javelin-behavior', 'javelin-scrollbar', ), '84845b5b' => array( 'javelin-behavior', 'javelin-dom', 'javelin-stratcom', 'javelin-workflow', 'phabricator-draggable-list', ), '85ea0626' => array( 'javelin-install', ), '8694b1df' => array( 'javelin-behavior', 'javelin-dom', 'javelin-stratcom', 'phabricator-tooltip', 'changeset-view-manager', ), '86a13f7f' => array( 'aphront-typeahead-control-css', 'phui-tag-view-css', ), '88236f00' => array( 'javelin-behavior', 'phabricator-keyboard-shortcut', 'javelin-stratcom', ), '886fd850' => array( 'javelin-install', 'javelin-reactor-dom', 'javelin-view-html', 'javelin-view-interpreter', 'javelin-view-renderer', ), '887ad43f' => array( 'javelin-behavior', 'javelin-request', 'javelin-stratcom', 'javelin-dom', ), '88f0c5b3' => array( 'javelin-behavior', 'javelin-dom', 'javelin-vector', ), '8906c07b' => array( 'phui-fontkit-css', ), '8a41885b' => array( 'javelin-install', 'javelin-dom', ), '8b3fd187' => array( 'javelin-install', 'javelin-util', 'javelin-request', 'javelin-typeahead-source', ), '8bfbb401' => array( 'javelin-behavior', 'javelin-util', 'javelin-dom', 'javelin-stratcom', 'javelin-vector', 'javelin-typeahead-static-source', ), '8ce821c5' => array( 'phabricator-notification', 'javelin-stratcom', 'javelin-behavior', ), '8cf6d262' => array( 'javelin-install', 'javelin-dom', 'javelin-util', ), '8ef9ab58' => array( 'javelin-behavior', 'javelin-dom', 'javelin-stratcom', ), '9007c197' => array( 'javelin-behavior', 'javelin-dom', 'javelin-stratcom', ), 93568464 => array( 'javelin-behavior', 'javelin-dom', 'javelin-stratcom', 'javelin-workflow', 'javelin-util', 'phabricator-notification', 'javelin-behavior-device', 'phuix-dropdown-menu', 'phuix-action-list-view', 'phuix-action-view', 'conpherence-thread-manager', ), '93d0c9e3' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-workflow', 'javelin-dom', ), '9414ff18' => array( 'javelin-behavior', 'javelin-resource', 'javelin-stratcom', 'javelin-workflow', 'javelin-util', ), '949c0fe5' => array( 'javelin-install', ), '94b750d2' => array( 'javelin-install', 'javelin-stratcom', 'javelin-util', 'javelin-behavior', 'javelin-json', 'javelin-dom', 'javelin-resource', 'javelin-routable', ), '988040b4' => array( 'javelin-install', 'javelin-dom', 'javelin-reactor-dom', ), '995ad707' => array( 'javelin-behavior', 'javelin-request', 'javelin-stratcom', 'javelin-vector', 'javelin-dom', 'javelin-uri', 'javelin-behavior-device', 'phabricator-title', ), '9a340b3d' => array( 'javelin-behavior', 'javelin-dom', 'javelin-util', 'phuix-dropdown-menu', 'phuix-action-list-view', 'phuix-action-view', 'javelin-workflow', ), '9f36c42d' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', 'javelin-vector', ), 'a0b57eb8' => array( 'javelin-behavior', 'javelin-dom', 'javelin-stratcom', 'javelin-workflow', 'javelin-util', 'phabricator-keyboard-shortcut', ), 'a155550f' => array( 'javelin-install', 'javelin-dom', 'javelin-reactor-dom', ), 'a16ec1c6' => array( 'javelin-install', 'javelin-dom', 'javelin-stratcom', 'javelin-util', 'javelin-vector', 'javelin-magical-init', ), 'a205cf28' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', 'javelin-vector', 'javelin-install', ), 'a464fe03' => array( 'javelin-behavior', 'javelin-uri', 'phabricator-notification', ), 'a80d0378' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', ), 'a8d8459d' => array( 'javelin-behavior', 'javelin-dom', 'javelin-stratcom', ), 'a8da01f0' => array( 'javelin-behavior', 'javelin-uri', 'phabricator-keyboard-shortcut', ), 'a9f88de2' => array( 'javelin-behavior', 'javelin-dom', 'javelin-stratcom', 'javelin-workflow', 'javelin-fx', 'javelin-util', ), 'aa1733d0' => array( 'multirow-row-manager', 'javelin-install', 'path-typeahead', 'javelin-dom', 'javelin-util', 'phabricator-prefab', ), 'ab5f468d' => array( 'javelin-dom', 'javelin-util', 'javelin-stratcom', 'javelin-install', ), 'b064af76' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', 'javelin-request', 'javelin-util', 'phabricator-shaped-request', ), 'b1a59974' => array( 'javelin-behavior', 'javelin-aphlict', 'javelin-stratcom', 'javelin-request', 'javelin-uri', 'javelin-dom', 'javelin-json', 'javelin-router', 'javelin-util', 'javelin-leader', 'javelin-sound', 'phabricator-notification', ), 'b1f0ccee' => array( 'javelin-install', 'javelin-dom', 'javelin-reactor-dom', ), 'b23b49e6' => array( 'javelin-behavior', 'javelin-dom', 'javelin-util', 'javelin-request', 'phabricator-shaped-request', ), 'b2b4fbaf' => array( 'javelin-behavior', 'javelin-dom', 'javelin-uri', 'javelin-request', ), 'b3a4b884' => array( 'javelin-behavior', 'phabricator-prefab', ), 'b3e7d692' => array( 'javelin-install', ), 'b42eddc7' => array( 'javelin-install', 'javelin-dom', 'javelin-typeahead-preloaded-source', 'javelin-util', ), 'b5c256b8' => array( 'javelin-install', 'javelin-dom', ), 'b5d57730' => array( 'javelin-install', 'javelin-stratcom', 'javelin-dom', 'javelin-util', ), 'b6993408' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', 'javelin-json', 'phabricator-draggable-list', ), 'ba4fa35c' => array( 'javelin-behavior', 'javelin-dom', 'javelin-util', 'javelin-vector', 'javelin-stratcom', 'javelin-workflow', 'phabricator-draggable-list', ), 'bba9eedf' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', ), 'bd4c8dca' => array( 'javelin-install', 'javelin-util', 'javelin-dom', 'javelin-vector', 'javelin-stratcom', ), 'bdaf4d04' => array( 'javelin-behavior', 'javelin-dom', 'javelin-util', 'javelin-request', ), 'be807912' => array( 'javelin-behavior', 'javelin-dom', 'javelin-util', 'phabricator-shaped-request', ), 'bea81850' => array( 'javelin-behavior', 'javelin-dom', 'javelin-stratcom', 'javelin-uri', ), 'c1700f6f' => array( 'javelin-install', 'javelin-util', 'javelin-stratcom', 'javelin-dom', 'javelin-vector', ), 'c8e57404' => array( 'javelin-behavior', 'javelin-dom', 'javelin-uri', 'javelin-mask', 'phabricator-drag-and-drop-file-upload', ), 'c90a04fc' => array( 'javelin-dom', 'javelin-dynval', 'javelin-reactor', 'javelin-reactornode', 'javelin-install', 'javelin-util', ), 'ca3f91eb' => array( 'javelin-behavior', 'javelin-dom', 'javelin-stratcom', 'phabricator-phtize', ), 'cf86d16a' => array( 'javelin-behavior', 'javelin-dom', 'javelin-workflow', 'phabricator-drag-and-drop-file-upload', ), 'd19198c8' => array( 'javelin-install', 'javelin-dom', 'javelin-util', 'javelin-dynval', 'javelin-reactor-dom', ), 'd254d646' => array( 'javelin-util', ), 'd4505101' => array( 'javelin-stratcom', 'javelin-install', 'javelin-uri', 'javelin-util', ), 'd4a14807' => array( 'javelin-install', 'javelin-dom', 'javelin-view', ), 'd4c87bf4' => array( 'javelin-dom', 'javelin-util', 'javelin-stratcom', 'javelin-install', 'javelin-request', 'javelin-workflow', ), 'd4eecc63' => array( 'javelin-behavior', 'javelin-dom', 'javelin-stratcom', ), 'd75709e6' => array( 'javelin-behavior', 'javelin-workflow', 'javelin-json', 'javelin-dom', 'phabricator-keyboard-shortcut', ), 'd835b03a' => array( 'javelin-behavior', 'javelin-dom', 'javelin-util', 'phabricator-shaped-request', ), 'dbbf48b6' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', 'phabricator-busy', ), 'de2e896f' => array( 'javelin-behavior', 'javelin-dom', 'javelin-typeahead', 'javelin-typeahead-ondemand-source', 'javelin-dom', ), 'df5e11d2' => array( 'javelin-install', ), 'e10f8e18' => array( 'javelin-behavior', 'javelin-dom', 'phabricator-prefab', ), 'e19cfd6e' => array( 'phui-inline-comment-view-css', ), 'e1d25dfb' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-workflow', 'javelin-dom', 'phabricator-draggable-list', ), 'e1ff79b1' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', ), 'e292eaf4' => array( 'javelin-install', ), 'e379b58e' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-vector', 'javelin-dom', 'javelin-uri', ), 'e4cc26b3' => array( 'javelin-behavior', 'javelin-dom', ), 'e5822781' => array( 'javelin-behavior', 'javelin-dom', 'javelin-json', 'javelin-workflow', 'javelin-magical-init', ), 'e6e25838' => array( 'javelin-install', ), 'e9581f08' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-workflow', 'javelin-dom', 'phabricator-draggable-list', ), 'ea681761' => array( 'javelin-behavior', 'javelin-aphlict', 'phabricator-phtize', 'javelin-dom', ), 'efe49472' => array( 'javelin-install', 'javelin-util', ), 'f36e01af' => array( 'javelin-behavior', 'javelin-behavior-device', 'javelin-stratcom', 'javelin-vector', 'phabricator-hovercard', ), 'f411b6ae' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-util', 'javelin-dom', 'javelin-request', 'phabricator-keyboard-shortcut', ), 'f5d1233b' => array( 'javelin-behavior', 'javelin-dom', 'javelin-util', 'phabricator-prefab', 'multirow-row-manager', 'javelin-json', ), 'f6555212' => array( 'javelin-install', 'javelin-reactornode', 'javelin-util', 'javelin-reactor', ), 'f7379f45' => array( 'javelin-behavior', 'javelin-dom', 'javelin-util', 'phabricator-shaped-request', ), 'f7fc67ec' => array( 'javelin-install', 'javelin-typeahead', 'javelin-dom', 'javelin-request', 'javelin-typeahead-ondemand-source', 'javelin-util', ), 'f829edb3' => array( 'javelin-view', 'javelin-install', 'javelin-dom', ), 'f8ba29d7' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', 'javelin-mask', 'javelin-util', 'phabricator-busy', ), 'fa0f4fc2' => array( 'javelin-behavior', 'javelin-dom', 'javelin-vector', 'javelin-magical-init', ), 'fbe497e7' => 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', ), 'fc91ab6c' => array( 'javelin-behavior', 'javelin-dom', 'phortune-credit-card-form', ), 'fe287620' => array( 'javelin-install', 'javelin-dom', 'javelin-view-visitor', 'javelin-util', ), ), 'packages' => array( 'core.pkg.css' => array( 'phabricator-core-css', 'phabricator-zindex-css', 'phui-button-css', 'phabricator-standard-page-view', 'aphront-dialog-view-css', 'phui-form-view-css', 'aphront-panel-view-css', 'aphront-table-view-css', 'aphront-tokenizer-control-css', 'aphront-typeahead-control-css', 'aphront-list-filter-view-css', 'phabricator-remarkup-css', 'syntax-highlighting-css', 'aphront-pager-view-css', 'aphront-tooltip-css', 'phabricator-flag-css', 'phui-info-view-css', 'sprite-gradient-css', 'sprite-menu-css', 'phabricator-main-menu-view', 'phabricator-notification-css', 'phabricator-notification-menu-css', 'lightbox-attachment-css', 'phui-header-view-css', 'phabricator-filetree-view-css', 'phabricator-nav-view-css', 'phabricator-side-menu-view-css', 'phui-crumbs-view-css', 'phui-object-item-list-view-css', 'global-drag-and-drop-css', 'phui-spacing-css', 'phui-form-css', 'phui-icon-view-css', 'phabricator-application-launch-view-css', 'phabricator-action-list-view-css', 'phui-property-list-view-css', 'phui-tag-view-css', 'phui-list-view-css', 'font-fontawesome', 'phui-font-icon-base-css', 'sprite-main-header-css', 'phui-box-css', 'phui-object-box-css', 'phui-timeline-view-css', 'sprite-tokens-css', 'tokens-css', 'phui-status-list-view-css', 'phui-feed-story-css', 'phabricator-feed-css', 'phabricator-dashboard-css', 'aphront-multi-column-view-css', 'phui-action-header-view-css', 'conpherence-durable-column-view', ), 'core.pkg.js' => array( 'javelin-util', 'javelin-install', 'javelin-event', 'javelin-stratcom', 'javelin-behavior', 'javelin-resource', 'javelin-request', 'javelin-vector', 'javelin-dom', 'javelin-json', 'javelin-uri', 'javelin-workflow', 'javelin-mask', 'javelin-typeahead', 'javelin-typeahead-normalizer', 'javelin-typeahead-source', 'javelin-typeahead-preloaded-source', 'javelin-typeahead-ondemand-source', 'javelin-tokenizer', 'javelin-history', 'javelin-router', 'javelin-routable', 'javelin-behavior-aphront-basic-tokenizer', 'javelin-behavior-workflow', 'javelin-behavior-aphront-form-disable-on-submit', 'phabricator-keyboard-shortcut-manager', 'phabricator-keyboard-shortcut', 'javelin-behavior-phabricator-keyboard-shortcuts', 'javelin-behavior-refresh-csrf', 'javelin-behavior-phabricator-watch-anchor', 'javelin-behavior-phabricator-autofocus', 'phuix-dropdown-menu', 'phuix-action-list-view', 'phuix-action-view', 'phabricator-phtize', 'javelin-behavior-phabricator-oncopy', 'phabricator-tooltip', 'javelin-behavior-phabricator-tooltips', 'phabricator-prefab', 'javelin-behavior-device', 'javelin-behavior-toggle-class', 'javelin-behavior-lightbox-attachments', 'phabricator-busy', 'javelin-aphlict', 'phabricator-notification', 'javelin-behavior-aphlict-listen', 'javelin-behavior-phabricator-search-typeahead', 'javelin-behavior-aphlict-dropdown', 'javelin-behavior-history-install', 'javelin-behavior-phabricator-gesture', 'javelin-behavior-phabricator-active-nav', 'javelin-behavior-phabricator-nav', 'javelin-behavior-phabricator-remarkup-assist', 'phabricator-textareautils', 'phabricator-file-upload', 'javelin-behavior-global-drag-and-drop', 'javelin-behavior-phabricator-reveal-content', 'phabricator-hovercard', 'javelin-behavior-phabricator-hovercards', 'javelin-color', 'javelin-fx', 'phabricator-draggable-list', 'javelin-behavior-phabricator-transaction-list', 'javelin-behavior-phabricator-show-older-transactions', 'javelin-behavior-phui-dropdown-menu', 'javelin-behavior-doorkeeper-tag', 'phabricator-title', 'javelin-leader', 'javelin-websocket', 'javelin-behavior-dashboard-async-panel', 'javelin-behavior-dashboard-tab-panel', 'javelin-quicksand', 'javelin-behavior-quicksand-blacklist', 'javelin-behavior-high-security-warning', 'javelin-scrollbar', 'javelin-behavior-scrollbar', 'javelin-behavior-durable-column', 'conpherence-thread-manager', ), 'darkconsole.pkg.js' => array( 'javelin-behavior-dark-console', 'javelin-behavior-error-log', ), 'differential.pkg.css' => array( 'differential-core-view-css', 'differential-changeset-view-css', 'differential-results-table-css', 'differential-revision-history-css', 'differential-revision-list-css', 'differential-table-of-contents-css', 'differential-revision-comment-css', 'differential-revision-add-comment-css', 'phabricator-object-selector-css', 'phabricator-content-source-view-css', 'inline-comment-summary-css', 'phui-inline-comment-view-css', ), 'differential.pkg.js' => array( 'phabricator-drag-and-drop-file-upload', 'phabricator-shaped-request', 'javelin-behavior-differential-feedback-preview', 'javelin-behavior-differential-edit-inline-comments', 'javelin-behavior-differential-populate', 'javelin-behavior-differential-diff-radios', 'javelin-behavior-differential-comment-jump', 'javelin-behavior-differential-add-reviewers-and-ccs', 'javelin-behavior-differential-keyboard-navigation', 'javelin-behavior-aphront-drag-and-drop-textarea', 'javelin-behavior-phabricator-object-selector', 'javelin-behavior-repository-crossreference', 'javelin-behavior-load-blame', 'differential-inline-comment-editor', 'javelin-behavior-differential-dropdown-menus', 'javelin-behavior-differential-toggle-files', 'javelin-behavior-differential-user-select', 'javelin-behavior-aphront-more', 'changeset-view-manager', ), 'diffusion.pkg.css' => array( 'diffusion-icons-css', ), 'diffusion.pkg.js' => array( 'javelin-behavior-diffusion-pull-lastmodified', 'javelin-behavior-diffusion-commit-graph', 'javelin-behavior-audit-preview', ), 'maniphest.pkg.css' => array( 'maniphest-task-summary-css', ), 'maniphest.pkg.js' => array( 'javelin-behavior-maniphest-batch-selector', 'javelin-behavior-maniphest-transaction-controls', 'javelin-behavior-maniphest-transaction-preview', 'javelin-behavior-maniphest-transaction-expand', 'javelin-behavior-maniphest-subpriority-editor', 'javelin-behavior-maniphest-list-editor', ), ), ); diff --git a/resources/sql/autopatches/20150527.calendar.recurringevents.sql b/resources/sql/autopatches/20150527.calendar.recurringevents.sql new file mode 100644 index 0000000000..43fa15177b --- /dev/null +++ b/resources/sql/autopatches/20150527.calendar.recurringevents.sql @@ -0,0 +1,17 @@ +ALTER TABLE {$NAMESPACE}_calendar.calendar_event + ADD isRecurring BOOL NOT NULL; + +ALTER TABLE {$NAMESPACE}_calendar.calendar_event + ADD recurrenceFrequency LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL; + +ALTER TABLE {$NAMESPACE}_calendar.calendar_event + ADD recurrenceEndDate INT UNSIGNED; + +ALTER TABLE {$NAMESPACE}_calendar.calendar_event + ADD instanceOfEventPHID varbinary(64); + +ALTER TABLE {$NAMESPACE}_calendar.calendar_event + ADD sequenceIndex INT UNSIGNED; + +UPDATE {$NAMESPACE}_calendar.calendar_event + SET recurrenceFrequency = '[]' WHERE recurrenceFrequency = ''; diff --git a/src/applications/calendar/application/PhabricatorCalendarApplication.php b/src/applications/calendar/application/PhabricatorCalendarApplication.php index d41577a3af..465bb2ae5c 100644 --- a/src/applications/calendar/application/PhabricatorCalendarApplication.php +++ b/src/applications/calendar/application/PhabricatorCalendarApplication.php @@ -1,96 +1,97 @@ [1-9]\d*)' => 'PhabricatorCalendarEventViewController', + '/E(?P[1-9]\d*)(?:/(?P\d+))?' + => 'PhabricatorCalendarEventViewController', '/calendar/' => array( '(?:query/(?P[^/]+)/(?:(?P\d+)/'. '(?P\d+)/)?(?:(?P\d+)/)?)?' => 'PhabricatorCalendarEventListController', 'icon/(?P[1-9]\d*)/' => 'PhabricatorCalendarEventEditIconController', 'icon/' => 'PhabricatorCalendarEventEditIconController', 'event/' => array( 'create/' => 'PhabricatorCalendarEventEditController', 'edit/(?P[1-9]\d*)/' => 'PhabricatorCalendarEventEditController', 'drag/(?P[1-9]\d*)/' => 'PhabricatorCalendarEventDragController', 'cancel/(?P[1-9]\d*)/' => 'PhabricatorCalendarEventCancelController', '(?Pjoin|decline|accept)/(?P[1-9]\d*)/' => 'PhabricatorCalendarEventJoinController', 'comment/(?P[1-9]\d*)/' => 'PhabricatorCalendarEventCommentController', ), ), ); } public function getQuickCreateItems(PhabricatorUser $viewer) { $items = array(); $item = id(new PHUIListItemView()) ->setName(pht('Calendar Event')) ->setIcon('fa-calendar') ->setHref($this->getBaseURI().'event/create/'); $items[] = $item; return $items; } public function getMailCommandObjects() { return array( 'event' => array( 'name' => pht('Email Commands: Events'), 'header' => pht('Interacting with Calendar Events'), 'object' => new PhabricatorCalendarEvent(), 'summary' => pht( 'This page documents the commands you can use to interact with '. 'events in Calendar. These commands work when creating new tasks '. 'via email and when replying to existing tasks.'), ), ); } } diff --git a/src/applications/calendar/controller/PhabricatorCalendarEventEditController.php b/src/applications/calendar/controller/PhabricatorCalendarEventEditController.php index 109aa692bd..0710b56907 100644 --- a/src/applications/calendar/controller/PhabricatorCalendarEventEditController.php +++ b/src/applications/calendar/controller/PhabricatorCalendarEventEditController.php @@ -1,424 +1,467 @@ id = idx($data, 'id'); } public function isCreate() { return !$this->id; } public function handleRequest(AphrontRequest $request) { $viewer = $request->getViewer(); $user_phid = $viewer->getPHID(); $error_name = true; $error_start_date = true; $error_end_date = true; $validation_exception = null; + $is_recurring_id = celerity_generate_unique_node_id(); + $frequency_id = celerity_generate_unique_node_id(); $all_day_id = celerity_generate_unique_node_id(); $start_date_id = celerity_generate_unique_node_id(); - $end_date_id = null; + $end_date_id = celerity_generate_unique_node_id(); $next_workflow = $request->getStr('next'); $uri_query = $request->getStr('query'); if ($this->isCreate()) { $mode = $request->getStr('mode'); $event = PhabricatorCalendarEvent::initializeNewCalendarEvent( $viewer, $mode); $create_start_year = $request->getInt('year'); $create_start_month = $request->getInt('month'); $create_start_day = $request->getInt('day'); $create_start_time = $request->getStr('time'); if ($create_start_year) { $start = AphrontFormDateControlValue::newFromParts( $viewer, $create_start_year, $create_start_month, $create_start_day, $create_start_time); if (!$start->isValid()) { return new Aphront400Response(); } $start_value = AphrontFormDateControlValue::newFromEpoch( $viewer, $start->getEpoch()); $end = clone $start_value->getDateTime(); $end->modify('+1 hour'); $end_value = AphrontFormDateControlValue::newFromEpoch( $viewer, $end->format('U')); } else { list($start_value, $end_value) = $this->getDefaultTimeValues($viewer); } $submit_label = pht('Create'); $page_title = pht('Create Event'); $redirect = 'created'; $subscribers = array(); $invitees = array($user_phid); $cancel_uri = $this->getApplicationURI(); - $end_date_id = celerity_generate_unique_node_id(); } else { $event = id(new PhabricatorCalendarEventQuery()) ->setViewer($viewer) ->withIDs(array($this->id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$event) { return new Aphront404Response(); } $end_value = AphrontFormDateControlValue::newFromEpoch( $viewer, $event->getDateTo()); $start_value = AphrontFormDateControlValue::newFromEpoch( $viewer, $event->getDateFrom()); $submit_label = pht('Update'); $page_title = pht('Update Event'); $subscribers = PhabricatorSubscribersQuery::loadSubscribersForPHID( $event->getPHID()); $invitees = array(); foreach ($event->getInvitees() as $invitee) { if ($invitee->isUninvited()) { continue; } else { $invitees[] = $invitee->getInviteePHID(); } } $cancel_uri = '/'.$event->getMonogram(); } $name = $event->getName(); $description = $event->getDescription(); $is_all_day = $event->getIsAllDay(); + $is_recurring = $event->getIsRecurring(); + $frequency = idx($event->getRecurrenceFrequency(), 'rule'); $icon = $event->getIcon(); $current_policies = id(new PhabricatorPolicyQuery()) ->setViewer($viewer) ->setObject($event) ->execute(); if ($request->isFormPost()) { $xactions = array(); $name = $request->getStr('name'); $start_value = AphrontFormDateControlValue::newFromRequest( $request, 'start'); $end_value = AphrontFormDateControlValue::newFromRequest( $request, 'end'); $description = $request->getStr('description'); $subscribers = $request->getArr('subscribers'); $edit_policy = $request->getStr('editPolicy'); $view_policy = $request->getStr('viewPolicy'); + $is_recurring = $request->getStr('isRecurring') ? 1 : 0; + $frequency = $request->getStr('frequency'); $is_all_day = $request->getStr('isAllDay'); $icon = $request->getStr('icon'); $invitees = $request->getArr('invitees'); $new_invitees = $this->getNewInviteeList($invitees, $event); $status_attending = PhabricatorCalendarEventInvitee::STATUS_ATTENDING; if ($this->isCreate()) { $status = idx($new_invitees, $viewer->getPHID()); if ($status) { $new_invitees[$viewer->getPHID()] = $status_attending; } } $xactions[] = id(new PhabricatorCalendarEventTransaction()) ->setTransactionType( PhabricatorCalendarEventTransaction::TYPE_NAME) ->setNewValue($name); + $xactions[] = id(new PhabricatorCalendarEventTransaction()) + ->setTransactionType( + PhabricatorCalendarEventTransaction::TYPE_RECURRING) + ->setNewValue($is_recurring); + + $xactions[] = id(new PhabricatorCalendarEventTransaction()) + ->setTransactionType( + PhabricatorCalendarEventTransaction::TYPE_FREQUENCY) + ->setNewValue(array('rule' => $frequency)); + $xactions[] = id(new PhabricatorCalendarEventTransaction()) ->setTransactionType( PhabricatorCalendarEventTransaction::TYPE_ALL_DAY) ->setNewValue($is_all_day); $xactions[] = id(new PhabricatorCalendarEventTransaction()) ->setTransactionType( PhabricatorCalendarEventTransaction::TYPE_ICON) ->setNewValue($icon); $xactions[] = id(new PhabricatorCalendarEventTransaction()) ->setTransactionType( PhabricatorCalendarEventTransaction::TYPE_START_DATE) ->setNewValue($start_value); $xactions[] = id(new PhabricatorCalendarEventTransaction()) ->setTransactionType( PhabricatorCalendarEventTransaction::TYPE_END_DATE) ->setNewValue($end_value); $xactions[] = id(new PhabricatorCalendarEventTransaction()) ->setTransactionType( PhabricatorTransactions::TYPE_SUBSCRIBERS) ->setNewValue(array('=' => array_fuse($subscribers))); $xactions[] = id(new PhabricatorCalendarEventTransaction()) ->setTransactionType( PhabricatorCalendarEventTransaction::TYPE_INVITE) ->setNewValue($new_invitees); $xactions[] = id(new PhabricatorCalendarEventTransaction()) ->setTransactionType( PhabricatorCalendarEventTransaction::TYPE_DESCRIPTION) ->setNewValue($description); $xactions[] = id(new PhabricatorCalendarEventTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY) ->setNewValue($request->getStr('viewPolicy')); $xactions[] = id(new PhabricatorCalendarEventTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_EDIT_POLICY) ->setNewValue($request->getStr('editPolicy')); $editor = id(new PhabricatorCalendarEventEditor()) ->setActor($viewer) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true); try { $xactions = $editor->applyTransactions($event, $xactions); $response = id(new AphrontRedirectResponse()); switch ($next_workflow) { case 'day': if (!$uri_query) { $uri_query = 'month'; } $year = $start_value->getDateTime()->format('Y'); $month = $start_value->getDateTime()->format('m'); $day = $start_value->getDateTime()->format('d'); $response->setURI( '/calendar/query/'.$uri_query.'/'.$year.'/'.$month.'/'.$day.'/'); break; default: $response->setURI('/E'.$event->getID()); break; } return $response; } catch (PhabricatorApplicationTransactionValidationException $ex) { $validation_exception = $ex; $error_name = $ex->getShortMessage( PhabricatorCalendarEventTransaction::TYPE_NAME); $error_start_date = $ex->getShortMessage( PhabricatorCalendarEventTransaction::TYPE_START_DATE); $error_end_date = $ex->getShortMessage( PhabricatorCalendarEventTransaction::TYPE_END_DATE); $event->setViewPolicy($view_policy); $event->setEditPolicy($edit_policy); } } - Javelin::initBehavior('event-all-day', array( - 'allDayID' => $all_day_id, - 'startDateID' => $start_date_id, - 'endDateID' => $end_date_id, - )); - $name = id(new AphrontFormTextControl()) ->setLabel(pht('Name')) ->setName('name') ->setValue($name) ->setError($error_name); + Javelin::initBehavior('recurring-edit', array( + 'isRecurring' => $is_recurring_id, + 'frequency' => $frequency_id, + )); + + $is_recurring_checkbox = id(new AphrontFormCheckboxControl()) + ->addCheckbox( + 'isRecurring', + 1, + pht('Recurring Event'), + $is_recurring, + $is_recurring_id); + + $recurrence_frequency_select = id(new AphrontFormSelectControl()) + ->setName('frequency') + ->setOptions(array( + 'daily' => pht('Daily'), + 'weekly' => pht('Weekly'), + 'monthly' => pht('Monthly'), + 'yearly' => pht('Yearly'), + )) + ->setValue($frequency) + ->setLabel(pht('Recurring Event Frequency')) + ->setID($frequency_id) + ->setDisabled(!$is_recurring); + + Javelin::initBehavior('event-all-day', array( + 'allDayID' => $all_day_id, + 'startDateID' => $start_date_id, + 'endDateID' => $end_date_id, + )); + $all_day_checkbox = id(new AphrontFormCheckboxControl()) ->addCheckbox( 'isAllDay', 1, pht('All Day Event'), $is_all_day, $all_day_id); $start_control = id(new AphrontFormDateControl()) ->setUser($viewer) ->setName('start') ->setLabel(pht('Start')) ->setError($error_start_date) ->setValue($start_value) ->setID($start_date_id) ->setIsTimeDisabled($is_all_day) ->setEndDateID($end_date_id); $end_control = id(new AphrontFormDateControl()) ->setUser($viewer) ->setName('end') ->setLabel(pht('End')) ->setError($error_end_date) ->setValue($end_value) ->setID($end_date_id) ->setIsTimeDisabled($is_all_day); $description = id(new AphrontFormTextAreaControl()) ->setLabel(pht('Description')) ->setName('description') ->setValue($description); $view_policies = id(new AphrontFormPolicyControl()) ->setUser($viewer) ->setCapability(PhabricatorPolicyCapability::CAN_VIEW) ->setPolicyObject($event) ->setPolicies($current_policies) ->setName('viewPolicy'); $edit_policies = id(new AphrontFormPolicyControl()) ->setUser($viewer) ->setCapability(PhabricatorPolicyCapability::CAN_EDIT) ->setPolicyObject($event) ->setPolicies($current_policies) ->setName('editPolicy'); $subscribers = id(new AphrontFormTokenizerControl()) ->setLabel(pht('Subscribers')) ->setName('subscribers') ->setValue($subscribers) ->setUser($viewer) ->setDatasource(new PhabricatorMetaMTAMailableDatasource()); $invitees = id(new AphrontFormTokenizerControl()) ->setLabel(pht('Invitees')) ->setName('invitees') ->setValue($invitees) ->setUser($viewer) ->setDatasource(new PhabricatorMetaMTAMailableDatasource()); if ($this->isCreate()) { $icon_uri = $this->getApplicationURI('icon/'); } else { $icon_uri = $this->getApplicationURI('icon/'.$event->getID().'/'); } $icon_display = PhabricatorCalendarIcon::renderIconForChooser($icon); $icon = id(new AphrontFormChooseButtonControl()) ->setLabel(pht('Icon')) ->setName('icon') ->setDisplayValue($icon_display) ->setButtonText(pht('Choose Icon...')) ->setChooseURI($icon_uri) ->setValue($icon); $form = id(new AphrontFormView()) ->addHiddenInput('next', $next_workflow) ->addHiddenInput('query', $uri_query) ->setUser($viewer) ->appendChild($name) + ->appendChild($is_recurring_checkbox) + ->appendChild($recurrence_frequency_select) ->appendChild($all_day_checkbox) ->appendChild($start_control) ->appendChild($end_control) ->appendControl($view_policies) ->appendControl($edit_policies) ->appendControl($subscribers) ->appendControl($invitees) ->appendChild($description) ->appendChild($icon); if ($request->isAjax()) { return $this->newDialog() ->setTitle($page_title) ->setWidth(AphrontDialogView::WIDTH_FULL) ->appendForm($form) ->addCancelButton($cancel_uri) ->addSubmitButton($submit_label); } $submit = id(new AphrontFormSubmitControl()) ->addCancelButton($cancel_uri) ->setValue($submit_label); $form->appendChild($submit); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($page_title) ->setForm($form); $crumbs = $this->buildApplicationCrumbs(); if (!$this->isCreate()) { $crumbs->addTextCrumb('E'.$event->getId(), '/E'.$event->getId()); } $crumbs->addTextCrumb($page_title); $object_box = id(new PHUIObjectBoxView()) ->setHeaderText($page_title) ->setValidationException($validation_exception) ->appendChild($form); return $this->buildApplicationPage( array( $crumbs, $object_box, ), array( 'title' => $page_title, )); } public function getNewInviteeList(array $phids, $event) { $invitees = $event->getInvitees(); $invitees = mpull($invitees, null, 'getInviteePHID'); $invited_status = PhabricatorCalendarEventInvitee::STATUS_INVITED; $uninvited_status = PhabricatorCalendarEventInvitee::STATUS_UNINVITED; $phids = array_fuse($phids); $new = array(); foreach ($phids as $phid) { $old_status = $event->getUserInviteStatus($phid); if ($old_status != $uninvited_status) { continue; } $new[$phid] = $invited_status; } foreach ($invitees as $invitee) { $deleted_invitee = !idx($phids, $invitee->getInviteePHID()); if ($deleted_invitee) { $new[$invitee->getInviteePHID()] = $uninvited_status; } } return $new; } private function getDefaultTimeValues($viewer) { $start = new DateTime('@'.time()); $start->setTimeZone($viewer->getTimeZone()); $start->setTime($start->format('H'), 0, 0); $start->modify('+1 hour'); $end = id(clone $start)->modify('+1 hour'); $start_value = AphrontFormDateControlValue::newFromEpoch( $viewer, $start->format('U')); $end_value = AphrontFormDateControlValue::newFromEpoch( $viewer, $end->format('U')); return array($start_value, $end_value); } } diff --git a/src/applications/calendar/controller/PhabricatorCalendarEventViewController.php b/src/applications/calendar/controller/PhabricatorCalendarEventViewController.php index 656446c2a3..c6bb1a5ae8 100644 --- a/src/applications/calendar/controller/PhabricatorCalendarEventViewController.php +++ b/src/applications/calendar/controller/PhabricatorCalendarEventViewController.php @@ -1,281 +1,297 @@ id = $data['id']; } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); + $sequence = $request->getURIData('sequence'); + $event = id(new PhabricatorCalendarEventQuery()) ->setViewer($viewer) ->withIDs(array($this->id)) ->executeOne(); if (!$event) { return new Aphront404Response(); } + if ($sequence && $event->getIsRecurring()) { + $event = $event->generateNthGhost($sequence, $viewer); + } else if ($sequence) { + return new Aphront404Response(); + } + $title = 'E'.$event->getID(); $page_title = $title.' '.$event->getName(); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb($title, '/E'.$event->getID()); $timeline = $this->buildTransactionTimeline( $event, new PhabricatorCalendarEventTransactionQuery()); $header = $this->buildHeaderView($event); $actions = $this->buildActionView($event); $properties = $this->buildPropertyView($event); $properties->setActionList($actions); $box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); $add_comment_header = $is_serious ? pht('Add Comment') : pht('Add To Plate'); $draft = PhabricatorDraft::newFromUserAndKey($viewer, $event->getPHID()); $add_comment_form = id(new PhabricatorApplicationTransactionCommentView()) ->setUser($viewer) ->setObjectPHID($event->getPHID()) ->setDraft($draft) ->setHeaderText($add_comment_header) ->setAction( $this->getApplicationURI('/event/comment/'.$event->getID().'/')) ->setSubmitButtonName(pht('Add Comment')); return $this->buildApplicationPage( array( $crumbs, $box, $timeline, $add_comment_form, ), array( 'title' => $page_title, )); } private function buildHeaderView(PhabricatorCalendarEvent $event) { $viewer = $this->getRequest()->getUser(); $id = $event->getID(); $is_cancelled = $event->getIsCancelled(); $icon = $is_cancelled ? ('fa-times') : ('fa-calendar'); $color = $is_cancelled ? ('grey') : ('green'); $status = $is_cancelled ? pht('Cancelled') : pht('Active'); $invite_status = $event->getUserInviteStatus($viewer->getPHID()); $status_invited = PhabricatorCalendarEventInvitee::STATUS_INVITED; $is_invite_pending = ($invite_status == $status_invited); $header = id(new PHUIHeaderView()) ->setUser($viewer) ->setHeader($event->getName()) ->setStatus($icon, $color, $status) ->setPolicyObject($event); if ($is_invite_pending) { $decline_button = id(new PHUIButtonView()) ->setTag('a') ->setIcon(id(new PHUIIconView()) ->setIconFont('fa-times grey')) ->setHref($this->getApplicationURI("/event/decline/{$id}/")) ->setWorkflow(true) ->setText(pht('Decline')); $accept_button = id(new PHUIButtonView()) ->setTag('a') ->setIcon(id(new PHUIIconView()) ->setIconFont('fa-check green')) ->setHref($this->getApplicationURI("/event/accept/{$id}/")) ->setWorkflow(true) ->setText(pht('Accept')); $header->addActionLink($decline_button) ->addActionLink($accept_button); } return $header; } private function buildActionView(PhabricatorCalendarEvent $event) { $viewer = $this->getRequest()->getUser(); $id = $event->getID(); $is_cancelled = $event->getIsCancelled(); $is_attending = $event->getIsUserAttending($viewer->getPHID()); $actions = id(new PhabricatorActionListView()) ->setObjectURI($this->getApplicationURI('event/'.$id.'/')) ->setUser($viewer) ->setObject($event); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $event, PhabricatorPolicyCapability::CAN_EDIT); - $actions->addAction( - id(new PhabricatorActionView()) - ->setName(pht('Edit Event')) - ->setIcon('fa-pencil') - ->setHref($this->getApplicationURI("event/edit/{$id}/")) - ->setDisabled(!$can_edit) - ->setWorkflow(!$can_edit)); + if (!$event->getIsGhostEvent()) { + $actions->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Edit Event')) + ->setIcon('fa-pencil') + ->setHref($this->getApplicationURI("event/edit/{$id}/")) + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit)); + } if ($is_attending) { $actions->addAction( id(new PhabricatorActionView()) ->setName(pht('Decline Event')) ->setIcon('fa-user-times') ->setHref($this->getApplicationURI("event/join/{$id}/")) ->setWorkflow(true)); } else { $actions->addAction( id(new PhabricatorActionView()) ->setName(pht('Join Event')) ->setIcon('fa-user-plus') ->setHref($this->getApplicationURI("event/join/{$id}/")) ->setWorkflow(true)); } if ($is_cancelled) { $actions->addAction( id(new PhabricatorActionView()) ->setName(pht('Reinstate Event')) ->setIcon('fa-plus') ->setHref($this->getApplicationURI("event/cancel/{$id}/")) ->setDisabled(!$can_edit) ->setWorkflow(true)); } else { $actions->addAction( id(new PhabricatorActionView()) ->setName(pht('Cancel Event')) ->setIcon('fa-times') ->setHref($this->getApplicationURI("event/cancel/{$id}/")) ->setDisabled(!$can_edit) ->setWorkflow(true)); } return $actions; } private function buildPropertyView(PhabricatorCalendarEvent $event) { $viewer = $this->getRequest()->getUser(); $properties = id(new PHUIPropertyListView()) ->setUser($viewer) ->setObject($event); if ($event->getIsAllDay()) { $date_start = phabricator_date($event->getDateFrom(), $viewer); $date_end = phabricator_date($event->getDateTo(), $viewer); if ($date_start == $date_end) { $properties->addProperty( pht('Time'), phabricator_date($event->getDateFrom(), $viewer)); } else { $properties->addProperty( pht('Starts'), phabricator_date($event->getDateFrom(), $viewer)); $properties->addProperty( pht('Ends'), phabricator_date($event->getDateTo(), $viewer)); } } else { $properties->addProperty( pht('Starts'), phabricator_datetime($event->getDateFrom(), $viewer)); $properties->addProperty( pht('Ends'), phabricator_datetime($event->getDateTo(), $viewer)); } + if ($event->getIsRecurring()) { + $properties->addProperty( + pht('Recurs'), + idx($event->getRecurrenceFrequency(), 'rule')); + } + $properties->addProperty( pht('Host'), $viewer->renderHandle($event->getUserPHID())); $invitees = $event->getInvitees(); foreach ($invitees as $key => $invitee) { if ($invitee->isUninvited()) { unset($invitees[$key]); } } if ($invitees) { $invitee_list = new PHUIStatusListView(); $icon_invited = PHUIStatusItemView::ICON_OPEN; $icon_attending = PHUIStatusItemView::ICON_ACCEPT; $icon_declined = PHUIStatusItemView::ICON_REJECT; $status_invited = PhabricatorCalendarEventInvitee::STATUS_INVITED; $status_attending = PhabricatorCalendarEventInvitee::STATUS_ATTENDING; $status_declined = PhabricatorCalendarEventInvitee::STATUS_DECLINED; $icon_map = array( $status_invited => $icon_invited, $status_attending => $icon_attending, $status_declined => $icon_declined, ); $icon_color_map = array( $status_invited => null, $status_attending => 'green', $status_declined => 'red', ); foreach ($invitees as $invitee) { $item = new PHUIStatusItemView(); $invitee_phid = $invitee->getInviteePHID(); $status = $invitee->getStatus(); $target = $viewer->renderHandle($invitee_phid); $icon = $icon_map[$status]; $icon_color = $icon_color_map[$status]; $item->setIcon($icon, $icon_color) ->setTarget($target); $invitee_list->addItem($item); } } else { $invitee_list = phutil_tag( 'em', array(), pht('None')); } $properties->addProperty( pht('Invitees'), $invitee_list); $properties->invokeWillRenderEvent(); $icon_display = PhabricatorCalendarIcon::renderIconForChooser( $event->getIcon()); $properties->addProperty( pht('Icon'), $icon_display); $properties->addSectionHeader( pht('Description'), PHUIPropertyListView::ICON_SUMMARY); $properties->addTextContent($event->getDescription()); return $properties; } } diff --git a/src/applications/calendar/editor/PhabricatorCalendarEventEditor.php b/src/applications/calendar/editor/PhabricatorCalendarEventEditor.php index 291f6c1865..ea0b886b35 100644 --- a/src/applications/calendar/editor/PhabricatorCalendarEventEditor.php +++ b/src/applications/calendar/editor/PhabricatorCalendarEventEditor.php @@ -1,388 +1,429 @@ getTransactionType()) { + case PhabricatorCalendarEventTransaction::TYPE_RECURRING: + return $object->getIsRecurring(); + case PhabricatorCalendarEventTransaction::TYPE_FREQUENCY: + return $object->getRecurrenceFrequency(); + case PhabricatorCalendarEventTransaction::TYPE_RECURRENCE_END_DATE: + return $object->getRecurrenceEndDate(); + case PhabricatorCalendarEventTransaction::TYPE_INSTANCE_OF_EVENT: + return $object->getInstanceOfEventPHID(); + case PhabricatorCalendarEventTransaction::TYPE_SEQUENCE_INDEX: + return $object->getSequenceIndex(); case PhabricatorCalendarEventTransaction::TYPE_NAME: return $object->getName(); case PhabricatorCalendarEventTransaction::TYPE_START_DATE: return $object->getDateFrom(); case PhabricatorCalendarEventTransaction::TYPE_END_DATE: return $object->getDateTo(); case PhabricatorCalendarEventTransaction::TYPE_DESCRIPTION: return $object->getDescription(); case PhabricatorCalendarEventTransaction::TYPE_CANCEL: return $object->getIsCancelled(); case PhabricatorCalendarEventTransaction::TYPE_ALL_DAY: return (int)$object->getIsAllDay(); case PhabricatorCalendarEventTransaction::TYPE_ICON: return $object->getIcon(); case PhabricatorCalendarEventTransaction::TYPE_INVITE: $map = $xaction->getNewValue(); $phids = array_keys($map); $invitees = mpull($object->getInvitees(), null, 'getInviteePHID'); $old = array(); foreach ($phids as $phid) { $invitee = idx($invitees, $phid); if ($invitee) { $old[$phid] = $invitee->getStatus(); } else { $old[$phid] = PhabricatorCalendarEventInvitee::STATUS_UNINVITED; } } return $old; } return parent::getCustomTransactionOldValue($object, $xaction); } protected function getCustomTransactionNewValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { + case PhabricatorCalendarEventTransaction::TYPE_RECURRING: + case PhabricatorCalendarEventTransaction::TYPE_FREQUENCY: + case PhabricatorCalendarEventTransaction::TYPE_RECURRENCE_END_DATE: + case PhabricatorCalendarEventTransaction::TYPE_INSTANCE_OF_EVENT: + case PhabricatorCalendarEventTransaction::TYPE_SEQUENCE_INDEX: case PhabricatorCalendarEventTransaction::TYPE_NAME: case PhabricatorCalendarEventTransaction::TYPE_DESCRIPTION: case PhabricatorCalendarEventTransaction::TYPE_CANCEL: case PhabricatorCalendarEventTransaction::TYPE_INVITE: case PhabricatorCalendarEventTransaction::TYPE_ICON: return $xaction->getNewValue(); case PhabricatorCalendarEventTransaction::TYPE_ALL_DAY: return (int)$xaction->getNewValue(); case PhabricatorCalendarEventTransaction::TYPE_START_DATE: case PhabricatorCalendarEventTransaction::TYPE_END_DATE: return $xaction->getNewValue()->getEpoch(); } return parent::getCustomTransactionNewValue($object, $xaction); } protected function applyCustomInternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { + case PhabricatorCalendarEventTransaction::TYPE_RECURRING: + return $object->setIsRecurring($xaction->getNewValue()); + case PhabricatorCalendarEventTransaction::TYPE_FREQUENCY: + return $object->setRecurrenceFrequency($xaction->getNewValue()); + case PhabricatorCalendarEventTransaction::TYPE_RECURRENCE_END_DATE: + return $object->setRecurrenceEndDate($xaction->getNewValue()); + case PhabricatorCalendarEventTransaction::TYPE_INSTANCE_OF_EVENT: + return $object->setInstanceOfEventPHID($xaction->getNewValue()); + case PhabricatorCalendarEventTransaction::TYPE_SEQUENCE_INDEX: + return $object->setSequenceIndex($xaction->getNewValue()); case PhabricatorCalendarEventTransaction::TYPE_NAME: $object->setName($xaction->getNewValue()); return; case PhabricatorCalendarEventTransaction::TYPE_START_DATE: $object->setDateFrom($xaction->getNewValue()); return; case PhabricatorCalendarEventTransaction::TYPE_END_DATE: $object->setDateTo($xaction->getNewValue()); return; case PhabricatorCalendarEventTransaction::TYPE_DESCRIPTION: $object->setDescription($xaction->getNewValue()); return; case PhabricatorCalendarEventTransaction::TYPE_CANCEL: $object->setIsCancelled((int)$xaction->getNewValue()); return; case PhabricatorCalendarEventTransaction::TYPE_ALL_DAY: $object->setIsAllDay((int)$xaction->getNewValue()); return; case PhabricatorCalendarEventTransaction::TYPE_ICON: $object->setIcon($xaction->getNewValue()); return; case PhabricatorCalendarEventTransaction::TYPE_INVITE: return; } return parent::applyCustomInternalTransaction($object, $xaction); } protected function applyCustomExternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { + case PhabricatorCalendarEventTransaction::TYPE_RECURRING: + case PhabricatorCalendarEventTransaction::TYPE_FREQUENCY: + case PhabricatorCalendarEventTransaction::TYPE_RECURRENCE_END_DATE: + case PhabricatorCalendarEventTransaction::TYPE_INSTANCE_OF_EVENT: + case PhabricatorCalendarEventTransaction::TYPE_SEQUENCE_INDEX: case PhabricatorCalendarEventTransaction::TYPE_NAME: case PhabricatorCalendarEventTransaction::TYPE_START_DATE: case PhabricatorCalendarEventTransaction::TYPE_END_DATE: case PhabricatorCalendarEventTransaction::TYPE_DESCRIPTION: case PhabricatorCalendarEventTransaction::TYPE_CANCEL: case PhabricatorCalendarEventTransaction::TYPE_ALL_DAY: case PhabricatorCalendarEventTransaction::TYPE_ICON: return; case PhabricatorCalendarEventTransaction::TYPE_INVITE: $map = $xaction->getNewValue(); $phids = array_keys($map); $invitees = $object->getInvitees(); $invitees = mpull($invitees, null, 'getInviteePHID'); foreach ($phids as $phid) { $invitee = idx($invitees, $phid); if (!$invitee) { $invitee = id(new PhabricatorCalendarEventInvitee()) ->setEventPHID($object->getPHID()) ->setInviteePHID($phid) ->setInviterPHID($this->getActingAsPHID()); $invitees[] = $invitee; } $invitee->setStatus($map[$phid]) ->save(); } $object->attachInvitees($invitees); return; } return parent::applyCustomExternalTransaction($object, $xaction); } protected function didApplyInternalEffects( PhabricatorLiskDAO $object, array $xactions) { $object->removeViewerTimezone($this->requireActor()); return $xactions; } protected function applyFinalEffects( PhabricatorLiskDAO $object, array $xactions) { // Clear the availability caches for users whose availability is affected // by this edit. $invalidate_all = false; $invalidate_phids = array(); foreach ($xactions as $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorCalendarEventTransaction::TYPE_ICON: break; + case PhabricatorCalendarEventTransaction::TYPE_RECURRING: + case PhabricatorCalendarEventTransaction::TYPE_FREQUENCY: + case PhabricatorCalendarEventTransaction::TYPE_RECURRENCE_END_DATE: + case PhabricatorCalendarEventTransaction::TYPE_INSTANCE_OF_EVENT: + case PhabricatorCalendarEventTransaction::TYPE_SEQUENCE_INDEX: case PhabricatorCalendarEventTransaction::TYPE_START_DATE: case PhabricatorCalendarEventTransaction::TYPE_END_DATE: case PhabricatorCalendarEventTransaction::TYPE_CANCEL: case PhabricatorCalendarEventTransaction::TYPE_ALL_DAY: // For these kinds of changes, we need to invalidate the availabilty // caches for all attendees. $invalidate_all = true; break; case PhabricatorCalendarEventTransaction::TYPE_INVITE: foreach ($xaction->getNewValue() as $phid => $ignored) { $invalidate_phids[$phid] = $phid; } break; } } $phids = mpull($object->getInvitees(), 'getInviteePHID'); $phids = array_fuse($phids); if (!$invalidate_all) { $phids = array_select_keys($phids, $invalidate_phids); } if ($phids) { $user = new PhabricatorUser(); $conn_w = $user->establishConnection('w'); queryfx( $conn_w, 'UPDATE %T SET availabilityCacheTTL = NULL WHERE phid IN (%Ls) AND availabilityCacheTTL >= %d', $user->getTableName(), $phids, $object->getDateFromForCache()); } return $xactions; } protected function validateAllTransactions( PhabricatorLiskDAO $object, array $xactions) { $start_date_xaction = PhabricatorCalendarEventTransaction::TYPE_START_DATE; $end_date_xaction = PhabricatorCalendarEventTransaction::TYPE_END_DATE; $start_date = $object->getDateFrom(); $end_date = $object->getDateTo(); $errors = array(); foreach ($xactions as $xaction) { if ($xaction->getTransactionType() == $start_date_xaction) { $start_date = $xaction->getNewValue()->getEpoch(); } else if ($xaction->getTransactionType() == $end_date_xaction) { $end_date = $xaction->getNewValue()->getEpoch(); } } if ($start_date > $end_date) { $type = PhabricatorCalendarEventTransaction::TYPE_END_DATE; $errors[] = new PhabricatorApplicationTransactionValidationError( $type, pht('Invalid'), pht('End date must be after start date.'), null); } return $errors; } protected function validateTransaction( PhabricatorLiskDAO $object, $type, array $xactions) { $errors = parent::validateTransaction($object, $type, $xactions); switch ($type) { case PhabricatorCalendarEventTransaction::TYPE_NAME: $missing = $this->validateIsEmptyTextField( $object->getName(), $xactions); if ($missing) { $error = new PhabricatorApplicationTransactionValidationError( $type, pht('Required'), pht('Event name is required.'), nonempty(last($xactions), null)); $error->setIsMissingFieldError(true); $errors[] = $error; } break; case PhabricatorCalendarEventTransaction::TYPE_START_DATE: case PhabricatorCalendarEventTransaction::TYPE_END_DATE: foreach ($xactions as $xaction) { $date_value = $xaction->getNewValue(); if (!$date_value->isValid()) { $errors[] = new PhabricatorApplicationTransactionValidationError( $type, pht('Invalid'), pht('Invalid date.'), $xaction); } } break; } return $errors; } protected function shouldPublishFeedStory( PhabricatorLiskDAO $object, array $xactions) { return true; } protected function supportsSearch() { return true; } protected function shouldSendMail( PhabricatorLiskDAO $object, array $xactions) { $xactions = mfilter($xactions, 'shouldHide', true); return $xactions; } protected function getMailSubjectPrefix() { return pht('[Calendar]'); } protected function getMailTo(PhabricatorLiskDAO $object) { $phids = array(); if ($object->getUserPHID()) { $phids[] = $object->getUserPHID(); } $phids[] = $this->getActingAsPHID(); $invitees = $object->getInvitees(); foreach ($invitees as $invitee) { $status = $invitee->getStatus(); if ($status === PhabricatorCalendarEventInvitee::STATUS_ATTENDING || $status === PhabricatorCalendarEventInvitee::STATUS_INVITED) { $phids[] = $invitee->getInviteePHID(); } } $phids = array_unique($phids); return $phids; } public function getMailTagsMap() { return array( PhabricatorCalendarEventTransaction::MAILTAG_CONTENT => pht( "An event's name, status, invite list, ". "icon, and description changes."), PhabricatorCalendarEventTransaction::MAILTAG_RESCHEDULE => pht( "An event's start and end date ". "and cancellation status changes."), PhabricatorCalendarEventTransaction::MAILTAG_OTHER => pht('Other event activity not listed above occurs.'), ); } protected function buildReplyHandler(PhabricatorLiskDAO $object) { return id(new PhabricatorCalendarReplyHandler()) ->setMailReceiver($object); } protected function buildMailTemplate(PhabricatorLiskDAO $object) { $id = $object->getID(); $name = $object->getName(); return id(new PhabricatorMetaMTAMail()) ->setSubject("E{$id}: {$name}") ->addHeader('Thread-Topic', "E{$id}: ".$object->getName()); } protected function buildMailBody( PhabricatorLiskDAO $object, array $xactions) { $description = $object->getDescription(); $body = parent::buildMailBody($object, $xactions); if (strlen($description)) { $body->addTextSection( pht('EVENT DESCRIPTION'), $object->getDescription()); } $body->addLinkSection( pht('EVENT DETAIL'), PhabricatorEnv::getProductionURI('/E'.$object->getID())); return $body; } } diff --git a/src/applications/calendar/query/PhabricatorCalendarEventQuery.php b/src/applications/calendar/query/PhabricatorCalendarEventQuery.php index bdc87aaf26..47d3466bd4 100644 --- a/src/applications/calendar/query/PhabricatorCalendarEventQuery.php +++ b/src/applications/calendar/query/PhabricatorCalendarEventQuery.php @@ -1,220 +1,285 @@ generateGhosts = $generate_ghosts; + return $this; + } + public function withIDs(array $ids) { $this->ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withDateRange($begin, $end) { $this->rangeBegin = $begin; $this->rangeEnd = $end; return $this; } public function withInvitedPHIDs(array $phids) { $this->inviteePHIDs = $phids; return $this; } public function withCreatorPHIDs(array $phids) { $this->creatorPHIDs = $phids; return $this; } public function withIsCancelled($is_cancelled) { $this->isCancelled = $is_cancelled; return $this; } protected function getDefaultOrderVector() { return array('start', 'id'); } public function getOrderableColumns() { return array( 'start' => array( 'table' => $this->getPrimaryTableAlias(), 'column' => 'dateFrom', 'reverse' => true, 'type' => 'int', 'unique' => false, ), ) + parent::getOrderableColumns(); } protected function getPagingValueMap($cursor, array $keys) { $event = $this->loadCursorObject($cursor); return array( 'start' => $event->getDateFrom(), 'id' => $event->getID(), ); } protected function loadPage() { $table = new PhabricatorCalendarEvent(); $conn_r = $table->establishConnection('r'); + $viewer = $this->getViewer(); $data = queryfx_all( $conn_r, 'SELECT event.* FROM %T event %Q %Q %Q %Q %Q', $table->getTableName(), $this->buildJoinClause($conn_r), $this->buildWhereClause($conn_r), $this->buildGroupClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); $events = $table->loadAllFromArray($data); foreach ($events as $event) { $event->applyViewerTimezone($this->getViewer()); } + if (!$this->generateGhosts) { + return $events; + } + + foreach ($events as $event) { + $sequence_start = 0; + $instance_count = null; + + if ($event->getIsRecurring()) { + $frequency = $event->getFrequencyUnit(); + $modify_key = '+1 '.$frequency; + + if ($this->rangeBegin && $this->rangeBegin > $event->getDateFrom()) { + $max_date = $this->rangeBegin; + $date = $event->getDateFrom(); + $datetime = PhabricatorTime::getDateTimeFromEpoch($date, $viewer); + + while ($date < $max_date) { + // TODO: optimize this to not loop through all off-screen events + $sequence_start++; + $datetime = PhabricatorTime::getDateTimeFromEpoch($date, $viewer); + $date = $datetime->modify($modify_key)->format('U'); + } + + $start = $this->rangeBegin; + } else { + $start = $event->getDateFrom(); + } + + $date = $start; + $start_datetime = PhabricatorTime::getDateTimeFromEpoch( + $start, + $viewer); + + if ($this->rangeEnd) { + $end = $this->rangeEnd; + $instance_count = $sequence_start; + + while ($date < $end) { + $instance_count++; + $datetime = PhabricatorTime::getDateTimeFromEpoch($date, $viewer); + $datetime->modify($modify_key); + $date = $datetime->format('U'); + } + } else { + $instance_count = $this->getRawResultLimit(); + } + + $sequence_start = max(1, $sequence_start); + + $max_sequence = $sequence_start + $instance_count; + for ($index = $sequence_start; $index < $max_sequence; $index++) { + $events[] = $event->generateNthGhost($index, $viewer); + } + } + } + return $events; } protected function buildJoinClauseParts(AphrontDatabaseConnection $conn_r) { $parts = parent::buildJoinClauseParts($conn_r); if ($this->inviteePHIDs !== null) { $parts[] = qsprintf( $conn_r, 'JOIN %T invitee ON invitee.eventPHID = event.phid AND invitee.status != %s', id(new PhabricatorCalendarEventInvitee())->getTableName(), PhabricatorCalendarEventInvitee::STATUS_UNINVITED); } return $parts; } protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { $where = array(); if ($this->ids) { $where[] = qsprintf( $conn_r, 'event.id IN (%Ld)', $this->ids); } if ($this->phids) { $where[] = qsprintf( $conn_r, 'event.phid IN (%Ls)', $this->phids); } if ($this->rangeBegin) { $where[] = qsprintf( $conn_r, - 'event.dateTo >= %d', + 'event.dateTo >= %d OR event.isRecurring = 1', $this->rangeBegin); } if ($this->rangeEnd) { $where[] = qsprintf( $conn_r, 'event.dateFrom <= %d', $this->rangeEnd); } if ($this->inviteePHIDs !== null) { $where[] = qsprintf( $conn_r, 'invitee.inviteePHID IN (%Ls)', $this->inviteePHIDs); } if ($this->creatorPHIDs) { $where[] = qsprintf( $conn_r, 'event.userPHID IN (%Ls)', $this->creatorPHIDs); } if ($this->isCancelled !== null) { $where[] = qsprintf( $conn_r, 'event.isCancelled = %d', (int)$this->isCancelled); } $where[] = $this->buildPagingClause($conn_r); return $this->formatWhereClause($where); } protected function getPrimaryTableAlias() { return 'event'; } protected function shouldGroupQueryResultRows() { if ($this->inviteePHIDs !== null) { return true; } return parent::shouldGroupQueryResultRows(); } protected function getApplicationSearchObjectPHIDColumn() { return 'event.phid'; } public function getQueryApplicationClass() { return 'PhabricatorCalendarApplication'; } protected function willFilterPage(array $events) { $range_start = $this->rangeBegin; $range_end = $this->rangeEnd; foreach ($events as $key => $event) { $event_start = $event->getDateFrom(); $event_end = $event->getDateTo(); if ($range_start && $event_end < $range_start) { unset($events[$key]); } if ($range_end && $event_start > $range_end) { unset($events[$key]); } } $phids = array(); foreach ($events as $event) { $phids[] = $event->getPHID(); } $invitees = id(new PhabricatorCalendarEventInviteeQuery()) ->setViewer($this->getViewer()) ->withEventPHIDs($phids) ->execute(); $invitees = mgroup($invitees, 'getEventPHID'); foreach ($events as $event) { $event_invitees = idx($invitees, $event->getPHID(), array()); $event->attachInvitees($event_invitees); } $events = msort($events, 'getDateFrom'); return $events; } } diff --git a/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php b/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php index b42b5dcdf8..d454248935 100644 --- a/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php +++ b/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php @@ -1,512 +1,515 @@ setParameter( 'rangeStart', $this->readDateFromRequest($request, 'rangeStart')); $saved->setParameter( 'rangeEnd', $this->readDateFromRequest($request, 'rangeEnd')); $saved->setParameter( 'upcoming', $this->readBoolFromRequest($request, 'upcoming')); $saved->setParameter( 'invitedPHIDs', $this->readUsersFromRequest($request, 'invited')); $saved->setParameter( 'creatorPHIDs', $this->readUsersFromRequest($request, 'creators')); $saved->setParameter( 'isCancelled', $request->getStr('isCancelled')); $saved->setParameter( 'display', $request->getStr('display')); return $saved; } public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) { - $query = id(new PhabricatorCalendarEventQuery()); + $query = id(new PhabricatorCalendarEventQuery()) + ->setGenerateGhosts(true); $viewer = $this->requireViewer(); $timezone = new DateTimeZone($viewer->getTimezoneIdentifier()); $min_range = $this->getDateFrom($saved)->getEpoch(); $max_range = $this->getDateTo($saved)->getEpoch(); if ($this->isMonthView($saved) || $this->isDayView($saved)) { list($start_year, $start_month, $start_day) = $this->getDisplayYearAndMonthAndDay($saved); $start_day = new DateTime( "{$start_year}-{$start_month}-{$start_day}", $timezone); $next = clone $start_day; if ($this->isMonthView($saved)) { $next->modify('+1 month'); } else if ($this->isDayView($saved)) { $next->modify('+6 day'); } $display_start = $start_day->format('U'); $display_end = $next->format('U'); $preferences = $viewer->loadPreferences(); $pref_week_day = PhabricatorUserPreferences::PREFERENCE_WEEK_START_DAY; $start_of_week = $preferences->getPreference($pref_week_day, 0); $end_of_week = ($start_of_week + 6) % 7; $first_of_month = $start_day->format('w'); $last_of_month = id(clone $next)->modify('-1 day')->format('w'); if (!$min_range || ($min_range < $display_start)) { $min_range = $display_start; if ($this->isMonthView($saved) && $first_of_month !== $start_of_week) { $interim_day_num = ($first_of_month + 7 - $start_of_week) % 7; $min_range = id(clone $start_day) ->modify('-'.$interim_day_num.' days') ->format('U'); } } if (!$max_range || ($max_range > $display_end)) { $max_range = $display_end; if ($this->isMonthView($saved) && $last_of_month !== $end_of_week) { $interim_day_num = ($end_of_week + 7 - $last_of_month) % 7; $max_range = id(clone $next) ->modify('+'.$interim_day_num.' days') ->format('U'); } } } if ($saved->getParameter('upcoming')) { if ($min_range) { $min_range = max(time(), $min_range); } else { $min_range = time(); } } if ($min_range || $max_range) { $query->withDateRange($min_range, $max_range); } $invited_phids = $saved->getParameter('invitedPHIDs'); if ($invited_phids) { $query->withInvitedPHIDs($invited_phids); } $creator_phids = $saved->getParameter('creatorPHIDs'); if ($creator_phids) { $query->withCreatorPHIDs($creator_phids); } - $is_cancelled = $saved->getParameter('isCancelled'); + $is_cancelled = $saved->getParameter('isCancelled', 'active'); + switch ($is_cancelled) { case 'active': $query->withIsCancelled(false); break; case 'cancelled': $query->withIsCancelled(true); break; } return $query; } public function buildSearchForm( AphrontFormView $form, PhabricatorSavedQuery $saved) { $range_start = $this->getDateFrom($saved); $e_start = null; $range_end = $this->getDateTo($saved); $e_end = null; if (!$range_start->isValid()) { $this->addError(pht('Start date is not valid.')); $e_start = pht('Invalid'); } if (!$range_end->isValid()) { $this->addError(pht('End date is not valid.')); $e_end = pht('Invalid'); } $start_epoch = $range_start->getEpoch(); $end_epoch = $range_end->getEpoch(); if ($start_epoch && $end_epoch && ($start_epoch > $end_epoch)) { $this->addError(pht('End date must be after start date.')); $e_start = pht('Invalid'); $e_end = pht('Invalid'); } $upcoming = $saved->getParameter('upcoming'); $is_cancelled = $saved->getParameter('isCancelled', 'active'); $display = $saved->getParameter('display', 'month'); $invited_phids = $saved->getParameter('invitedPHIDs', array()); $creator_phids = $saved->getParameter('creatorPHIDs', array()); $resolution_types = array( 'active' => pht('Active Events Only'), 'cancelled' => pht('Cancelled Events Only'), 'both' => pht('Both Cancelled and Active Events'), ); $display_options = array( 'month' => pht('Month View'), 'day' => pht('Day View (beta)'), 'list' => pht('List View'), ); $form ->appendControl( id(new AphrontFormTokenizerControl()) ->setDatasource(new PhabricatorPeopleDatasource()) ->setName('creators') ->setLabel(pht('Created By')) ->setValue($creator_phids)) ->appendControl( id(new AphrontFormTokenizerControl()) ->setDatasource(new PhabricatorPeopleDatasource()) ->setName('invited') ->setLabel(pht('Invited')) ->setValue($invited_phids)) ->appendChild( id(new AphrontFormDateControl()) ->setLabel(pht('Occurs After')) ->setUser($this->requireViewer()) ->setName('rangeStart') ->setError($e_start) ->setValue($range_start)) ->appendChild( id(new AphrontFormDateControl()) ->setLabel(pht('Occurs Before')) ->setUser($this->requireViewer()) ->setName('rangeEnd') ->setError($e_end) ->setValue($range_end)) ->appendChild( id(new AphrontFormCheckboxControl()) ->addCheckbox( 'upcoming', 1, pht('Show only upcoming events.'), $upcoming)) ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Cancelled Events')) ->setName('isCancelled') ->setValue($is_cancelled) ->setOptions($resolution_types)) ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Display Options')) ->setName('display') ->setValue($display) ->setOptions($display_options)); } protected function getURI($path) { return '/calendar/'.$path; } protected function getBuiltinQueryNames() { $names = array( 'month' => pht('Month View'), 'day' => pht('Day View'), 'upcoming' => pht('Upcoming Events'), 'all' => pht('All Events'), ); return $names; } public function setCalendarYearAndMonthAndDay($year, $month, $day = null) { $this->calendarYear = $year; $this->calendarMonth = $month; $this->calendarDay = $day; return $this; } public function buildSavedQueryFromBuiltin($query_key) { $query = $this->newSavedQuery(); $query->setQueryKey($query_key); switch ($query_key) { case 'month': return $query->setParameter('display', 'month'); case 'day': return $query->setParameter('display', 'day'); case 'upcoming': return $query->setParameter('upcoming', true); case 'all': return $query; } return parent::buildSavedQueryFromBuiltin($query_key); } protected function getRequiredHandlePHIDsForResultList( array $objects, PhabricatorSavedQuery $query) { $phids = array(); foreach ($objects as $event) { $phids[$event->getUserPHID()] = 1; } return array_keys($phids); } protected function renderResultList( array $events, PhabricatorSavedQuery $query, array $handles) { if ($this->isMonthView($query)) { return $this->buildCalendarView($events, $query, $handles); } else if ($this->isDayView($query)) { return $this->buildCalendarDayView($events, $query, $handles); } assert_instances_of($events, 'PhabricatorCalendarEvent'); $viewer = $this->requireViewer(); $list = new PHUIObjectItemListView(); foreach ($events as $event) { - $href = '/E'.$event->getID(); + // $href = '/E'.$event->getID(); $from = phabricator_datetime($event->getDateFrom(), $viewer); $to = phabricator_datetime($event->getDateTo(), $viewer); $creator_handle = $handles[$event->getUserPHID()]; $item = id(new PHUIObjectItemView()) ->setHeader($event->getName()) - ->setHref($href) + ->setHref($event->getURI()) ->addByline(pht('Creator: %s', $creator_handle->renderLink())) ->addAttribute(pht('From %s to %s', $from, $to)) ->addAttribute(id(new PhutilUTF8StringTruncator()) ->setMaximumGlyphs(64) ->truncateString($event->getDescription())); $list->addItem($item); } return $list; } private function buildCalendarView( array $statuses, PhabricatorSavedQuery $query, array $handles) { $viewer = $this->requireViewer(); $now = time(); list($start_year, $start_month) = $this->getDisplayYearAndMonthAndDay($query); $now_year = phabricator_format_local_time($now, $viewer, 'Y'); $now_month = phabricator_format_local_time($now, $viewer, 'm'); $now_day = phabricator_format_local_time($now, $viewer, 'j'); if ($start_month == $now_month && $start_year == $now_year) { $month_view = new PHUICalendarMonthView( $this->getDateFrom($query), $this->getDateTo($query), $start_month, $start_year, $now_day); } else { $month_view = new PHUICalendarMonthView( $this->getDateFrom($query), $this->getDateTo($query), $start_month, $start_year); } $month_view->setUser($viewer); $phids = mpull($statuses, 'getUserPHID'); foreach ($statuses as $status) { $viewer_is_invited = $status->getIsUserInvited($viewer->getPHID()); $event = new AphrontCalendarEventView(); $event->setEpochRange($status->getDateFrom(), $status->getDateTo()); $event->setIsAllDay($status->getIsAllDay()); $event->setIcon($status->getIcon()); $name_text = $handles[$status->getUserPHID()]->getName(); $status_text = $status->getName(); $event->setUserPHID($status->getUserPHID()); $event->setDescription(pht('%s (%s)', $name_text, $status_text)); $event->setName($status_text); - $event->setEventID($status->getID()); + $event->setURI($status->getURI()); + // $event->setEventID($status->getID()); $event->setViewerIsInvited($viewer_is_invited); $month_view->addEvent($event); } $month_view->setBrowseURI( $this->getURI('query/'.$query->getQueryKey().'/')); return $month_view; } private function buildCalendarDayView( array $statuses, PhabricatorSavedQuery $query, array $handles) { $viewer = $this->requireViewer(); list($start_year, $start_month, $start_day) = $this->getDisplayYearAndMonthAndDay($query); $day_view = id(new PHUICalendarDayView( $this->getDateFrom($query), $this->getDateTo($query), $start_year, $start_month, $start_day)) ->setQuery($query->getQueryKey()); $day_view->setUser($viewer); $phids = mpull($statuses, 'getUserPHID'); foreach ($statuses as $status) { if ($status->getIsCancelled()) { continue; } $viewer_is_invited = $status->getIsUserInvited($viewer->getPHID()); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $status, PhabricatorPolicyCapability::CAN_EDIT); $event = new AphrontCalendarEventView(); $event->setCanEdit($can_edit); $event->setEventID($status->getID()); $event->setEpochRange($status->getDateFrom(), $status->getDateTo()); $event->setIsAllDay($status->getIsAllDay()); $event->setIcon($status->getIcon()); $event->setViewerIsInvited($viewer_is_invited); $event->setName($status->getName()); - $event->setURI('/'.$status->getMonogram()); + $event->setURI($status->getURI()); $day_view->addEvent($event); } $day_view->setBrowseURI( $this->getURI('query/'.$query->getQueryKey().'/')); return $day_view; } private function getDisplayYearAndMonthAndDay( PhabricatorSavedQuery $query) { $viewer = $this->requireViewer(); if ($this->calendarYear && $this->calendarMonth) { $start_year = $this->calendarYear; $start_month = $this->calendarMonth; $start_day = $this->calendarDay ? $this->calendarDay : 1; } else { $epoch = $this->getDateFrom($query)->getEpoch(); if (!$epoch) { $epoch = $this->getDateTo($query)->getEpoch(); if (!$epoch) { $epoch = time(); } } if ($this->isMonthView($query)) { $day = 1; } else { $day = phabricator_format_local_time($epoch, $viewer, 'd'); } $start_year = phabricator_format_local_time($epoch, $viewer, 'Y'); $start_month = phabricator_format_local_time($epoch, $viewer, 'm'); $start_day = $day; } return array($start_year, $start_month, $start_day); } public function getPageSize(PhabricatorSavedQuery $saved) { return $saved->getParameter('limit', 1000); } private function getDateFrom(PhabricatorSavedQuery $saved) { return $this->getDate($saved, 'rangeStart'); } private function getDateTo(PhabricatorSavedQuery $saved) { return $this->getDate($saved, 'rangeEnd'); } private function getDate(PhabricatorSavedQuery $saved, $key) { $viewer = $this->requireViewer(); $wild = $saved->getParameter($key); if ($wild) { $value = AphrontFormDateControlValue::newFromWild($viewer, $wild); } else { $value = AphrontFormDateControlValue::newFromEpoch( $viewer, PhabricatorTime::getTodayMidnightDateTime($viewer)->format('U')); $value->setEnabled(false); } $value->setOptional(true); return $value; } private function isMonthView(PhabricatorSavedQuery $query) { if ($this->isDayView($query)) { return false; } if ($query->getParameter('display') == 'month') { return true; } } private function isDayView(PhabricatorSavedQuery $query) { if ($query->getParameter('display') == 'day') { return true; } if ($this->calendarDay) { return true; } return false; } } diff --git a/src/applications/calendar/storage/PhabricatorCalendarEvent.php b/src/applications/calendar/storage/PhabricatorCalendarEvent.php index cd829369d8..5110974306 100644 --- a/src/applications/calendar/storage/PhabricatorCalendarEvent.php +++ b/src/applications/calendar/storage/PhabricatorCalendarEvent.php @@ -1,388 +1,476 @@ setViewer($actor) ->withClasses(array('PhabricatorCalendarApplication')) ->executeOne(); + $view_policy = null; + $is_recurring = 0; + if ($mode == 'public') { $view_policy = PhabricatorPolicies::getMostOpenPolicy(); + } else if ($mode == 'recurring') { + $is_recurring = true; } else { $view_policy = $actor->getPHID(); } return id(new PhabricatorCalendarEvent()) ->setUserPHID($actor->getPHID()) ->setIsCancelled(0) ->setIsAllDay(0) + ->setIsRecurring($is_recurring) ->setIcon(self::DEFAULT_ICON) ->setViewPolicy($view_policy) ->setEditPolicy($actor->getPHID()) ->attachInvitees(array()) ->applyViewerTimezone($actor); } public function applyViewerTimezone(PhabricatorUser $viewer) { if ($this->appliedViewer) { throw new Exception(pht('Viewer timezone is already applied!')); } $this->appliedViewer = $viewer; if (!$this->getIsAllDay()) { return $this; } $zone = $viewer->getTimeZone(); $this->setDateFrom( $this->getDateEpochForTimeZone( $this->getDateFrom(), new DateTimeZone('Pacific/Kiritimati'), 'Y-m-d', null, $zone)); $this->setDateTo( $this->getDateEpochForTimeZone( $this->getDateTo(), new DateTimeZone('Pacific/Midway'), 'Y-m-d 23:59:00', '-1 day', $zone)); return $this; } public function removeViewerTimezone(PhabricatorUser $viewer) { if (!$this->appliedViewer) { throw new Exception(pht('Viewer timezone is not applied!')); } if ($viewer->getPHID() != $this->appliedViewer->getPHID()) { throw new Exception(pht('Removed viewer must match applied viewer!')); } $this->appliedViewer = null; if (!$this->getIsAllDay()) { return $this; } $zone = $viewer->getTimeZone(); $this->setDateFrom( $this->getDateEpochForTimeZone( $this->getDateFrom(), $zone, 'Y-m-d', null, new DateTimeZone('Pacific/Kiritimati'))); $this->setDateTo( $this->getDateEpochForTimeZone( $this->getDateTo(), $zone, 'Y-m-d', '+1 day', new DateTimeZone('Pacific/Midway'))); return $this; } private function getDateEpochForTimeZone( $epoch, $src_zone, $format, $adjust, $dst_zone) { $src = new DateTime('@'.$epoch); $src->setTimeZone($src_zone); if (strlen($adjust)) { $adjust = ' '.$adjust; } $dst = new DateTime($src->format($format).$adjust, $dst_zone); return $dst->format('U'); } public function save() { if ($this->appliedViewer) { throw new Exception( pht( 'Can not save event with viewer timezone still applied!')); } if (!$this->mailKey) { $this->mailKey = Filesystem::readRandomCharacters(20); } return parent::save(); } /** * Get the event start epoch for evaluating invitee availability. * * When assessing availability, we pretend events start earlier than they * really. This allows us to mark users away for the entire duration of a * series of back-to-back meetings, even if they don't strictly overlap. * * @return int Event start date for availability caches. */ public function getDateFromForCache() { return ($this->getDateFrom() - phutil_units('15 minutes in seconds')); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text', 'dateFrom' => 'epoch', 'dateTo' => 'epoch', 'description' => 'text', 'isCancelled' => 'bool', 'isAllDay' => 'bool', 'icon' => 'text32', 'mailKey' => 'bytes20', + 'isRecurring' => 'bool', + 'recurrenceEndDate' => 'epoch?', + 'instanceOfEventPHID' => 'phid?', + 'sequenceIndex' => 'uint32?', ), self::CONFIG_KEY_SCHEMA => array( 'userPHID_dateFrom' => array( 'columns' => array('userPHID', 'dateTo'), ), ), + self::CONFIG_SERIALIZATION => array( + 'recurrenceFrequency' => self::SERIALIZATION_JSON, + ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorCalendarEventPHIDType::TYPECONST); } public function getMonogram() { return 'E'.$this->getID(); } public function getInvitees() { return $this->assertAttached($this->invitees); } public function attachInvitees(array $invitees) { $this->invitees = $invitees; return $this; } public function getUserInviteStatus($phid) { $invitees = $this->getInvitees(); $invitees = mpull($invitees, null, 'getInviteePHID'); $invited = idx($invitees, $phid); if (!$invited) { return PhabricatorCalendarEventInvitee::STATUS_UNINVITED; } $invited = $invited->getStatus(); return $invited; } public function getIsUserAttending($phid) { $attending_status = PhabricatorCalendarEventInvitee::STATUS_ATTENDING; $old_status = $this->getUserInviteStatus($phid); $is_attending = ($old_status == $attending_status); return $is_attending; } public function getIsUserInvited($phid) { $uninvited_status = PhabricatorCalendarEventInvitee::STATUS_UNINVITED; $declined_status = PhabricatorCalendarEventInvitee::STATUS_DECLINED; $status = $this->getUserInviteStatus($phid); if ($status == $uninvited_status || $status == $declined_status) { return false; } return true; } + public function getIsGhostEvent() { + return $this->isGhostEvent; + } + + public function setIsGhostEvent($is_ghost_event) { + $this->isGhostEvent = $is_ghost_event; + return $this; + } + + public function generateNthGhost( + $sequence_index, + PhabricatorUser $actor) { + + $frequency = $this->getFrequencyUnit(); + $modify_key = '+'.$sequence_index.' '.$frequency; + + $date = $this->dateFrom; + $date_time = PhabricatorTime::getDateTimeFromEpoch($date, $actor); + $date_time->modify($modify_key); + $date = $date_time->format('U'); + + $duration = $this->dateTo - $this->dateFrom; + + $edit_policy = PhabricatorPolicies::POLICY_NOONE; + + $ghost_event = id(clone $this) + ->setIsGhostEvent(true) + ->setDateFrom($date) + ->setDateTo($date + $duration) + ->setIsRecurring(false) + ->setRecurrenceFrequency(null) + ->setInstanceOfEventPHID($this->getPHID()) + ->setSequenceIndex($sequence_index) + ->setEditPolicy($edit_policy); + + return $ghost_event; + } + + public function getFrequencyUnit() { + $frequency = idx($this->recurrenceFrequency, 'rule'); + + switch ($frequency) { + case 'daily': + return 'day'; + case 'weekly': + return 'week'; + case 'monthly': + return 'month'; + case 'yearly': + return 'yearly'; + default: + return 'day'; + } + } + + public function getURI() { + $uri = '/'.$this->getMonogram(); + if ($this->isGhostEvent) { + $uri = $uri.'/'.$this->sequenceIndex; + } + return $uri; + } + /* -( Markup Interface )--------------------------------------------------- */ /** * @task markup */ public function getMarkupFieldKey($field) { $hash = PhabricatorHash::digest($this->getMarkupText($field)); $id = $this->getID(); return "calendar:T{$id}:{$field}:{$hash}"; } /** * @task markup */ public function getMarkupText($field) { return $this->getDescription(); } /** * @task markup */ public function newMarkupEngine($field) { return PhabricatorMarkupEngine::newCalendarMarkupEngine(); } /** * @task markup */ public function didMarkupText( $field, $output, PhutilMarkupEngine $engine) { return $output; } /** * @task markup */ public function shouldUseMarkupCache($field) { return (bool)$this->getID(); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: return $this->getEditPolicy(); } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { // The owner of a task can always view and edit it. $user_phid = $this->getUserPHID(); + if ($this->isGhostEvent) { + return false; + } if ($user_phid) { $viewer_phid = $viewer->getPHID(); if ($viewer_phid == $user_phid) { return true; } } if ($capability == PhabricatorPolicyCapability::CAN_VIEW) { $status = $this->getUserInviteStatus($viewer->getPHID()); if ($status == PhabricatorCalendarEventInvitee::STATUS_INVITED || $status == PhabricatorCalendarEventInvitee::STATUS_ATTENDING || $status == PhabricatorCalendarEventInvitee::STATUS_DECLINED) { return true; } } return false; } public function describeAutomaticCapability($capability) { return pht('The owner of an event can always view and edit it, - and invitees can always view it.'); + and invitees can always view it, except if the event is an + instance of a recurring event.'); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhabricatorCalendarEventEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new PhabricatorCalendarEventTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( PhabricatorSubscribableInterface )----------------------------------- */ public function isAutomaticallySubscribed($phid) { return ($phid == $this->getUserPHID()); } public function shouldShowSubscribersProperty() { return true; } public function shouldAllowSubscription($phid) { return true; } /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ public function getUsersToNotifyOfTokenGiven() { return array($this->getUserPHID()); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $this->delete(); $this->saveTransaction(); } } diff --git a/src/applications/calendar/storage/PhabricatorCalendarEventTransaction.php b/src/applications/calendar/storage/PhabricatorCalendarEventTransaction.php index 767208648e..64656b48e4 100644 --- a/src/applications/calendar/storage/PhabricatorCalendarEventTransaction.php +++ b/src/applications/calendar/storage/PhabricatorCalendarEventTransaction.php @@ -1,479 +1,513 @@ getTransactionType()) { case self::TYPE_NAME: case self::TYPE_START_DATE: case self::TYPE_END_DATE: case self::TYPE_DESCRIPTION: case self::TYPE_CANCEL: case self::TYPE_ALL_DAY: + case self::TYPE_RECURRING: + case self::TYPE_FREQUENCY: + case self::TYPE_RECURRENCE_END_DATE: + case self::TYPE_INSTANCE_OF_EVENT: + case self::TYPE_SEQUENCE_INDEX: $phids[] = $this->getObjectPHID(); break; case self::TYPE_INVITE: $new = $this->getNewValue(); foreach ($new as $phid => $status) { $phids[] = $phid; } break; } return $phids; } public function shouldHide() { $old = $this->getOldValue(); switch ($this->getTransactionType()) { case self::TYPE_START_DATE: case self::TYPE_END_DATE: case self::TYPE_DESCRIPTION: case self::TYPE_CANCEL: case self::TYPE_ALL_DAY: case self::TYPE_INVITE: + case self::TYPE_RECURRING: + case self::TYPE_FREQUENCY: + case self::TYPE_RECURRENCE_END_DATE: + case self::TYPE_INSTANCE_OF_EVENT: + case self::TYPE_SEQUENCE_INDEX: return ($old === null); } return parent::shouldHide(); } public function getIcon() { switch ($this->getTransactionType()) { case self::TYPE_ICON: return $this->getNewValue(); case self::TYPE_NAME: case self::TYPE_START_DATE: case self::TYPE_END_DATE: case self::TYPE_DESCRIPTION: case self::TYPE_ALL_DAY: case self::TYPE_CANCEL: + case self::TYPE_RECURRING: + case self::TYPE_FREQUENCY: + case self::TYPE_RECURRENCE_END_DATE: + case self::TYPE_INSTANCE_OF_EVENT: + case self::TYPE_SEQUENCE_INDEX: return 'fa-pencil'; break; case self::TYPE_INVITE: return 'fa-user-plus'; break; } return parent::getIcon(); } public function getTitle() { $author_phid = $this->getAuthorPHID(); $object_phid = $this->getObjectPHID(); $old = $this->getOldValue(); $new = $this->getNewValue(); $type = $this->getTransactionType(); switch ($type) { case self::TYPE_NAME: if ($old === null) { return pht( '%s created this event.', $this->renderHandleLink($author_phid)); } else { return pht( '%s changed the name of this event from %s to %s.', $this->renderHandleLink($author_phid), $old, $new); } case self::TYPE_START_DATE: if ($old) { return pht( '%s edited the start date of this event.', $this->renderHandleLink($author_phid)); } break; case self::TYPE_END_DATE: if ($old) { return pht( '%s edited the end date of this event.', $this->renderHandleLink($author_phid)); } break; case self::TYPE_DESCRIPTION: return pht( "%s updated the event's description.", $this->renderHandleLink($author_phid)); case self::TYPE_ALL_DAY: if ($new) { return pht( '%s made this an all day event.', $this->renderHandleLink($author_phid)); } else { return pht( '%s converted this from an all day event.', $this->renderHandleLink($author_phid)); } case self::TYPE_ICON: return pht( '%s set this event\'s icon to %s.', $this->renderHandleLink($author_phid), PhabricatorCalendarIcon::getLabel($new)); break; case self::TYPE_CANCEL: if ($new) { return pht( '%s cancelled this event.', $this->renderHandleLink($author_phid)); } else { return pht( '%s reinstated this event.', $this->renderHandleLink($author_phid)); } case self::TYPE_INVITE: $text = null; if (count($old) === 1 && count($new) === 1 && isset($old[$author_phid])) { // user joined/declined/accepted event themself $old_status = $old[$author_phid]; $new_status = $new[$author_phid]; if ($old_status !== $new_status) { switch ($new_status) { case PhabricatorCalendarEventInvitee::STATUS_INVITED: $text = pht( '%s has joined this event.', $this->renderHandleLink($author_phid)); break; case PhabricatorCalendarEventInvitee::STATUS_ATTENDING: $text = pht( '%s is attending this event.', $this->renderHandleLink($author_phid)); break; case PhabricatorCalendarEventInvitee::STATUS_DECLINED: case PhabricatorCalendarEventInvitee::STATUS_UNINVITED: $text = pht( '%s has declined this event.', $this->renderHandleLink($author_phid)); break; default: $text = pht( '%s has changed their status for this event.', $this->renderHandleLink($author_phid)); break; } } } else { // user changed status for many users $self_joined = null; $self_declined = null; $added = array(); $uninvited = array(); foreach ($new as $phid => $status) { if ($status == PhabricatorCalendarEventInvitee::STATUS_INVITED || $status == PhabricatorCalendarEventInvitee::STATUS_ATTENDING) { // added users $added[] = $phid; } else if ( $status == PhabricatorCalendarEventInvitee::STATUS_DECLINED || $status == PhabricatorCalendarEventInvitee::STATUS_UNINVITED) { $uninvited[] = $phid; } } $count_added = count($added); $count_uninvited = count($uninvited); $added_text = null; $uninvited_text = null; if ($count_added > 0 && $count_uninvited == 0) { $added_text = $this->renderHandleList($added); $text = pht('%s invited %s.', $this->renderHandleLink($author_phid), $added_text); } else if ($count_added > 0 && $count_uninvited > 0) { $added_text = $this->renderHandleList($added); $uninvited_text = $this->renderHandleList($uninvited); $text = pht('%s invited %s and uninvited: %s', $this->renderHandleLink($author_phid), $added_text, $uninvited_text); } else if ($count_added == 0 && $count_uninvited > 0) { $uninvited_text = $this->renderHandleList($uninvited); $text = pht('%s uninvited %s.', $this->renderHandleLink($author_phid), $uninvited_text); } else { $text = pht('%s updated the invitee list.', $this->renderHandleLink($author_phid)); } } return $text; + case self::TYPE_RECURRING: + case self::TYPE_FREQUENCY: + case self::TYPE_RECURRENCE_END_DATE: + case self::TYPE_INSTANCE_OF_EVENT: + case self::TYPE_SEQUENCE_INDEX: + return pht('Recurring event has been updated'); } return parent::getTitle(); } public function getTitleForFeed() { $author_phid = $this->getAuthorPHID(); $object_phid = $this->getObjectPHID(); $old = $this->getOldValue(); $new = $this->getNewValue(); $viewer = $this->getViewer(); $type = $this->getTransactionType(); switch ($type) { case self::TYPE_NAME: if ($old === null) { return pht( '%s created %s', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); } else { return pht( '%s changed the name of %s from %s to %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid), $old, $new); } case self::TYPE_START_DATE: if ($old) { $old = phabricator_datetime($old, $viewer); $new = phabricator_datetime($new, $viewer); return pht( '%s changed the start date of %s from %s to %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid), $old, $new); } break; case self::TYPE_END_DATE: if ($old) { $old = phabricator_datetime($old, $viewer); $new = phabricator_datetime($new, $viewer); return pht( '%s edited the end date of %s from %s to %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid), $old, $new); } break; case self::TYPE_DESCRIPTION: return pht( '%s updated the description of %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); case self::TYPE_ALL_DAY: if ($new) { return pht( '%s made %s an all day event.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); } else { return pht( '%s converted %s from an all day event.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); } case self::TYPE_ICON: return pht( '%s set the icon for %s to %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid), PhabricatorCalendarIcon::getLabel($new)); case self::TYPE_CANCEL: if ($new) { return pht( '%s cancelled %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); } else { return pht( '%s reinstated %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); } case self::TYPE_INVITE: $text = null; if (count($old) === 1 && count($new) === 1 && isset($old[$author_phid])) { // user joined/declined/accepted event themself $old_status = $old[$author_phid]; $new_status = $new[$author_phid]; if ($old_status !== $new_status) { switch ($new_status) { case PhabricatorCalendarEventInvitee::STATUS_INVITED: $text = pht( '%s has joined %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); break; case PhabricatorCalendarEventInvitee::STATUS_ATTENDING: $text = pht( '%s is attending %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); break; case PhabricatorCalendarEventInvitee::STATUS_DECLINED: case PhabricatorCalendarEventInvitee::STATUS_UNINVITED: $text = pht( '%s has declined %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); break; default: $text = pht( '%s has changed their status of %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); break; } } } else { // user changed status for many users $self_joined = null; $self_declined = null; $added = array(); $uninvited = array(); // $event = $this->renderHandleLink($object_phid); foreach ($new as $phid => $status) { if ($status == PhabricatorCalendarEventInvitee::STATUS_INVITED || $status == PhabricatorCalendarEventInvitee::STATUS_ATTENDING) { // added users $added[] = $phid; } else if ( $status == PhabricatorCalendarEventInvitee::STATUS_DECLINED || $status == PhabricatorCalendarEventInvitee::STATUS_UNINVITED) { $uninvited[] = $phid; } } $count_added = count($added); $count_uninvited = count($uninvited); $added_text = null; $uninvited_text = null; if ($count_added > 0 && $count_uninvited == 0) { $added_text = $this->renderHandleList($added); $text = pht('%s invited %s to %s.', $this->renderHandleLink($author_phid), $added_text, $this->renderHandleLink($object_phid)); } else if ($count_added > 0 && $count_uninvited > 0) { $added_text = $this->renderHandleList($added); $uninvited_text = $this->renderHandleList($uninvited); $text = pht('%s invited %s and uninvited %s to %s', $this->renderHandleLink($author_phid), $added_text, $uninvited_text, $this->renderHandleLink($object_phid)); } else if ($count_added == 0 && $count_uninvited > 0) { $uninvited_text = $this->renderHandleList($uninvited); $text = pht('%s uninvited %s to %s.', $this->renderHandleLink($author_phid), $uninvited_text, $this->renderHandleLink($object_phid)); } else { $text = pht('%s updated the invitee list of %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); } } return $text; + case self::TYPE_RECURRING: + case self::TYPE_FREQUENCY: + case self::TYPE_RECURRENCE_END_DATE: + case self::TYPE_INSTANCE_OF_EVENT: + case self::TYPE_SEQUENCE_INDEX: + return pht('Recurring event has been updated'); } return parent::getTitleForFeed(); } public function getColor() { $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case self::TYPE_NAME: case self::TYPE_START_DATE: case self::TYPE_END_DATE: case self::TYPE_DESCRIPTION: case self::TYPE_CANCEL: case self::TYPE_INVITE: return PhabricatorTransactions::COLOR_GREEN; } return parent::getColor(); } public function hasChangeDetails() { switch ($this->getTransactionType()) { case self::TYPE_DESCRIPTION: return ($this->getOldValue() !== null); } return parent::hasChangeDetails(); } public function renderChangeDetails(PhabricatorUser $viewer) { switch ($this->getTransactionType()) { case self::TYPE_DESCRIPTION: $old = $this->getOldValue(); $new = $this->getNewValue(); return $this->renderTextCorpusChangeDetails( $viewer, $old, $new); } return parent::renderChangeDetails($viewer); } public function getMailTags() { $tags = array(); switch ($this->getTransactionType()) { case self::TYPE_NAME: case self::TYPE_DESCRIPTION: case self::TYPE_INVITE: case self::TYPE_ICON: $tags[] = self::MAILTAG_CONTENT; break; case self::TYPE_START_DATE: case self::TYPE_END_DATE: case self::TYPE_CANCEL: $tags[] = self::MAILTAG_RESCHEDULE; break; } return $tags; } } diff --git a/src/view/phui/calendar/PHUICalendarListView.php b/src/view/phui/calendar/PHUICalendarListView.php index fa998f11a6..1e0639262d 100644 --- a/src/view/phui/calendar/PHUICalendarListView.php +++ b/src/view/phui/calendar/PHUICalendarListView.php @@ -1,189 +1,189 @@ view; } public function setView($view) { $this->view = $view; return $this; } public function addEvent(AphrontCalendarEventView $event) { $this->events[] = $event; return $this; } public function showBlankState($state) { $this->blankState = $state; return $this; } protected function getTagName() { return 'div'; } protected function getTagAttributes() { require_celerity_resource('phui-calendar-css'); require_celerity_resource('phui-calendar-list-css'); return array('class' => 'phui-calendar-event-list'); } protected function getTagContent() { if (!$this->blankState && empty($this->events)) { return ''; } $singletons = array(); $allday = false; foreach ($this->events as $event) { $start_epoch = $event->getEpochStart(); if ($event->getIsAllDay()) { $timelabel = pht('All Day'); } else { $timelabel = phabricator_time( $event->getEpochStart(), $this->getUser()); } if ($event->getViewerIsInvited()) { $icon_color = 'green'; } else { $icon_color = null; } $dot = id(new PHUIIconView()) ->setIconFont($event->getIcon(), $icon_color) ->addClass('phui-calendar-list-item-icon'); $title = phutil_tag( 'span', array( 'class' => 'phui-calendar-list-title', ), $this->getEventTitle($event, $allday)); $time = phutil_tag( 'span', array( 'class' => 'phui-calendar-list-time', ), $timelabel); $class = 'phui-calendar-list-item'; if ($event->getViewerIsInvited()) { $class = $class.' phui-calendar-viewer-invited'; } if ($event->getIsAllDay()) { $class = $class.' all-day'; } $tip = $this->getEventTooltip($event); $tip_align = ($this->getView() == 'day') ? 'E' : 'N'; $content = javelin_tag( 'a', array( - 'href' => '/E'.$event->getEventID(), + 'href' => $event->getURI(), 'sigil' => 'has-tooltip', 'meta' => array( 'tip' => $tip, 'size' => 200, 'align' => $tip_align, ), ), array( $dot, $time, $title, )); $singletons[] = phutil_tag( 'li', array( 'class' => $class, ), $content); } if (empty($singletons)) { $singletons[] = phutil_tag( 'li', array( 'class' => 'phui-calendar-list-item-empty', ), pht('Clear sailing ahead.')); } $list = phutil_tag( 'ul', array( 'class' => 'phui-calendar-list', ), $singletons); return $list; } private function getEventTitle($event) { $class = 'phui-calendar-item'; return phutil_tag( 'span', array( 'class' => $class, ), $event->getName()); } private function getEventTooltip(AphrontCalendarEventView $event) { Javelin::initBehavior('phabricator-tooltips'); $start = id(AphrontFormDateControlValue::newFromEpoch( $this->getUser(), $event->getEpochStart())); $end = id(AphrontFormDateControlValue::newFromEpoch( $this->getUser(), $event->getEpochEnd())); $start_date = $start->getDateTime()->format('m d Y'); $end_date = $end->getDateTime()->format('m d Y'); if ($event->getIsAllDay()) { if ($start_date == $end_date) { $tip = pht('All day'); } else { $tip = pht( 'All day, %s - %s', $start->getValueAsFormat('M j, Y'), $end->getValueAsFormat('M j, Y')); } } else { if ($start->getValueDate() == $end->getValueDate()) { $tip = pht( '%s - %s', $start->getValueAsFormat('g:i A'), $end->getValueAsFormat('g:i A')); } else { $tip = pht( '%s - %s', $start->getValueAsFormat('M j, Y, g:i A'), $end->getValueAsFormat('M j, Y, g:i A')); } } return $tip; } public function getIsViewerInvitedOnList() { foreach ($this->events as $event) { if ($event->getViewerIsInvited()) { return true; } } return false; } } diff --git a/webroot/rsrc/css/phui/phui-form.css b/webroot/rsrc/css/phui/phui-form.css index a499f344b8..a6c73117b8 100644 --- a/webroot/rsrc/css/phui/phui-form.css +++ b/webroot/rsrc/css/phui/phui-form.css @@ -1,146 +1,146 @@ /** * @provides phui-form-css */ select, textarea, input[type="text"], input[type="password"], input[type="datetime"], input[type="datetime-local"], input[type="date"], input[type="month"], input[type="time"], input[type="week"], input[type="number"], input[type="email"], input[type="url"], input[type="search"], input[type="tel"], input[type="color"], div.jx-tokenizer-container { display: inline-block; height: 28px; line-height: 18px; color: #333; vertical-align: middle; font: {$basefont}; -webkit-font-smoothing: antialiased; } textarea, input[type="text"], input[type="password"], input[type="datetime"], input[type="datetime-local"], input[type="date"], input[type="month"], input[type="time"], input[type="week"], input[type="number"], input[type="email"], input[type="url"], input[type="search"], input[type="tel"], input[type="color"], div.jx-tokenizer-container { padding: 4px 6px; background-color: #ffffff; border: 1px solid {$blueborder}; -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); -webkit-transition: border linear .05s, box-shadow linear .05s; -moz-transition: border linear .05s, box-shadow linear .05s; -o-transition: border linear .05s, box-shadow linear .05s; transition: border linear .05s, box-shadow linear .05s; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; /* iOS Safari */ -webkit-appearance: none; border-radius: 0; } textarea:focus, input[type="text"]:focus, input[type="password"]:focus, input[type="datetime"]:focus, input[type="datetime-local"]:focus, input[type="date"]:focus, input[type="month"]:focus, input[type="time"]:focus, input[type="week"]:focus, input[type="number"]:focus, input[type="email"]:focus, input[type="url"]:focus, input[type="search"]:focus, input[type="tel"]:focus, input[type="color"]:focus, div.jx-tokenizer-container-focused { border-color: rgba(82, 168, 236, 0.8); outline: 0; /* IE6-9 */ -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(82,168,236,.6); -moz-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(82,168,236,.6); box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(82,168,236,.6); } input[type="radio"], input[type="checkbox"] { margin: 4px 0 0; margin-top: 1px \9; /* IE8-9 */ line-height: normal; } select { height: 24px; line-height: 24px; border: 1px solid #a1a5a9; background-color: #ffffff; } select[multiple], select[size] { height: auto; } select:focus, input[type="file"]:focus, input[type="radio"]:focus, input[type="checkbox"]:focus { outline: thin dotted #333; outline: 5px auto -webkit-focus-ring-color; outline-offset: -2px; } input:-moz-placeholder, textarea:-moz-placeholder { color: {$lightgreytext}; } input:-ms-input-placeholder, textarea:-ms-input-placeholder { color: {$lightgreytext}; } input::-webkit-input-placeholder, textarea::-webkit-input-placeholder { color: {$lightgreytext}; } -select[disabled="disabled"], -input[disabled="disabled"] { +select[disabled], +input[disabled] { opacity: 0.5; } diff --git a/webroot/rsrc/js/application/calendar/behavior-recurring-edit.js b/webroot/rsrc/js/application/calendar/behavior-recurring-edit.js new file mode 100644 index 0000000000..ee5c916d2c --- /dev/null +++ b/webroot/rsrc/js/application/calendar/behavior-recurring-edit.js @@ -0,0 +1,14 @@ +/** + * @provides javelin-behavior-recurring-edit + */ + + +JX.behavior('recurring-edit', function(config) { + var checkbox = JX.$(config.isRecurring); + JX.DOM.listen(checkbox, 'change', null, function() { + var frequency = JX.$(config.frequency); + + frequency.disabled = checkbox.checked ? false : true; + }); + +});