1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-22 23:02:42 +01:00

Support subtypes in Projects

Summary:
Ref T13242. See PHI1039. Maniphest subtypes generally seem to be working well. I designed them as a general capability that might be extended to other `EditEngine` objects later, and PHI1039 describes a situation where extending subtypes to projects would give us some reasonable tools.

(Some installs also already use icons/colors as a sort of lightweight version of subtypes, so I believe this is generally useful capability.)

Some of this is a little bit copy-pasted and could probably be shared, but I'd like to wait a bit longer before merging it. For example, both configs have exactly the same structure right now, but Projects should possibly have some different flags (for example: to disable creating subprojects / milestones).

This implementation is pretty basic for now: notably, subprojects/milestones don't get the nice "choose from among subtype forms" treatment that tasks do. If this ends up being part of a solution to PHI1039, I'd plan to fill that in later on.

Test Plan: Defined multiple subtypes, created subtype forms, created projects with appropriate subtypes. Filtered them by subtype. Saw subtype information on list/detail views.

Reviewers: amckinley

Reviewed By: amckinley

Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam

Maniphest Tasks: T13242

Differential Revision: https://secure.phabricator.com/D20040
This commit is contained in:
epriestley 2019-01-26 07:29:03 -08:00
parent 13e4aeb590
commit c9760e8d64
11 changed files with 174 additions and 1 deletions

View file

@ -0,0 +1,2 @@
ALTER TABLE {$NAMESPACE}_project.project
ADD subtype VARCHAR(64) COLLATE {$COLLATE_TEXT} NOT NULL;

View file

@ -0,0 +1,2 @@
UPDATE {$NAMESPACE}_project.project
SET subtype = 'default' WHERE subtype = '';

View file

@ -4114,6 +4114,8 @@ phutil_register_library_map(array(
'PhabricatorProjectSubprojectWarningController' => 'applications/project/controller/PhabricatorProjectSubprojectWarningController.php', 'PhabricatorProjectSubprojectWarningController' => 'applications/project/controller/PhabricatorProjectSubprojectWarningController.php',
'PhabricatorProjectSubprojectsController' => 'applications/project/controller/PhabricatorProjectSubprojectsController.php', 'PhabricatorProjectSubprojectsController' => 'applications/project/controller/PhabricatorProjectSubprojectsController.php',
'PhabricatorProjectSubprojectsProfileMenuItem' => 'applications/project/menuitem/PhabricatorProjectSubprojectsProfileMenuItem.php', 'PhabricatorProjectSubprojectsProfileMenuItem' => 'applications/project/menuitem/PhabricatorProjectSubprojectsProfileMenuItem.php',
'PhabricatorProjectSubtypeDatasource' => 'applications/project/typeahead/PhabricatorProjectSubtypeDatasource.php',
'PhabricatorProjectSubtypesConfigType' => 'applications/project/config/PhabricatorProjectSubtypesConfigType.php',
'PhabricatorProjectTestDataGenerator' => 'applications/project/lipsum/PhabricatorProjectTestDataGenerator.php', 'PhabricatorProjectTestDataGenerator' => 'applications/project/lipsum/PhabricatorProjectTestDataGenerator.php',
'PhabricatorProjectTransaction' => 'applications/project/storage/PhabricatorProjectTransaction.php', 'PhabricatorProjectTransaction' => 'applications/project/storage/PhabricatorProjectTransaction.php',
'PhabricatorProjectTransactionEditor' => 'applications/project/editor/PhabricatorProjectTransactionEditor.php', 'PhabricatorProjectTransactionEditor' => 'applications/project/editor/PhabricatorProjectTransactionEditor.php',
@ -10029,6 +10031,7 @@ phutil_register_library_map(array(
'PhabricatorConduitResultInterface', 'PhabricatorConduitResultInterface',
'PhabricatorColumnProxyInterface', 'PhabricatorColumnProxyInterface',
'PhabricatorSpacesInterface', 'PhabricatorSpacesInterface',
'PhabricatorEditEngineSubtypeInterface',
), ),
'PhabricatorProjectAddHeraldAction' => 'PhabricatorProjectHeraldAction', 'PhabricatorProjectAddHeraldAction' => 'PhabricatorProjectHeraldAction',
'PhabricatorProjectApplication' => 'PhabricatorApplication', 'PhabricatorProjectApplication' => 'PhabricatorApplication',
@ -10156,6 +10159,8 @@ phutil_register_library_map(array(
'PhabricatorProjectSubprojectWarningController' => 'PhabricatorProjectController', 'PhabricatorProjectSubprojectWarningController' => 'PhabricatorProjectController',
'PhabricatorProjectSubprojectsController' => 'PhabricatorProjectController', 'PhabricatorProjectSubprojectsController' => 'PhabricatorProjectController',
'PhabricatorProjectSubprojectsProfileMenuItem' => 'PhabricatorProfileMenuItem', 'PhabricatorProjectSubprojectsProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorProjectSubtypeDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorProjectSubtypesConfigType' => 'PhabricatorJSONConfigType',
'PhabricatorProjectTestDataGenerator' => 'PhabricatorTestDataGenerator', 'PhabricatorProjectTestDataGenerator' => 'PhabricatorTestDataGenerator',
'PhabricatorProjectTransaction' => 'PhabricatorModularTransaction', 'PhabricatorProjectTransaction' => 'PhabricatorModularTransaction',
'PhabricatorProjectTransactionEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorProjectTransactionEditor' => 'PhabricatorApplicationTransactionEditor',

View file

@ -83,6 +83,34 @@ EOTEXT
$custom_field_type = 'custom:PhabricatorCustomFieldConfigOptionType'; $custom_field_type = 'custom:PhabricatorCustomFieldConfigOptionType';
$subtype_type = 'projects.subtypes';
$subtype_default_key = PhabricatorEditEngineSubtype::SUBTYPE_DEFAULT;
$subtype_example = array(
array(
'key' => $subtype_default_key,
'name' => pht('Project'),
),
array(
'key' => 'team',
'name' => pht('Team'),
),
);
$subtype_example = id(new PhutilJSON())->encodeAsList($subtype_example);
$subtype_default = array(
array(
'key' => $subtype_default_key,
'name' => pht('Project'),
),
);
$subtype_description = $this->deformat(pht(<<<EOTEXT
Allows you to define project subtypes. For a more detailed description of
subtype configuration, see @{config:maniphest.subtypes}.
EOTEXT
));
return array( return array(
$this->newOption('projects.custom-field-definitions', 'wild', array()) $this->newOption('projects.custom-field-definitions', 'wild', array())
->setSummary(pht('Custom Projects fields.')) ->setSummary(pht('Custom Projects fields.'))
@ -102,6 +130,11 @@ EOTEXT
$this->newOption('projects.colors', $colors_type, $default_colors) $this->newOption('projects.colors', $colors_type, $default_colors)
->setSummary(pht('Adjust project colors.')) ->setSummary(pht('Adjust project colors.'))
->setDescription($colors_description), ->setDescription($colors_description),
$this->newOption('projects.subtypes', $subtype_type, $subtype_default)
->setSummary(pht('Define project subtypes.'))
->setDescription($subtype_description)
->addExample($subtype_example, pht('Simple Subtypes')),
); );
} }

View file

@ -0,0 +1,14 @@
<?php
final class PhabricatorProjectSubtypesConfigType
extends PhabricatorJSONConfigType {
const TYPEKEY = 'projects.subtypes';
public function validateStoredValue(
PhabricatorConfigOption $option,
$value) {
PhabricatorEditEngineSubtype::validateConfiguration($value);
}
}

View file

@ -51,6 +51,12 @@ final class PhabricatorProjectProfileController
$watch_action = $this->renderWatchAction($project); $watch_action = $this->renderWatchAction($project);
$header->addActionLink($watch_action); $header->addActionLink($watch_action);
$subtype = $project->newSubtypeObject();
if ($subtype && $subtype->hasTagView()) {
$subtype_tag = $subtype->newTagView();
$header->addTag($subtype_tag);
}
$milestone_list = $this->buildMilestoneList($project); $milestone_list = $this->buildMilestoneList($project);
$subproject_list = $this->buildSubprojectList($project); $subproject_list = $this->buildSubprojectList($project);

View file

@ -24,6 +24,7 @@ final class PhabricatorProjectQuery
private $maxDepth; private $maxDepth;
private $minMilestoneNumber; private $minMilestoneNumber;
private $maxMilestoneNumber; private $maxMilestoneNumber;
private $subtypes;
private $status = 'status-any'; private $status = 'status-any';
const STATUS_ANY = 'status-any'; const STATUS_ANY = 'status-any';
@ -131,6 +132,11 @@ final class PhabricatorProjectQuery
return $this; return $this;
} }
public function withSubtypes(array $subtypes) {
$this->subtypes = $subtypes;
return $this;
}
public function needMembers($need_members) { public function needMembers($need_members) {
$this->needMembers = $need_members; $this->needMembers = $need_members;
return $this; return $this;
@ -618,6 +624,13 @@ final class PhabricatorProjectQuery
$this->maxMilestoneNumber); $this->maxMilestoneNumber);
} }
if ($this->subtypes !== null) {
$where[] = qsprintf(
$conn,
'subtype IN (%Ls)',
$this->subtypes);
}
return $where; return $where;
} }

View file

@ -19,6 +19,9 @@ final class PhabricatorProjectSearchEngine
} }
protected function buildCustomSearchFields() { protected function buildCustomSearchFields() {
$subtype_map = id(new PhabricatorProject())->newEditEngineSubtypeMap();
$hide_subtypes = ($subtype_map->getCount() == 1);
return array( return array(
id(new PhabricatorSearchTextField()) id(new PhabricatorSearchTextField())
->setLabel(pht('Name')) ->setLabel(pht('Name'))
@ -62,6 +65,14 @@ final class PhabricatorProjectSearchEngine
pht( pht(
'Pass true to find only milestones, or false to omit '. 'Pass true to find only milestones, or false to omit '.
'milestones.')), 'milestones.')),
id(new PhabricatorSearchDatasourceField())
->setLabel(pht('Subtypes'))
->setKey('subtypes')
->setAliases(array('subtype'))
->setDescription(
pht('Search for projects with given subtypes.'))
->setDatasource(new PhabricatorProjectSubtypeDatasource())
->setIsHidden($hide_subtypes),
id(new PhabricatorSearchCheckboxesField()) id(new PhabricatorSearchCheckboxesField())
->setLabel(pht('Icons')) ->setLabel(pht('Icons'))
->setKey('icons') ->setKey('icons')
@ -134,6 +145,10 @@ final class PhabricatorProjectSearchEngine
$query->withAncestorProjectPHIDs($map['ancestorPHIDs']); $query->withAncestorProjectPHIDs($map['ancestorPHIDs']);
} }
if ($map['subtypes']) {
$query->withSubtypes($map['subtypes']);
}
return $query; return $query;
} }

View file

@ -12,7 +12,8 @@ final class PhabricatorProject extends PhabricatorProjectDAO
PhabricatorFerretInterface, PhabricatorFerretInterface,
PhabricatorConduitResultInterface, PhabricatorConduitResultInterface,
PhabricatorColumnProxyInterface, PhabricatorColumnProxyInterface,
PhabricatorSpacesInterface { PhabricatorSpacesInterface,
PhabricatorEditEngineSubtypeInterface {
protected $name; protected $name;
protected $status = PhabricatorProjectStatus::STATUS_ACTIVE; protected $status = PhabricatorProjectStatus::STATUS_ACTIVE;
@ -40,6 +41,7 @@ final class PhabricatorProject extends PhabricatorProjectDAO
protected $properties = array(); protected $properties = array();
protected $spacePHID; protected $spacePHID;
protected $subtype;
private $memberPHIDs = self::ATTACHABLE; private $memberPHIDs = self::ATTACHABLE;
private $watcherPHIDs = self::ATTACHABLE; private $watcherPHIDs = self::ATTACHABLE;
@ -102,6 +104,7 @@ final class PhabricatorProject extends PhabricatorProjectDAO
->setHasWorkboard(0) ->setHasWorkboard(0)
->setHasMilestones(0) ->setHasMilestones(0)
->setHasSubprojects(0) ->setHasSubprojects(0)
->setSubtype(PhabricatorEditEngineSubtype::SUBTYPE_DEFAULT)
->attachParentProject(null); ->attachParentProject(null);
} }
@ -237,6 +240,7 @@ final class PhabricatorProject extends PhabricatorProjectDAO
'projectPath' => 'hashpath64', 'projectPath' => 'hashpath64',
'projectDepth' => 'uint32', 'projectDepth' => 'uint32',
'projectPathKey' => 'bytes4', 'projectPathKey' => 'bytes4',
'subtype' => 'text64',
), ),
self::CONFIG_KEY_SCHEMA => array( self::CONFIG_KEY_SCHEMA => array(
'key_icon' => array( 'key_icon' => array(
@ -765,6 +769,10 @@ final class PhabricatorProject extends PhabricatorProjectDAO
->setKey('slug') ->setKey('slug')
->setType('string') ->setType('string')
->setDescription(pht('Primary slug/hashtag.')), ->setDescription(pht('Primary slug/hashtag.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('subtype')
->setType('string')
->setDescription(pht('Subtype of the project.')),
id(new PhabricatorConduitSearchFieldSpecification()) id(new PhabricatorConduitSearchFieldSpecification())
->setKey('milestone') ->setKey('milestone')
->setType('int?') ->setType('int?')
@ -814,6 +822,7 @@ final class PhabricatorProject extends PhabricatorProjectDAO
return array( return array(
'name' => $this->getName(), 'name' => $this->getName(),
'slug' => $this->getPrimarySlug(), 'slug' => $this->getPrimarySlug(),
'subtype' => $this->getSubtype(),
'milestone' => $milestone, 'milestone' => $milestone,
'depth' => (int)$this->getProjectDepth(), 'depth' => (int)$this->getProjectDepth(),
'parent' => $parent_ref, 'parent' => $parent_ref,
@ -873,4 +882,26 @@ final class PhabricatorProject extends PhabricatorProjectDAO
} }
/* -( PhabricatorEditEngineSubtypeInterface )------------------------------ */
public function getEditEngineSubtype() {
return $this->getSubtype();
}
public function setEditEngineSubtype($value) {
return $this->setSubtype($value);
}
public function newEditEngineSubtypeMap() {
$config = PhabricatorEnv::getEnvConfig('projects.subtypes');
return PhabricatorEditEngineSubtype::newSubtypeMap($config);
}
public function newSubtypeObject() {
$subtype_key = $this->getEditEngineSubtype();
$subtype_map = $this->newEditEngineSubtypeMap();
return $subtype_map->getSubtype($subtype_key);
}
} }

View file

@ -0,0 +1,45 @@
<?php
final class PhabricatorProjectSubtypeDatasource
extends PhabricatorTypeaheadDatasource {
public function getBrowseTitle() {
return pht('Browse Subtypes');
}
public function getPlaceholderText() {
return pht('Type a project subtype name...');
}
public function getDatasourceApplicationClass() {
return 'PhabricatorProjectApplication';
}
public function loadResults() {
$results = $this->buildResults();
return $this->filterResultsAgainstTokens($results);
}
protected function renderSpecialTokens(array $values) {
return $this->renderTokensFromResults($this->buildResults(), $values);
}
private function buildResults() {
$results = array();
$subtype_map = id(new PhabricatorProject())->newEditEngineSubtypeMap();
foreach ($subtype_map->getSubtypes() as $key => $subtype) {
$result = id(new PhabricatorTypeaheadResult())
->setIcon($subtype->getIcon())
->setColor($subtype->getColor())
->setPHID($key)
->setName($subtype->getName());
$results[$key] = $result;
}
return $results;
}
}

View file

@ -87,6 +87,13 @@ final class PhabricatorProjectListView extends AphrontView {
} }
} }
$subtype = $project->newSubtypeObject();
if ($subtype && $subtype->hasTagView()) {
$subtype_tag = $subtype->newTagView()
->setSlimShady(true);
$item->addAttribute($subtype_tag);
}
$list->addItem($item); $list->addItem($item);
} }