diff --git a/src/applications/project/engine/PhabricatorProjectEditEngine.php b/src/applications/project/engine/PhabricatorProjectEditEngine.php index 71f91b7bc1..ee101cb2e4 100644 --- a/src/applications/project/engine/PhabricatorProjectEditEngine.php +++ b/src/applications/project/engine/PhabricatorProjectEditEngine.php @@ -1,287 +1,304 @@ parentProject = $parent_project; return $this; } public function getParentProject() { return $this->parentProject; } public function setMilestoneProject(PhabricatorProject $milestone_project) { $this->milestoneProject = $milestone_project; return $this; } public function getMilestoneProject() { return $this->milestoneProject; } public function getEngineName() { return pht('Projects'); } public function getSummaryHeader() { return pht('Configure Project Forms'); } public function getSummaryText() { return pht('Configure forms for creating projects.'); } public function getEngineApplicationClass() { return 'PhabricatorProjectApplication'; } protected function newEditableObject() { - $project = PhabricatorProject::initializeNewProject($this->getViewer()); - - $milestone = $this->getMilestoneProject(); - if ($milestone) { - $default_name = pht( - 'Milestone %s', - new PhutilNumber($milestone->loadNextMilestoneNumber())); - $project->setName($default_name); - } - - return $project; + return PhabricatorProject::initializeNewProject($this->getViewer()); } protected function newObjectQuery() { return id(new PhabricatorProjectQuery()) ->needSlugs(true); } protected function getObjectCreateTitleText($object) { return pht('Create New Project'); } protected function getObjectEditTitleText($object) { return pht('Edit %s', $object->getName()); } protected function getObjectEditShortText($object) { return $object->getName(); } protected function getObjectCreateShortText() { return pht('Create Project'); } protected function getObjectViewURI($object) { if ($this->getIsCreate()) { return $object->getURI(); } else { $id = $object->getID(); return "/project/manage/{$id}/"; } } protected function getObjectCreateCancelURI($object) { $parent = $this->getParentProject(); $milestone = $this->getMilestoneProject(); if ($parent || $milestone) { $id = nonempty($parent, $milestone)->getID(); return "/project/subprojects/{$id}/"; } return parent::getObjectCreateCancelURI($object); } protected function getCreateNewObjectPolicy() { return $this->getApplication()->getPolicy( ProjectCreateProjectsCapability::CAPABILITY); } protected function willConfigureFields($object, array $fields) { $is_milestone = ($this->getMilestoneProject() || $object->isMilestone()); $unavailable = array( PhabricatorTransactions::TYPE_VIEW_POLICY, PhabricatorTransactions::TYPE_EDIT_POLICY, PhabricatorTransactions::TYPE_JOIN_POLICY, PhabricatorProjectTransaction::TYPE_ICON, PhabricatorProjectTransaction::TYPE_COLOR, ); $unavailable = array_fuse($unavailable); if ($is_milestone) { foreach ($fields as $key => $field) { $xaction_type = $field->getTransactionType(); if (isset($unavailable[$xaction_type])) { unset($fields[$key]); } } } return $fields; } protected function newBuiltinEngineConfigurations() { $configuration = head(parent::newBuiltinEngineConfigurations()); // TODO: This whole method is clumsy, and the ordering for the custom // field is especially clumsy. Maybe try to make this more natural to // express. $configuration ->setFieldOrder( array( 'parent', 'milestone', + 'milestone.previous', 'name', 'std:project:internal:description', 'icon', 'color', 'slugs', )); return array( $configuration, ); } protected function buildCustomEditFields($object) { $slugs = mpull($object->getSlugs(), 'getSlug'); $slugs = array_fuse($slugs); unset($slugs[$object->getPrimarySlug()]); $slugs = array_values($slugs); $milestone = $this->getMilestoneProject(); $parent = $this->getParentProject(); if ($parent) { $parent_phid = $parent->getPHID(); } else { $parent_phid = null; } + $previous_milestone_phid = null; if ($milestone) { $milestone_phid = $milestone->getPHID(); + + // Load the current milestone so we can show the user a hint about what + // it was called, so they don't have to remember if the next one should + // be "Sprint 287" or "Sprint 278". + + $number = ($milestone->loadNextMilestoneNumber() - 1); + if ($number > 0) { + $previous_milestone = id(new PhabricatorProjectQuery()) + ->setViewer($this->getViewer()) + ->withParentProjectPHIDs(array($milestone->getPHID())) + ->withIsMilestone(true) + ->withMilestoneNumberBetween($number, $number) + ->executeOne(); + if ($previous_milestone) { + $previous_milestone_phid = $previous_milestone->getPHID(); + } + } } else { $milestone_phid = null; } $fields = array( id(new PhabricatorHandlesEditField()) ->setKey('parent') ->setLabel(pht('Parent')) ->setDescription(pht('Create a subproject of an existing project.')) ->setConduitDescription( pht('Choose a parent project to create a subproject beneath.')) ->setConduitTypeDescription(pht('PHID of the parent project.')) ->setAliases(array('parentPHID')) ->setTransactionType(PhabricatorProjectTransaction::TYPE_PARENT) ->setHandleParameterType(new AphrontPHIDHTTPParameterType()) ->setSingleValue($parent_phid) ->setIsReorderable(false) ->setIsDefaultable(false) ->setIsLockable(false) ->setIsLocked(true), id(new PhabricatorHandlesEditField()) ->setKey('milestone') ->setLabel(pht('Milestone Of')) ->setDescription(pht('Parent project to create a milestone for.')) ->setConduitDescription( pht('Choose a parent project to create a new milestone for.')) ->setConduitTypeDescription(pht('PHID of the parent project.')) ->setAliases(array('milestonePHID')) ->setTransactionType(PhabricatorProjectTransaction::TYPE_MILESTONE) ->setHandleParameterType(new AphrontPHIDHTTPParameterType()) ->setSingleValue($milestone_phid) ->setIsReorderable(false) ->setIsDefaultable(false) ->setIsLockable(false) ->setIsLocked(true), + id(new PhabricatorHandlesEditField()) + ->setKey('milestone.previous') + ->setLabel(pht('Previous Milestone')) + ->setSingleValue($previous_milestone_phid) + ->setIsReorderable(false) + ->setIsDefaultable(false) + ->setIsLockable(false) + ->setIsLocked(true), id(new PhabricatorTextEditField()) ->setKey('name') ->setLabel(pht('Name')) ->setTransactionType(PhabricatorProjectTransaction::TYPE_NAME) ->setIsRequired(true) ->setDescription(pht('Project name.')) ->setConduitDescription(pht('Rename the project')) ->setConduitTypeDescription(pht('New project name.')) ->setValue($object->getName()), id(new PhabricatorIconSetEditField()) ->setKey('icon') ->setLabel(pht('Icon')) ->setTransactionType(PhabricatorProjectTransaction::TYPE_ICON) ->setIconSet(new PhabricatorProjectIconSet()) ->setDescription(pht('Project icon.')) ->setConduitDescription(pht('Change the project icon.')) ->setConduitTypeDescription(pht('New project icon.')) ->setValue($object->getIcon()), id(new PhabricatorSelectEditField()) ->setKey('color') ->setLabel(pht('Color')) ->setTransactionType(PhabricatorProjectTransaction::TYPE_COLOR) ->setOptions(PhabricatorProjectIconSet::getColorMap()) ->setDescription(pht('Project tag color.')) ->setConduitDescription(pht('Change the project tag color.')) ->setConduitTypeDescription(pht('New project tag color.')) ->setValue($object->getColor()), id(new PhabricatorStringListEditField()) ->setKey('slugs') ->setLabel(pht('Additional Hashtags')) ->setTransactionType(PhabricatorProjectTransaction::TYPE_SLUGS) ->setDescription(pht('Additional project slugs.')) ->setConduitDescription(pht('Change project slugs.')) ->setConduitTypeDescription(pht('New list of slugs.')) ->setValue($slugs), ); $can_edit_members = (!$milestone) && (!$object->isMilestone()) && (!$object->getHasSubprojects()); if ($can_edit_members) { // Show this on the web UI when creating a project, but not when editing // one. It is always available via Conduit. $conduit_only = !$this->getIsCreate(); $members_field = id(new PhabricatorUsersEditField()) ->setKey('members') ->setAliases(array('memberPHIDs')) ->setLabel(pht('Initial Members')) ->setIsConduitOnly($conduit_only) ->setUseEdgeTransactions(true) ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) ->setMetadataValue( 'edge:type', PhabricatorProjectProjectHasMemberEdgeType::EDGECONST) ->setDescription(pht('Initial project members.')) ->setConduitDescription(pht('Set project members.')) ->setConduitTypeDescription(pht('New list of members.')) ->setValue(array()); $members_field->setViewer($this->getViewer()); $edit_add = $members_field->getConduitEditType('members.add') ->setConduitDescription(pht('Add members.')); $edit_set = $members_field->getConduitEditType('members.set') ->setConduitDescription( pht('Set members, overwriting the current value.')); $edit_rem = $members_field->getConduitEditType('members.remove') ->setConduitDescription(pht('Remove members.')); $fields[] = $members_field; } return $fields; } } diff --git a/src/applications/project/query/PhabricatorProjectQuery.php b/src/applications/project/query/PhabricatorProjectQuery.php index f4711bb4f7..d12e66e392 100644 --- a/src/applications/project/query/PhabricatorProjectQuery.php +++ b/src/applications/project/query/PhabricatorProjectQuery.php @@ -1,735 +1,758 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withStatus($status) { $this->status = $status; return $this; } public function withStatuses(array $statuses) { $this->statuses = $statuses; return $this; } public function withMemberPHIDs(array $member_phids) { $this->memberPHIDs = $member_phids; return $this; } public function withSlugs(array $slugs) { $this->slugs = $slugs; return $this; } public function withNames(array $names) { $this->names = $names; return $this; } public function withNameTokens(array $tokens) { $this->nameTokens = array_values($tokens); return $this; } public function withIcons(array $icons) { $this->icons = $icons; return $this; } public function withColors(array $colors) { $this->colors = $colors; return $this; } public function withParentProjectPHIDs($parent_phids) { $this->parentPHIDs = $parent_phids; return $this; } public function withAncestorProjectPHIDs($ancestor_phids) { $this->ancestorPHIDs = $ancestor_phids; return $this; } public function withIsMilestone($is_milestone) { $this->isMilestone = $is_milestone; return $this; } public function withHasSubprojects($has_subprojects) { $this->hasSubprojects = $has_subprojects; return $this; } public function withDepthBetween($min, $max) { $this->minDepth = $min; $this->maxDepth = $max; return $this; } + public function withMilestoneNumberBetween($min, $max) { + $this->minMilestoneNumber = $min; + $this->maxMilestoneNumber = $max; + return $this; + } + public function needMembers($need_members) { $this->needMembers = $need_members; return $this; } public function needAncestorMembers($need_ancestor_members) { $this->needAncestorMembers = $need_ancestor_members; return $this; } public function needWatchers($need_watchers) { $this->needWatchers = $need_watchers; return $this; } public function needImages($need_images) { $this->needImages = $need_images; return $this; } public function needSlugs($need_slugs) { $this->needSlugs = $need_slugs; return $this; } public function newResultObject() { return new PhabricatorProject(); } protected function getDefaultOrderVector() { return array('name'); } public function getBuiltinOrders() { return array( 'name' => array( 'vector' => array('name'), 'name' => pht('Name'), ), ) + parent::getBuiltinOrders(); } public function getOrderableColumns() { return parent::getOrderableColumns() + array( 'name' => array( 'table' => $this->getPrimaryTableAlias(), 'column' => 'name', 'reverse' => true, 'type' => 'string', 'unique' => true, ), ); } protected function getPagingValueMap($cursor, array $keys) { $project = $this->loadCursorObject($cursor); return array( 'name' => $project->getName(), ); } public function getSlugMap() { if ($this->slugMap === null) { throw new PhutilInvalidStateException('execute'); } return $this->slugMap; } protected function willExecute() { $this->slugMap = array(); $this->slugNormals = array(); $this->allSlugs = array(); if ($this->slugs) { foreach ($this->slugs as $slug) { if (PhabricatorSlug::isValidProjectSlug($slug)) { $normal = PhabricatorSlug::normalizeProjectSlug($slug); $this->slugNormals[$slug] = $normal; $this->allSlugs[$normal] = $normal; } // NOTE: At least for now, we query for the normalized slugs but also // for the slugs exactly as entered. This allows older projects with // slugs that are no longer valid to continue to work. $this->allSlugs[$slug] = $slug; } } } protected function loadPage() { return $this->loadStandardPage($this->newResultObject()); } protected function willFilterPage(array $projects) { $ancestor_paths = array(); foreach ($projects as $project) { foreach ($project->getAncestorProjectPaths() as $path) { $ancestor_paths[$path] = $path; } } if ($ancestor_paths) { $ancestors = id(new PhabricatorProject())->loadAllWhere( 'projectPath IN (%Ls)', $ancestor_paths); } else { $ancestors = array(); } $projects = $this->linkProjectGraph($projects, $ancestors); $viewer_phid = $this->getViewer()->getPHID(); $material_type = PhabricatorProjectMaterializedMemberEdgeType::EDGECONST; $watcher_type = PhabricatorObjectHasWatcherEdgeType::EDGECONST; $types = array(); $types[] = $material_type; if ($this->needWatchers) { $types[] = $watcher_type; } $all_graph = $this->getAllReachableAncestors($projects); if ($this->needAncestorMembers) { $src_projects = $all_graph; } else { $src_projects = $projects; } $all_sources = array(); foreach ($src_projects as $project) { if ($project->isMilestone()) { $phid = $project->getParentProjectPHID(); } else { $phid = $project->getPHID(); } $all_sources[$phid] = $phid; } $edge_query = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs($all_sources) ->withEdgeTypes($types); $need_all_edges = $this->needMembers || $this->needWatchers || $this->needAncestorMembers; // If we only need to know if the viewer is a member, we can restrict // the query to just their PHID. $any_edges = true; if (!$need_all_edges) { if ($viewer_phid) { $edge_query->withDestinationPHIDs(array($viewer_phid)); } else { // If we don't need members or watchers and don't have a viewer PHID // (viewer is logged-out or omnipotent), they'll never be a member // so we don't need to issue this query at all. $any_edges = false; } } if ($any_edges) { $edge_query->execute(); } $membership_projects = array(); foreach ($src_projects as $project) { $project_phid = $project->getPHID(); if ($project->isMilestone()) { $source_phids = array($project->getParentProjectPHID()); } else { $source_phids = array($project_phid); } if ($any_edges) { $member_phids = $edge_query->getDestinationPHIDs( $source_phids, array($material_type)); } else { $member_phids = array(); } if (in_array($viewer_phid, $member_phids)) { $membership_projects[$project_phid] = $project; } if ($this->needMembers || $this->needAncestorMembers) { $project->attachMemberPHIDs($member_phids); } if ($this->needWatchers) { $watcher_phids = $edge_query->getDestinationPHIDs( $source_phids, array($watcher_type)); $project->attachWatcherPHIDs($watcher_phids); $project->setIsUserWatcher( $viewer_phid, in_array($viewer_phid, $watcher_phids)); } } // If we loaded ancestor members, we've already populated membership // lists above, so we can skip this step. if (!$this->needAncestorMembers) { $member_graph = $this->getAllReachableAncestors($membership_projects); foreach ($all_graph as $phid => $project) { $is_member = isset($member_graph[$phid]); $project->setIsUserMember($viewer_phid, $is_member); } } return $projects; } protected function didFilterPage(array $projects) { if ($this->needImages) { $default = null; $file_phids = mpull($projects, 'getProfileImagePHID'); $file_phids = array_filter($file_phids); if ($file_phids) { $files = id(new PhabricatorFileQuery()) ->setParentQuery($this) ->setViewer($this->getViewer()) ->withPHIDs($file_phids) ->execute(); $files = mpull($files, null, 'getPHID'); } else { $files = array(); } 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); } } $this->loadSlugs($projects); return $projects; } protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { $where = parent::buildWhereClauseParts($conn); if ($this->status != self::STATUS_ANY) { switch ($this->status) { case self::STATUS_OPEN: case self::STATUS_ACTIVE: $filter = array( PhabricatorProjectStatus::STATUS_ACTIVE, ); break; case self::STATUS_CLOSED: case self::STATUS_ARCHIVED: $filter = array( PhabricatorProjectStatus::STATUS_ARCHIVED, ); break; default: throw new Exception( pht( "Unknown project status '%s'!", $this->status)); } $where[] = qsprintf( $conn, 'status IN (%Ld)', $filter); } if ($this->statuses !== null) { $where[] = qsprintf( $conn, 'status IN (%Ls)', $this->statuses); } if ($this->ids !== null) { $where[] = qsprintf( $conn, 'id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( $conn, 'phid IN (%Ls)', $this->phids); } if ($this->memberPHIDs !== null) { $where[] = qsprintf( $conn, 'e.dst IN (%Ls)', $this->memberPHIDs); } if ($this->slugs !== null) { $where[] = qsprintf( $conn, 'slug.slug IN (%Ls)', $this->allSlugs); } if ($this->names !== null) { $where[] = qsprintf( $conn, 'name IN (%Ls)', $this->names); } if ($this->icons !== null) { $where[] = qsprintf( $conn, 'icon IN (%Ls)', $this->icons); } if ($this->colors !== null) { $where[] = qsprintf( $conn, 'color IN (%Ls)', $this->colors); } if ($this->parentPHIDs !== null) { $where[] = qsprintf( $conn, 'parentProjectPHID IN (%Ls)', $this->parentPHIDs); } if ($this->ancestorPHIDs !== null) { $ancestor_paths = queryfx_all( $conn, 'SELECT projectPath, projectDepth FROM %T WHERE phid IN (%Ls)', id(new PhabricatorProject())->getTableName(), $this->ancestorPHIDs); if (!$ancestor_paths) { throw new PhabricatorEmptyQueryException(); } $sql = array(); foreach ($ancestor_paths as $ancestor_path) { $sql[] = qsprintf( $conn, '(projectPath LIKE %> AND projectDepth > %d)', $ancestor_path['projectPath'], $ancestor_path['projectDepth']); } $where[] = '('.implode(' OR ', $sql).')'; $where[] = qsprintf( $conn, 'parentProjectPHID IS NOT NULL'); } if ($this->isMilestone !== null) { if ($this->isMilestone) { $where[] = qsprintf( $conn, 'milestoneNumber IS NOT NULL'); } else { $where[] = qsprintf( $conn, 'milestoneNumber IS NULL'); } } + if ($this->hasSubprojects !== null) { $where[] = qsprintf( $conn, 'hasSubprojects = %d', (int)$this->hasSubprojects); } if ($this->minDepth !== null) { $where[] = qsprintf( $conn, 'projectDepth >= %d', $this->minDepth); } if ($this->maxDepth !== null) { $where[] = qsprintf( $conn, 'projectDepth <= %d', $this->maxDepth); } + if ($this->minMilestoneNumber !== null) { + $where[] = qsprintf( + $conn, + 'milestoneNumber >= %d', + $this->minMilestoneNumber); + } + + if ($this->maxMilestoneNumber !== null) { + $where[] = qsprintf( + $conn, + 'milestoneNumber <= %d', + $this->maxMilestoneNumber); + } + return $where; } protected function shouldGroupQueryResultRows() { if ($this->memberPHIDs || $this->nameTokens) { return true; } return parent::shouldGroupQueryResultRows(); } protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) { $joins = parent::buildJoinClauseParts($conn); if ($this->memberPHIDs !== null) { $joins[] = qsprintf( $conn, 'JOIN %T e ON e.src = p.phid AND e.type = %d', PhabricatorEdgeConfig::TABLE_NAME_EDGE, PhabricatorProjectMaterializedMemberEdgeType::EDGECONST); } if ($this->slugs !== null) { $joins[] = qsprintf( $conn, 'JOIN %T slug on slug.projectPHID = p.phid', id(new PhabricatorProjectSlug())->getTableName()); } if ($this->nameTokens !== null) { foreach ($this->nameTokens as $key => $token) { $token_table = 'token_'.$key; $joins[] = qsprintf( $conn, 'JOIN %T %T ON %T.projectID = p.id AND %T.token LIKE %>', PhabricatorProject::TABLE_DATASOURCE_TOKEN, $token_table, $token_table, $token_table, $token); } } return $joins; } public function getQueryApplicationClass() { return 'PhabricatorProjectApplication'; } protected function getPrimaryTableAlias() { return 'p'; } private function linkProjectGraph(array $projects, array $ancestors) { $ancestor_map = mpull($ancestors, null, 'getPHID'); $projects_map = mpull($projects, null, 'getPHID'); $all_map = $projects_map + $ancestor_map; $done = array(); foreach ($projects as $key => $project) { $seen = array($project->getPHID() => true); if (!$this->linkProject($project, $all_map, $done, $seen)) { $this->didRejectResult($project); unset($projects[$key]); continue; } foreach ($project->getAncestorProjects() as $ancestor) { $seen[$ancestor->getPHID()] = true; } } return $projects; } private function linkProject($project, array $all, array $done, array $seen) { $parent_phid = $project->getParentProjectPHID(); // This project has no parent, so just attach `null` and return. if (!$parent_phid) { $project->attachParentProject(null); return true; } // This project has a parent, but it failed to load. if (empty($all[$parent_phid])) { return false; } // Test for graph cycles. If we encounter one, we're going to hide the // entire cycle since we can't meaningfully resolve it. if (isset($seen[$parent_phid])) { return false; } $seen[$parent_phid] = true; $parent = $all[$parent_phid]; $project->attachParentProject($parent); if (!empty($done[$parent_phid])) { return true; } return $this->linkProject($parent, $all, $done, $seen); } private function getAllReachableAncestors(array $projects) { $ancestors = array(); $seen = mpull($projects, null, 'getPHID'); $stack = $projects; while ($stack) { $project = array_pop($stack); $phid = $project->getPHID(); $ancestors[$phid] = $project; $parent_phid = $project->getParentProjectPHID(); if (!$parent_phid) { continue; } if (isset($seen[$parent_phid])) { continue; } $seen[$parent_phid] = true; $stack[] = $project->getParentProject(); } return $ancestors; } private function loadSlugs(array $projects) { // Build a map from primary slugs to projects. $primary_map = array(); foreach ($projects as $project) { $primary_slug = $project->getPrimarySlug(); if ($primary_slug === null) { continue; } $primary_map[$primary_slug] = $project; } // Link up all of the queried slugs which correspond to primary // slugs. If we can link up everything from this (no slugs were queried, // or only primary slugs were queried) we don't need to load anything // else. $unknown = $this->slugNormals; foreach ($unknown as $input => $normal) { if (isset($primary_map[$input])) { $match = $input; } else if (isset($primary_map[$normal])) { $match = $normal; } else { continue; } $this->slugMap[$input] = array( 'slug' => $match, 'projectPHID' => $primary_map[$match]->getPHID(), ); unset($unknown[$input]); } // If we need slugs, we have to load everything. // If we still have some queried slugs which we haven't mapped, we only // need to look for them. // If we've mapped everything, we don't have to do any work. $project_phids = mpull($projects, 'getPHID'); if ($this->needSlugs) { $slugs = id(new PhabricatorProjectSlug())->loadAllWhere( 'projectPHID IN (%Ls)', $project_phids); } else if ($unknown) { $slugs = id(new PhabricatorProjectSlug())->loadAllWhere( 'projectPHID IN (%Ls) AND slug IN (%Ls)', $project_phids, $unknown); } else { $slugs = array(); } // Link up any slugs we were not able to link up earlier. $extra_map = mpull($slugs, 'getProjectPHID', 'getSlug'); foreach ($unknown as $input => $normal) { if (isset($extra_map[$input])) { $match = $input; } else if (isset($extra_map[$normal])) { $match = $normal; } else { continue; } $this->slugMap[$input] = array( 'slug' => $match, 'projectPHID' => $extra_map[$match], ); unset($unknown[$input]); } if ($this->needSlugs) { $slug_groups = mgroup($slugs, 'getProjectPHID'); foreach ($projects as $project) { $project_slugs = idx($slug_groups, $project->getPHID(), array()); $project->attachSlugs($project_slugs); } } } } diff --git a/src/applications/transactions/storage/PhabricatorEditEngineConfiguration.php b/src/applications/transactions/storage/PhabricatorEditEngineConfiguration.php index a2793d626f..38a4fda0a6 100644 --- a/src/applications/transactions/storage/PhabricatorEditEngineConfiguration.php +++ b/src/applications/transactions/storage/PhabricatorEditEngineConfiguration.php @@ -1,311 +1,323 @@ setEngineKey($engine->getEngineKey()) ->attachEngine($engine) ->setViewPolicy(PhabricatorPolicies::getMostOpenPolicy()); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorEditEngineConfigurationPHIDType::TYPECONST); } public function getCreateSortKey() { return $this->getSortKey($this->createOrder); } public function getEditSortKey() { return $this->getSortKey($this->editOrder); } private function getSortKey($order) { // Put objects at the bottom by default if they haven't previously been // reordered. When they're explicitly reordered, the smallest sort key we // assign is 1, so if the object has a value of 0 it means it hasn't been // ordered yet. if ($order != 0) { $group = 'A'; } else { $group = 'B'; } return sprintf( "%s%012d%s\0%012d", $group, $order, $this->getName(), $this->getID()); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'properties' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'engineKey' => 'text64', 'builtinKey' => 'text64?', 'name' => 'text255', 'isDisabled' => 'bool', 'isDefault' => 'bool', 'isEdit' => 'bool', 'createOrder' => 'uint32', 'editOrder' => 'uint32', ), self::CONFIG_KEY_SCHEMA => array( 'key_engine' => array( 'columns' => array('engineKey', 'builtinKey'), 'unique' => true, ), 'key_default' => array( 'columns' => array('engineKey', 'isDefault', 'isDisabled'), ), 'key_edit' => array( 'columns' => array('engineKey', 'isEdit', 'isDisabled'), ), ), ) + parent::getConfiguration(); } public function getProperty($key, $default = null) { return idx($this->properties, $key, $default); } public function setProperty($key, $value) { $this->properties[$key] = $value; return $this; } public function attachEngine(PhabricatorEditEngine $engine) { $this->engine = $engine; return $this; } public function getEngine() { return $this->assertAttached($this->engine); } public function applyConfigurationToFields( PhabricatorEditEngine $engine, $object, array $fields) { $fields = mpull($fields, null, 'getKey'); $is_new = !$object->getID(); $values = $this->getProperty('defaults', array()); foreach ($fields as $key => $field) { if ($is_new) { if (array_key_exists($key, $values)) { $field->readDefaultValueFromConfiguration($values[$key]); } } } $locks = $this->getFieldLocks(); foreach ($fields as $field) { $key = $field->getKey(); switch (idx($locks, $key)) { case self::LOCK_LOCKED: $field->setIsHidden(false); $field->setIsLocked(true); break; case self::LOCK_HIDDEN: $field->setIsHidden(true); $field->setIsLocked(false); break; case self::LOCK_VISIBLE: $field->setIsHidden(false); $field->setIsLocked(false); break; default: // If we don't have an explicit value, don't make any adjustments. break; } } $fields = $this->reorderFields($fields); $preamble = $this->getPreamble(); if (strlen($preamble)) { $fields = array( 'config.preamble' => id(new PhabricatorInstructionsEditField()) ->setKey('config.preamble') ->setIsReorderable(false) ->setIsDefaultable(false) ->setIsLockable(false) ->setValue($preamble), ) + $fields; } return $fields; } private function reorderFields(array $fields) { + // Fields which can not be reordered are fixed in order at the top of the + // form. These are used to show instructions or contextual information. + + $fixed = array(); + foreach ($fields as $key => $field) { + if (!$field->getIsReorderable()) { + $fixed[$key] = $field; + } + } + $keys = $this->getFieldOrder(); - $fields = array_select_keys($fields, $keys) + $fields; + + $fields = $fixed + array_select_keys($fields, $keys) + $fields; + return $fields; } public function getURI() { $engine_key = $this->getEngineKey(); $key = $this->getIdentifier(); return "/transactions/editengine/{$engine_key}/view/{$key}/"; } public function getIdentifier() { $key = $this->getID(); if (!$key) { $key = $this->getBuiltinKey(); } return $key; } public function getDisplayName() { $name = $this->getName(); if (strlen($name)) { return $name; } $builtin = $this->getBuiltinKey(); if ($builtin !== null) { return pht('Builtin Form "%s"', $builtin); } return pht('Untitled Form'); } public function getPreamble() { return $this->getProperty('preamble'); } public function setPreamble($preamble) { return $this->setProperty('preamble', $preamble); } public function setFieldOrder(array $field_order) { return $this->setProperty('order', $field_order); } public function getFieldOrder() { return $this->getProperty('order', array()); } public function setFieldLocks(array $field_locks) { return $this->setProperty('locks', $field_locks); } public function getFieldLocks() { return $this->getProperty('locks', array()); } public function getFieldDefault($key) { $defaults = $this->getProperty('defaults', array()); return idx($defaults, $key); } public function setFieldDefault($key, $value) { $defaults = $this->getProperty('defaults', array()); $defaults[$key] = $value; return $this->setProperty('defaults', $defaults); } public function getIcon() { return $this->getEngine()->getIcon(); } /* -( 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->getEngine() ->getApplication() ->getPolicy($capability); } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return PhabricatorPolicyFilter::hasCapability( $viewer, $this->getEngine()->getApplication(), PhabricatorPolicyCapability::CAN_EDIT); } return false; } public function describeAutomaticCapability($capability) { return null; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhabricatorEditEngineConfigurationEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new PhabricatorEditEngineConfigurationTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } }