diff --git a/resources/sql/autopatches/20140812.projkey.1.sql b/resources/sql/autopatches/20140812.projkey.1.sql new file mode 100644 index 0000000000..c1726843ad --- /dev/null +++ b/resources/sql/autopatches/20140812.projkey.1.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_project.project + ADD KEY `key_icon` (icon); diff --git a/resources/sql/autopatches/20140812.projkey.2.sql b/resources/sql/autopatches/20140812.projkey.2.sql new file mode 100644 index 0000000000..c48a54d4d8 --- /dev/null +++ b/resources/sql/autopatches/20140812.projkey.2.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_project.project + ADD KEY `key_color` (color); diff --git a/src/applications/project/controller/PhabricatorProjectEditDetailsController.php b/src/applications/project/controller/PhabricatorProjectEditDetailsController.php index 3852f7dee6..9732ff59aa 100644 --- a/src/applications/project/controller/PhabricatorProjectEditDetailsController.php +++ b/src/applications/project/controller/PhabricatorProjectEditDetailsController.php @@ -1,232 +1,228 @@ id = $data['id']; } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $project = id(new PhabricatorProjectQuery()) ->setViewer($viewer) ->withIDs(array($this->id)) ->needSlugs(true) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$project) { return new Aphront404Response(); } $field_list = PhabricatorCustomField::getObjectFields( $project, PhabricatorCustomField::ROLE_EDIT); $field_list ->setViewer($viewer) ->readFieldsFromStorage($project); $view_uri = $this->getApplicationURI('view/'.$project->getID().'/'); $edit_uri = $this->getApplicationURI('edit/'.$project->getID().'/'); $e_name = true; $e_slugs = false; $e_edit = null; $v_name = $project->getName(); $project_slugs = $project->getSlugs(); $project_slugs = mpull($project_slugs, 'getSlug', 'getSlug'); $v_primary_slug = $project->getPrimarySlug(); unset($project_slugs[$v_primary_slug]); $v_slugs = $project_slugs; $v_color = $project->getColor(); $v_icon = $project->getIcon(); $validation_exception = null; if ($request->isFormPost()) { $e_name = null; $e_slugs = null; $v_name = $request->getStr('name'); $v_slugs = $request->getStrList('slugs'); $v_view = $request->getStr('can_view'); $v_edit = $request->getStr('can_edit'); $v_join = $request->getStr('can_join'); $v_color = $request->getStr('color'); $v_icon = $request->getStr('icon'); $xactions = $field_list->buildFieldTransactionsFromRequest( new PhabricatorProjectTransaction(), $request); $type_name = PhabricatorProjectTransaction::TYPE_NAME; $type_slugs = PhabricatorProjectTransaction::TYPE_SLUGS; $type_edit = PhabricatorTransactions::TYPE_EDIT_POLICY; $type_icon = PhabricatorProjectTransaction::TYPE_ICON; $type_color = PhabricatorProjectTransaction::TYPE_COLOR; $xactions[] = id(new PhabricatorProjectTransaction()) ->setTransactionType($type_name) ->setNewValue($v_name); $xactions[] = id(new PhabricatorProjectTransaction()) ->setTransactionType($type_slugs) ->setNewValue($v_slugs); $xactions[] = id(new PhabricatorProjectTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY) ->setNewValue($v_view); $xactions[] = id(new PhabricatorProjectTransaction()) ->setTransactionType($type_edit) ->setNewValue($v_edit); $xactions[] = id(new PhabricatorProjectTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_JOIN_POLICY) ->setNewValue($v_join); $xactions[] = id(new PhabricatorProjectTransaction()) ->setTransactionType($type_icon) ->setNewValue($v_icon); $xactions[] = id(new PhabricatorProjectTransaction()) ->setTransactionType($type_color) ->setNewValue($v_color); $editor = id(new PhabricatorProjectTransactionEditor()) ->setActor($viewer) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true); try { $editor->applyTransactions($project, $xactions); return id(new AphrontRedirectResponse())->setURI($edit_uri); } catch (PhabricatorApplicationTransactionValidationException $ex) { $validation_exception = $ex; $e_name = $ex->getShortMessage($type_name); $e_slugs = $ex->getShortMessage($type_slugs); $e_edit = $ex->getShortMessage($type_edit); $project->setViewPolicy($v_view); $project->setEditPolicy($v_edit); $project->setJoinPolicy($v_join); } } $header_name = pht('Edit Project'); $title = pht('Edit Project'); $policies = id(new PhabricatorPolicyQuery()) ->setViewer($viewer) ->setObject($project) ->execute(); $v_slugs = implode(', ', $v_slugs); $form = new AphrontFormView(); $form ->setUser($viewer) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Name')) ->setName('name') ->setValue($v_name) ->setError($e_name)); $field_list->appendFieldsToForm($form); - $shades = PHUITagView::getShadeMap(); - $shades = array_select_keys( - $shades, - array(PhabricatorProject::DEFAULT_COLOR)) + $shades; - unset($shades[PHUITagView::COLOR_DISABLED]); + $shades = PhabricatorProjectIcon::getColorMap(); $icon_uri = $this->getApplicationURI('icon/'.$project->getID().'/'); $icon_display = PhabricatorProjectIcon::renderIconForChooser($v_icon); $form ->appendChild( id(new AphrontFormChooseButtonControl()) ->setLabel(pht('Icon')) ->setName('icon') ->setDisplayValue($icon_display) ->setButtonText(pht('Choose Icon...')) ->setChooseURI($icon_uri) ->setValue($v_icon)) ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Color')) ->setName('color') ->setValue($v_color) ->setOptions($shades)) ->appendChild( id(new AphrontFormStaticControl()) ->setLabel(pht('Primary Hashtag')) ->setCaption(pht('The primary hashtag is derived from the name.')) ->setValue($v_primary_slug)) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Additional Hashtags')) ->setCaption(pht( 'Specify a comma-separated list of additional hashtags.')) ->setName('slugs') ->setValue($v_slugs) ->setError($e_slugs)) ->appendChild( id(new AphrontFormPolicyControl()) ->setUser($viewer) ->setName('can_view') ->setPolicyObject($project) ->setPolicies($policies) ->setCapability(PhabricatorPolicyCapability::CAN_VIEW)) ->appendChild( id(new AphrontFormPolicyControl()) ->setUser($viewer) ->setName('can_edit') ->setPolicyObject($project) ->setPolicies($policies) ->setCapability(PhabricatorPolicyCapability::CAN_EDIT) ->setError($e_edit)) ->appendChild( id(new AphrontFormPolicyControl()) ->setUser($viewer) ->setName('can_join') ->setCaption( pht('Users who can edit a project can always join a project.')) ->setPolicyObject($project) ->setPolicies($policies) ->setCapability(PhabricatorPolicyCapability::CAN_JOIN)) ->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton($edit_uri) ->setValue(pht('Save'))); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setValidationException($validation_exception) ->setForm($form); $crumbs = $this->buildApplicationCrumbs($this->buildSideNavView()) ->addTextCrumb($project->getName(), $view_uri) ->addTextCrumb(pht('Edit'), $edit_uri) ->addTextCrumb(pht('Details')); return $this->buildApplicationPage( array( $crumbs, $form_box, ), array( 'title' => $title, )); } } diff --git a/src/applications/project/icon/PhabricatorProjectIcon.php b/src/applications/project/icon/PhabricatorProjectIcon.php index 6f4e6d2925..7ed9d1c535 100644 --- a/src/applications/project/icon/PhabricatorProjectIcon.php +++ b/src/applications/project/icon/PhabricatorProjectIcon.php @@ -1,45 +1,55 @@ pht('Briefcase'), 'fa-tags' => pht('Tag'), 'fa-folder' => pht('Folder'), 'fa-users' => pht('Team'), 'fa-bug' => pht('Bug'), 'fa-trash-o' => pht('Garbage'), 'fa-calendar' => pht('Deadline'), 'fa-flag-checkered' => pht('Goal'), 'fa-envelope' => pht('Communication'), 'fa-truck' => pht('Release'), 'fa-lock' => pht('Policy'), 'fa-umbrella' => pht('An Umbrella'), 'fa-cloud' => pht('The Cloud'), 'fa-building' => pht('Company'), 'fa-credit-card' => pht('Accounting'), 'fa-flask' => pht('Experimental'), ); } + public static function getColorMap() { + $shades = PHUITagView::getShadeMap(); + $shades = array_select_keys( + $shades, + array(PhabricatorProject::DEFAULT_COLOR)) + $shades; + unset($shades[PHUITagView::COLOR_DISABLED]); + + return $shades; + } + public static function getLabel($key) { $map = self::getIconMap(); return $map[$key]; } public static function renderIconForChooser($icon) { $project_icons = PhabricatorProjectIcon::getIconMap(); return phutil_tag( 'span', array(), array( id(new PHUIIconView())->setIconFont($icon), ' ', idx($project_icons, $icon, pht('Unknown Icon')), )); } } diff --git a/src/applications/project/query/PhabricatorProjectQuery.php b/src/applications/project/query/PhabricatorProjectQuery.php index bb2d280ef3..bf2ec81461 100644 --- a/src/applications/project/query/PhabricatorProjectQuery.php +++ b/src/applications/project/query/PhabricatorProjectQuery.php @@ -1,366 +1,392 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withStatus($status) { $this->status = $status; return $this; } public function withMemberPHIDs(array $member_phids) { $this->memberPHIDs = $member_phids; return $this; } public function withSlugs(array $slugs) { $this->slugs = $slugs; return $this; } public function withPhrictionSlugs(array $slugs) { $this->phrictionSlugs = $slugs; return $this; } public function withNames(array $names) { $this->names = $names; return $this; } public function withDatasourceQuery($string) { $this->datasourceQuery = $string; return $this; } + public function withIcons(array $icons) { + $this->icons = $icons; + return $this; + } + + public function withColors(array $colors) { + $this->colors = $colors; + return $this; + } + public function needMembers($need_members) { $this->needMembers = $need_members; return $this; } public function needWatchers($need_watchers) { $this->needWatchers = $need_watchers; return $this; } public function needImages($need_images) { $this->needImages = $need_images; return $this; } public function needSlugs($need_slugs) { $this->needSlugs = $need_slugs; return $this; } protected function getPagingColumn() { return 'name'; } protected function getPagingValue($result) { return $result->getName(); } protected function getReversePaging() { return true; } protected function loadPage() { $table = new PhabricatorProject(); $conn_r = $table->establishConnection('r'); // NOTE: Because visibility checks for projects depend on whether or not // the user is a project member, we always load their membership. If we're // loading all members anyway we can piggyback on that; otherwise we // do an explicit join. $select_clause = ''; if (!$this->needMembers) { $select_clause = ', vm.dst viewerIsMember'; } $data = queryfx_all( $conn_r, 'SELECT p.* %Q FROM %T p %Q %Q %Q %Q %Q', $select_clause, $table->getTableName(), $this->buildJoinClause($conn_r), $this->buildWhereClause($conn_r), $this->buildGroupClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); $projects = $table->loadAllFromArray($data); if ($projects) { $viewer_phid = $this->getViewer()->getPHID(); $project_phids = mpull($projects, 'getPHID'); $member_type = PhabricatorEdgeConfig::TYPE_PROJ_MEMBER; $watcher_type = PhabricatorEdgeConfig::TYPE_OBJECT_HAS_WATCHER; $need_edge_types = array(); if ($this->needMembers) { $need_edge_types[] = $member_type; } else { foreach ($data as $row) { $projects[$row['id']]->setIsUserMember( $viewer_phid, ($row['viewerIsMember'] !== null)); } } if ($this->needWatchers) { $need_edge_types[] = $watcher_type; } if ($need_edge_types) { $edges = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs($project_phids) ->withEdgeTypes($need_edge_types) ->execute(); if ($this->needMembers) { foreach ($projects as $project) { $phid = $project->getPHID(); $project->attachMemberPHIDs( array_keys($edges[$phid][$member_type])); $project->setIsUserMember( $viewer_phid, isset($edges[$phid][$member_type][$viewer_phid])); } } if ($this->needWatchers) { foreach ($projects as $project) { $phid = $project->getPHID(); $project->attachWatcherPHIDs( array_keys($edges[$phid][$watcher_type])); $project->setIsUserWatcher( $viewer_phid, isset($edges[$phid][$watcher_type][$viewer_phid])); } } } } return $projects; } protected function didFilterPage(array $projects) { if ($this->needImages) { $default = null; $file_phids = mpull($projects, 'getProfileImagePHID'); $files = id(new PhabricatorFileQuery()) ->setParentQuery($this) ->setViewer($this->getViewer()) ->withPHIDs($file_phids) ->execute(); $files = mpull($files, null, 'getPHID'); foreach ($projects as $project) { $file = idx($files, $project->getProfileImagePHID()); if (!$file) { if (!$default) { $default = PhabricatorFile::loadBuiltin( $this->getViewer(), 'project.png'); } $file = $default; } $project->attachProfileImageFile($file); } } if ($this->needSlugs) { $slugs = id(new PhabricatorProjectSlug()) ->loadAllWhere( 'projectPHID IN (%Ls)', mpull($projects, 'getPHID')); $slugs = mgroup($slugs, 'getProjectPHID'); foreach ($projects as $project) { $project_slugs = idx($slugs, $project->getPHID(), array()); $project->attachSlugs($project_slugs); } } return $projects; } private function buildWhereClause($conn_r) { $where = array(); if ($this->status != self::STATUS_ANY) { switch ($this->status) { case self::STATUS_OPEN: case self::STATUS_ACTIVE: $filter = array( PhabricatorProjectStatus::STATUS_ACTIVE, ); break; case self::STATUS_CLOSED: case self::STATUS_ARCHIVED: $filter = array( PhabricatorProjectStatus::STATUS_ARCHIVED, ); break; default: throw new Exception( "Unknown project status '{$this->status}'!"); } $where[] = qsprintf( $conn_r, 'status IN (%Ld)', $filter); } - if ($this->ids) { + if ($this->ids !== null) { $where[] = qsprintf( $conn_r, 'id IN (%Ld)', $this->ids); } - if ($this->phids) { + if ($this->phids !== null) { $where[] = qsprintf( $conn_r, 'phid IN (%Ls)', $this->phids); } - if ($this->memberPHIDs) { + if ($this->memberPHIDs !== null) { $where[] = qsprintf( $conn_r, 'e.dst IN (%Ls)', $this->memberPHIDs); } - if ($this->slugs) { + if ($this->slugs !== null) { $slugs = array(); foreach ($this->slugs as $slug) { $slugs[] = rtrim(PhabricatorSlug::normalize($slug), '/'); } $where[] = qsprintf( $conn_r, 'slug.slug IN (%Ls)', $slugs); } - if ($this->phrictionSlugs) { + if ($this->phrictionSlugs !== null) { $where[] = qsprintf( $conn_r, 'phrictionSlug IN (%Ls)', $this->phrictionSlugs); } - if ($this->names) { + if ($this->names !== null) { $where[] = qsprintf( $conn_r, 'name IN (%Ls)', $this->names); } + if ($this->icons !== null) { + $where[] = qsprintf( + $conn_r, + 'icon IN (%Ls)', + $this->icons); + } + + if ($this->colors !== null) { + $where[] = qsprintf( + $conn_r, + 'color IN (%Ls)', + $this->colors); + } + $where[] = $this->buildPagingClause($conn_r); return $this->formatWhereClause($where); } private function buildGroupClause($conn_r) { if ($this->memberPHIDs || $this->datasourceQuery) { return 'GROUP BY p.id'; } else { return $this->buildApplicationSearchGroupClause($conn_r); } } private function buildJoinClause($conn_r) { $joins = array(); if (!$this->needMembers !== null) { $joins[] = qsprintf( $conn_r, 'LEFT JOIN %T vm ON vm.src = p.phid AND vm.type = %d AND vm.dst = %s', PhabricatorEdgeConfig::TABLE_NAME_EDGE, PhabricatorEdgeConfig::TYPE_PROJ_MEMBER, $this->getViewer()->getPHID()); } if ($this->memberPHIDs !== null) { $joins[] = qsprintf( $conn_r, 'JOIN %T e ON e.src = p.phid AND e.type = %d', PhabricatorEdgeConfig::TABLE_NAME_EDGE, PhabricatorEdgeConfig::TYPE_PROJ_MEMBER); } if ($this->slugs !== null) { $joins[] = qsprintf( $conn_r, 'JOIN %T slug on slug.projectPHID = p.phid', id(new PhabricatorProjectSlug())->getTableName()); } if ($this->datasourceQuery !== null) { $tokens = PhabricatorTypeaheadDatasource::tokenizeString( $this->datasourceQuery); if (!$tokens) { throw new PhabricatorEmptyQueryException(); } $likes = array(); foreach ($tokens as $token) { $likes[] = qsprintf($conn_r, 'token.token LIKE %>', $token); } $joins[] = qsprintf( $conn_r, 'JOIN %T token ON token.projectID = p.id AND (%Q)', PhabricatorProject::TABLE_DATASOURCE_TOKEN, '('.implode(') OR (', $likes).')'); } $joins[] = $this->buildApplicationSearchJoinClause($conn_r); return implode(' ', $joins); } public function getQueryApplicationClass() { return 'PhabricatorProjectApplication'; } protected function getApplicationSearchObjectPHIDColumn() { return 'p.phid'; } } diff --git a/src/applications/project/query/PhabricatorProjectSearchEngine.php b/src/applications/project/query/PhabricatorProjectSearchEngine.php index 0b0f94ba0c..dab0526739 100644 --- a/src/applications/project/query/PhabricatorProjectSearchEngine.php +++ b/src/applications/project/query/PhabricatorProjectSearchEngine.php @@ -1,190 +1,260 @@ setParameter( 'memberPHIDs', $this->readUsersFromRequest($request, 'members')); $saved->setParameter('status', $request->getStr('status')); $saved->setParameter('name', $request->getStr('name')); + $saved->setParameter( + 'icons', + $this->readListFromRequest($request, 'icons')); + + $saved->setParameter( + 'colors', + $this->readListFromRequest($request, 'colors')); + $this->readCustomFieldsFromRequest($request, $saved); return $saved; } public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) { $query = id(new PhabricatorProjectQuery()) ->needImages(true); $member_phids = $saved->getParameter('memberPHIDs', array()); if ($member_phids && is_array($member_phids)) { $query->withMemberPHIDs($member_phids); } $status = $saved->getParameter('status'); $status = idx($this->getStatusValues(), $status); if ($status) { $query->withStatus($status); } $name = $saved->getParameter('name'); if (strlen($name)) { $query->withDatasourceQuery($name); } + $icons = $saved->getParameter('icons'); + if ($icons) { + $query->withIcons($icons); + } + + $colors = $saved->getParameter('colors'); + if ($colors) { + $query->withColors($colors); + } + $this->applyCustomFieldsToQuery($query, $saved); return $query; } public function buildSearchForm( AphrontFormView $form, PhabricatorSavedQuery $saved) { $phids = $saved->getParameter('memberPHIDs', array()); $member_handles = id(new PhabricatorHandleQuery()) ->setViewer($this->requireViewer()) ->withPHIDs($phids) ->execute(); $status = $saved->getParameter('status'); - $name = $saved->getParameter('name'); + $name_match = $saved->getParameter('name'); + + $icons = array_fuse($saved->getParameter('icons', array())); + $colors = array_fuse($saved->getParameter('colors', array())); + + $icon_control = id(new AphrontFormCheckboxControl()) + ->setLabel(pht('Icons')); + foreach (PhabricatorProjectIcon::getIconMap() as $icon => $name) { + $image = id(new PHUIIconView()) + ->setIconFont($icon); + + $icon_control->addCheckbox( + 'icons[]', + $icon, + array($image, ' ', $name), + isset($icons[$icon])); + } + + $color_control = id(new AphrontFormCheckboxControl()) + ->setLabel(pht('Colors')); + foreach (PhabricatorProjectIcon::getColorMap() as $color => $name) { + $tag = id(new PHUITagView()) + ->setType(PHUITagView::TYPE_SHADE) + ->setShade($color) + ->setName($name); + + $color_control->addCheckbox( + 'colors[]', + $color, + $tag, + isset($colors[$color])); + } $form ->appendChild( id(new AphrontFormTextControl()) ->setName('name') ->setLabel(pht('Name')) - ->setValue($name)) + ->setValue($name_match)) ->appendChild( id(new AphrontFormTokenizerControl()) ->setDatasource(new PhabricatorPeopleDatasource()) ->setName('members') ->setLabel(pht('Members')) ->setValue($member_handles)) ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Status')) ->setName('status') ->setOptions($this->getStatusOptions()) - ->setValue($status)); + ->setValue($status)) + ->appendChild($icon_control) + ->appendChild($color_control); $this->appendCustomFieldsToForm($form, $saved); } protected function getURI($path) { return '/project/'.$path; } public function getBuiltinQueryNames() { $names = array(); if ($this->requireViewer()->isLoggedIn()) { $names['joined'] = pht('Joined'); } $names['active'] = pht('Active'); $names['all'] = pht('All'); return $names; } public function buildSavedQueryFromBuiltin($query_key) { $query = $this->newSavedQuery(); $query->setQueryKey($query_key); $viewer_phid = $this->requireViewer()->getPHID(); switch ($query_key) { case 'all': return $query; case 'active': return $query ->setParameter('status', 'active'); case 'joined': return $query ->setParameter('memberPHIDs', array($viewer_phid)) ->setParameter('status', 'active'); } return parent::buildSavedQueryFromBuiltin($query_key); } private function getStatusOptions() { return array( 'active' => pht('Show Only Active Projects'), 'all' => pht('Show All Projects'), ); } private function getStatusValues() { return array( 'active' => PhabricatorProjectQuery::STATUS_ACTIVE, 'all' => PhabricatorProjectQuery::STATUS_ANY, ); } + private function getColorValues() { + + } + + private function getIconValues() { + + } + + protected function getRequiredHandlePHIDsForResultList( + array $projects, + PhabricatorSavedQuery $query) { + return mpull($projects, 'getPHID'); + } + protected function renderResultList( array $projects, PhabricatorSavedQuery $query, array $handles) { assert_instances_of($projects, 'PhabricatorProject'); $viewer = $this->requireViewer(); $list = new PHUIObjectItemListView(); $list->setUser($viewer); foreach ($projects as $project) { $id = $project->getID(); $workboards_uri = $this->getApplicationURI("board/{$id}/"); $members_uri = $this->getApplicationURI("members/{$id}/"); $workboards_url = phutil_tag( 'a', array( 'href' => $workboards_uri ), pht('Workboards')); $members_url = phutil_tag( 'a', array( 'href' => $members_uri ), pht('Members')); + $tag_list = id(new PHUIHandleTagListView()) + ->setSlim(true) + ->setHandles(array($handles[$project->getPHID()])); + $item = id(new PHUIObjectItemView()) ->setHeader($project->getName()) ->setHref($this->getApplicationURI("view/{$id}/")) ->setImageURI($project->getProfileImageURI()) + ->addAttribute($tag_list) ->addAttribute($workboards_url) ->addAttribute($members_url); if ($project->getStatus() == PhabricatorProjectStatus::STATUS_ARCHIVED) { $item->addIcon('delete-grey', pht('Archived')); $item->setDisabled(true); } $list->addItem($item); } return $list; } }