mirror of
https://we.phorge.it/source/phorge.git
synced 2025-01-03 11:21:01 +01:00
Some owners write workflows.
This commit is contained in:
parent
5038ab850c
commit
23f882a0ee
24 changed files with 730 additions and 174 deletions
|
@ -1,6 +1,6 @@
|
|||
CREATE DATABASE phabricator_owners;
|
||||
|
||||
CREATE TABLE phabricator_owners.onwners_package (
|
||||
CREATE TABLE phabricator_owners.owners_package (
|
||||
id int unsigned not null auto_increment primary key,
|
||||
phid varchar(64) binary not null,
|
||||
unique key(phid),
|
||||
|
@ -23,6 +23,5 @@ CREATE TABLE phabricator_owners.owners_path (
|
|||
packageID int unsigned not null,
|
||||
key(packageID),
|
||||
repositoryPHID varchar(64) binary not null,
|
||||
path varchar(255) not null,
|
||||
unique key (repositoryPHID, path)
|
||||
path varchar(255) not null
|
||||
);
|
|
@ -63,7 +63,7 @@ celerity_register_resource_map(array(
|
|||
),
|
||||
'aphront-list-filter-view-css' =>
|
||||
array(
|
||||
'uri' => '/res/89f641c5/rsrc/css/aphront/list-filter-view.css',
|
||||
'uri' => '/res/50a790ae/rsrc/css/aphront/list-filter-view.css',
|
||||
'type' => 'css',
|
||||
'requires' =>
|
||||
array(
|
||||
|
@ -81,7 +81,7 @@ celerity_register_resource_map(array(
|
|||
),
|
||||
'aphront-panel-view-css' =>
|
||||
array(
|
||||
'uri' => '/res/63672373/rsrc/css/aphront/panel-view.css',
|
||||
'uri' => '/res/8f9f3632/rsrc/css/aphront/panel-view.css',
|
||||
'type' => 'css',
|
||||
'requires' =>
|
||||
array(
|
||||
|
@ -643,7 +643,7 @@ celerity_register_resource_map(array(
|
|||
), array (
|
||||
'packages' =>
|
||||
array (
|
||||
'e3ec35d7' =>
|
||||
'2de9aa4e' =>
|
||||
array (
|
||||
'name' => 'core.pkg.css',
|
||||
'symbols' =>
|
||||
|
@ -663,7 +663,7 @@ celerity_register_resource_map(array(
|
|||
12 => 'phabricator-remarkup-css',
|
||||
13 => 'syntax-highlighting-css',
|
||||
),
|
||||
'uri' => '/res/pkg/e3ec35d7/core.pkg.css',
|
||||
'uri' => '/res/pkg/2de9aa4e/core.pkg.css',
|
||||
'type' => 'css',
|
||||
),
|
||||
'76f3c1f8' =>
|
||||
|
@ -710,20 +710,20 @@ celerity_register_resource_map(array(
|
|||
),
|
||||
'reverse' =>
|
||||
array (
|
||||
'phabricator-core-css' => 'e3ec35d7',
|
||||
'phabricator-core-buttons-css' => 'e3ec35d7',
|
||||
'phabricator-standard-page-view' => 'e3ec35d7',
|
||||
'aphront-dialog-view-css' => 'e3ec35d7',
|
||||
'aphront-form-view-css' => 'e3ec35d7',
|
||||
'aphront-panel-view-css' => 'e3ec35d7',
|
||||
'aphront-side-nav-view-css' => 'e3ec35d7',
|
||||
'aphront-table-view-css' => 'e3ec35d7',
|
||||
'aphront-crumbs-view-css' => 'e3ec35d7',
|
||||
'aphront-tokenizer-control-css' => 'e3ec35d7',
|
||||
'aphront-typeahead-control-css' => 'e3ec35d7',
|
||||
'phabricator-directory-css' => 'e3ec35d7',
|
||||
'phabricator-remarkup-css' => 'e3ec35d7',
|
||||
'syntax-highlighting-css' => 'e3ec35d7',
|
||||
'phabricator-core-css' => '2de9aa4e',
|
||||
'phabricator-core-buttons-css' => '2de9aa4e',
|
||||
'phabricator-standard-page-view' => '2de9aa4e',
|
||||
'aphront-dialog-view-css' => '2de9aa4e',
|
||||
'aphront-form-view-css' => '2de9aa4e',
|
||||
'aphront-panel-view-css' => '2de9aa4e',
|
||||
'aphront-side-nav-view-css' => '2de9aa4e',
|
||||
'aphront-table-view-css' => '2de9aa4e',
|
||||
'aphront-crumbs-view-css' => '2de9aa4e',
|
||||
'aphront-tokenizer-control-css' => '2de9aa4e',
|
||||
'aphront-typeahead-control-css' => '2de9aa4e',
|
||||
'phabricator-directory-css' => '2de9aa4e',
|
||||
'phabricator-remarkup-css' => '2de9aa4e',
|
||||
'syntax-highlighting-css' => '2de9aa4e',
|
||||
'differential-core-view-css' => '76f3c1f8',
|
||||
'differential-changeset-view-css' => '76f3c1f8',
|
||||
'differential-revision-detail-css' => '76f3c1f8',
|
||||
|
|
|
@ -320,7 +320,9 @@ phutil_register_library_map(array(
|
|||
'PhabricatorObjectSelectorDialog' => 'view/control/objectselector',
|
||||
'PhabricatorOwnersController' => 'applications/owners/controller/base',
|
||||
'PhabricatorOwnersDAO' => 'applications/owners/storage/base',
|
||||
'PhabricatorOwnersDeleteController' => 'applications/owners/controller/delete',
|
||||
'PhabricatorOwnersDetailController' => 'applications/owners/controller/detail',
|
||||
'PhabricatorOwnersEditController' => 'applications/owners/controller/edit',
|
||||
'PhabricatorOwnersListController' => 'applications/owners/controller/list',
|
||||
'PhabricatorOwnersOwner' => 'applications/owners/storage/owner',
|
||||
'PhabricatorOwnersPackage' => 'applications/owners/storage/package',
|
||||
|
@ -677,7 +679,9 @@ phutil_register_library_map(array(
|
|||
'PhabricatorOAuthUnlinkController' => 'PhabricatorAuthController',
|
||||
'PhabricatorOwnersController' => 'PhabricatorController',
|
||||
'PhabricatorOwnersDAO' => 'PhabricatorLiskDAO',
|
||||
'PhabricatorOwnersDeleteController' => 'PhabricatorOwnersController',
|
||||
'PhabricatorOwnersDetailController' => 'PhabricatorOwnersController',
|
||||
'PhabricatorOwnersEditController' => 'PhabricatorOwnersController',
|
||||
'PhabricatorOwnersListController' => 'PhabricatorOwnersController',
|
||||
'PhabricatorOwnersOwner' => 'PhabricatorOwnersDAO',
|
||||
'PhabricatorOwnersPackage' => 'PhabricatorOwnersDAO',
|
||||
|
|
|
@ -260,8 +260,10 @@ class AphrontDefaultApplicationConfiguration
|
|||
'/owners/' => array(
|
||||
'$' => 'PhabricatorOwnersListController',
|
||||
'view/(?P<view>[^/]+)/$' => 'PhabricatorOwnersListController',
|
||||
'edit/(?P<id>\d+)/$' => 'PhabricatorOwnersEditController',
|
||||
'new/$' => 'PhabricatorOwnersEditController',
|
||||
'package/(?P<id>\d+)/$' => 'PhabricatorOwnersDetailController',
|
||||
'new/$' => 'PhabricatorOwnersDetailController',
|
||||
'delete/(?P<id>\d+)/$' => 'PhabricatorOwnersDeleteController',
|
||||
),
|
||||
|
||||
);
|
||||
|
|
|
@ -47,8 +47,7 @@ class DiffusionPathCompleteController extends DiffusionController {
|
|||
$drequest = DiffusionRequest::newFromAphrontRequestDictionary(
|
||||
array(
|
||||
'callsign' => $repository->getCallsign(),
|
||||
'path' => $query_dir,
|
||||
'nobranch' => true,
|
||||
'path' => ':/'.$query_dir,
|
||||
));
|
||||
|
||||
$browse_query = DiffusionBrowseQuery::newFromDiffusionRequest($drequest);
|
||||
|
|
|
@ -39,8 +39,7 @@ class DiffusionPathValidateController extends DiffusionController {
|
|||
$drequest = DiffusionRequest::newFromAphrontRequestDictionary(
|
||||
array(
|
||||
'callsign' => $repository->getCallsign(),
|
||||
'path' => $path,
|
||||
'nobranch' => true,
|
||||
'path' => ':/'.$path,
|
||||
));
|
||||
|
||||
$browse_query = DiffusionBrowseQuery::newFromDiffusionRequest($drequest);
|
||||
|
|
|
@ -24,8 +24,8 @@ class DiffusionGitRequest extends DiffusionRequest {
|
|||
$path = $this->path;
|
||||
$parts = explode('/', $path);
|
||||
|
||||
if (empty($data['nobranch'])) {
|
||||
$branch = array_shift($parts);
|
||||
$branch = array_shift($parts);
|
||||
if ($branch != ':') {
|
||||
$this->branch = $this->decodeBranchName($branch);
|
||||
}
|
||||
|
||||
|
|
|
@ -20,6 +20,14 @@ class DiffusionSvnRequest extends DiffusionRequest {
|
|||
|
||||
private $loadedCommit;
|
||||
|
||||
protected function initializeFromAphrontRequestDictionary(array $data) {
|
||||
parent::initializeFromAphrontRequestDictionary($data);
|
||||
if (!strncmp($this->path, ':', 1)) {
|
||||
$this->path = substr($this->path, 1);
|
||||
$this->path = ltrim($this->path, '/');
|
||||
}
|
||||
}
|
||||
|
||||
public function getCommit() {
|
||||
if ($this->commit) {
|
||||
return $this->commit;
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* Copyright 2011 Facebook, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
class PhabricatorOwnersDeleteController extends PhabricatorOwnersController {
|
||||
|
||||
private $id;
|
||||
|
||||
public function willProcessRequest(array $data) {
|
||||
$this->id = $data['id'];
|
||||
}
|
||||
|
||||
public function processRequest() {
|
||||
$request = $this->getRequest();
|
||||
$user = $request->getUser();
|
||||
|
||||
$package = id(new PhabricatorOwnersPackage())->load($this->id);
|
||||
if (!$package) {
|
||||
return new Aphront404Response();
|
||||
}
|
||||
|
||||
if ($request->isDialogFormPost()) {
|
||||
$package->delete();
|
||||
return id(new AphrontRedirectResponse())->setURI('/owners/');
|
||||
}
|
||||
|
||||
$dialog = id(new AphrontDialogView())
|
||||
->setUser($user)
|
||||
->setTitle('Really delete this package?')
|
||||
->appendChild(
|
||||
'<p>Are you sure you want to delete the "'.
|
||||
phutil_escape_html($package->getName()).'" package? This operation '.
|
||||
'can not be undone.</p>')
|
||||
->addSubmitButton('Delete')
|
||||
->addCancelButton('/owners/package/'.$package->getID().'/')
|
||||
->setSubmitURI($request->getRequestURI());
|
||||
|
||||
return id(new AphrontDialogResponse())->setDialog($dialog);
|
||||
}
|
||||
|
||||
}
|
19
src/applications/owners/controller/delete/__init__.php
Normal file
19
src/applications/owners/controller/delete/__init__.php
Normal file
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
/**
|
||||
* This file is automatically generated. Lint this module to rebuild it.
|
||||
* @generated
|
||||
*/
|
||||
|
||||
|
||||
|
||||
phutil_require_module('phabricator', 'aphront/response/404');
|
||||
phutil_require_module('phabricator', 'aphront/response/dialog');
|
||||
phutil_require_module('phabricator', 'aphront/response/redirect');
|
||||
phutil_require_module('phabricator', 'applications/owners/controller/base');
|
||||
phutil_require_module('phabricator', 'applications/owners/storage/package');
|
||||
phutil_require_module('phabricator', 'view/dialog');
|
||||
|
||||
phutil_require_module('phutil', 'utils');
|
||||
|
||||
|
||||
phutil_require_source('PhabricatorOwnersDeleteController.php');
|
|
@ -21,132 +21,122 @@ class PhabricatorOwnersDetailController extends PhabricatorOwnersController {
|
|||
private $id;
|
||||
|
||||
public function willProcessRequest(array $data) {
|
||||
$this->id = idx($data, 'id');
|
||||
$this->id = $data['id'];
|
||||
}
|
||||
|
||||
public function processRequest() {
|
||||
$request = $this->getRequest();
|
||||
$user = $request->getUser();
|
||||
|
||||
if ($this->id) {
|
||||
$package = id(new PhabricatorOwnersPackage())->load($this->id);
|
||||
if (!$package) {
|
||||
return new Aphront404Response();
|
||||
}
|
||||
} else {
|
||||
$package = new PhabricatorOwnersPackage();
|
||||
$package->setPrimaryOwnerPHID($user->getPHID());
|
||||
$package = id(new PhabricatorOwnersPackage())->load($this->id);
|
||||
if (!$package) {
|
||||
return new Aphront404Response();
|
||||
}
|
||||
|
||||
$e_name = true;
|
||||
$e_primary = true;
|
||||
$paths = $package->loadPaths();
|
||||
$owners = $package->loadOwners();
|
||||
|
||||
|
||||
$token_primary_owner = array();
|
||||
$token_all_owners = array();
|
||||
|
||||
$title = $package->getID() ? 'Edit Package' : 'New Package';
|
||||
|
||||
$repos = id(new PhabricatorRepository())->loadAll();
|
||||
|
||||
$default_paths = array();
|
||||
foreach ($repos as $repo) {
|
||||
$default_path = $repo->getDetail('default-owners-path');
|
||||
if ($default_path) {
|
||||
$default_paths[$repo->getPHID()] = $default_path;
|
||||
}
|
||||
$phids = array();
|
||||
foreach ($paths as $path) {
|
||||
$phids[$path->getRepositoryPHID()] = true;
|
||||
}
|
||||
foreach ($owners as $owner) {
|
||||
$phids[$owner->getUserPHID()] = true;
|
||||
}
|
||||
$phids = array_keys($phids);
|
||||
|
||||
$repos = mpull($repos, 'getCallsign', 'getPHID');
|
||||
$handles = id(new PhabricatorObjectHandleData($phids))->loadHandles();
|
||||
|
||||
$template = new AphrontTypeaheadTemplateView();
|
||||
$template = $template->render();
|
||||
$rows = array();
|
||||
|
||||
$rows[] = array(
|
||||
'Name',
|
||||
phutil_escape_html($package->getName()));
|
||||
$rows[] = array(
|
||||
'Description',
|
||||
phutil_escape_html($package->getDescription()));
|
||||
|
||||
Javelin::initBehavior(
|
||||
'owners-path-editor',
|
||||
$primary_owner = null;
|
||||
$primary_phid = $package->getPrimaryOwnerPHID();
|
||||
if ($primary_phid && isset($handles[$primary_phid])) {
|
||||
$primary_owner =
|
||||
'<strong>'.$handles[$primary_phid]->renderLink().'</strong>';
|
||||
}
|
||||
$rows[] = array(
|
||||
'Primary Owner',
|
||||
$primary_owner,
|
||||
);
|
||||
|
||||
$owner_links = array();
|
||||
foreach ($owners as $owner) {
|
||||
$owner_links[] = $handles[$owner->getUserPHID()]->renderLink();
|
||||
}
|
||||
$owner_links = implode('<br />', $owner_links);
|
||||
$rows[] = array(
|
||||
'Owners',
|
||||
$owner_links);
|
||||
|
||||
$path_links = array();
|
||||
foreach ($paths as $path) {
|
||||
$callsign = $handles[$path->getRepositoryPHID()]->getName();
|
||||
$repo = phutil_escape_html('r'.$callsign);
|
||||
$path_link = phutil_render_tag(
|
||||
'a',
|
||||
array(
|
||||
'href' => '/diffusion/'.$callsign.'/browse/:'.$path->getPath(),
|
||||
),
|
||||
phutil_escape_html($path->getPath()));
|
||||
$path_links[] = $repo.' '.$path_link;
|
||||
}
|
||||
$path_links = implode('<br />', $path_links);
|
||||
$rows[] = array(
|
||||
'Paths',
|
||||
$path_links);
|
||||
|
||||
$table = new AphrontTableView($rows);
|
||||
$table->setColumnClasses(
|
||||
array(
|
||||
'root' => 'path-editor',
|
||||
'table' => 'paths',
|
||||
'add_button' => 'addpath',
|
||||
'repositories' => $repos,
|
||||
'input_template' => $template,
|
||||
'path_refs' => array(),
|
||||
|
||||
'completeURI' => '/diffusion/services/path/complete/',
|
||||
'validateURI' => '/diffusion/services/path/validate/',
|
||||
|
||||
'repositoryDefaultPaths' => $default_paths,
|
||||
'header',
|
||||
'wide',
|
||||
));
|
||||
|
||||
require_celerity_resource('owners-path-editor-css');
|
||||
|
||||
$form = id(new AphrontFormView())
|
||||
->setUser($user)
|
||||
->appendChild(
|
||||
id(new AphrontFormTextControl())
|
||||
->setLabel('Name')
|
||||
->setName('name')
|
||||
->setValue($package->getName())
|
||||
->setError($e_name))
|
||||
->appendChild(
|
||||
id(new AphrontFormTokenizerControl())
|
||||
->setDatasource('/typeahead/common/users/')
|
||||
->setLabel('Primary Owner')
|
||||
->setName('primary')
|
||||
->setLimit(1)
|
||||
->setValue($token_primary_owner)
|
||||
->setError($e_primary))
|
||||
->appendChild(
|
||||
id(new AphrontFormTokenizerControl())
|
||||
->setDatasource('/typeahead/common/users/')
|
||||
->setLabel('Owners')
|
||||
->setName('owners')
|
||||
->setValue($token_all_owners)
|
||||
->setError($e_primary))
|
||||
->appendChild(
|
||||
'<h1>Paths</h1>'.
|
||||
'<div class="aphront-form-inset" id="path-editor">'.
|
||||
'<div style="float: right;">'.
|
||||
javelin_render_tag(
|
||||
'a',
|
||||
array(
|
||||
'href' => '#',
|
||||
'class' => 'button green',
|
||||
'sigil' => 'addpath',
|
||||
'mustcapture' => true,
|
||||
),
|
||||
'Add New Path').
|
||||
'</div>'.
|
||||
'<p>Specify the files and directories which comprise this '.
|
||||
'package.</p>'.
|
||||
'<div style="clear: both;"></div>'.
|
||||
javelin_render_tag(
|
||||
'table',
|
||||
array(
|
||||
'class' => 'owners-path-editor-table',
|
||||
'sigil' => 'paths',
|
||||
),
|
||||
'').
|
||||
'</div>')
|
||||
->appendChild(
|
||||
id(new AphrontFormTextAreaControl())
|
||||
->setLabel('Description')
|
||||
->setName('description')
|
||||
->setValue($package->getDescription()))
|
||||
->appendChild(
|
||||
id(new AphrontFormSubmitControl())
|
||||
->setValue('Save Package'));
|
||||
|
||||
$panel = new AphrontPanelView();
|
||||
$panel->setHeader($title);
|
||||
$panel->setWidth(AphrontPanelView::WIDTH_WIDE);
|
||||
$panel->appendChild($form);
|
||||
$panel->setHeader(
|
||||
'Package Details for "'.phutil_escape_html($package->getName()).'"');
|
||||
$panel->addButton(
|
||||
javelin_render_tag(
|
||||
'a',
|
||||
array(
|
||||
'href' => '/owners/delete/'.$package->getID().'/',
|
||||
'class' => 'button grey',
|
||||
'sigil' => 'workflow',
|
||||
),
|
||||
'Delete Package'));
|
||||
$panel->addButton(
|
||||
phutil_render_tag(
|
||||
'a',
|
||||
array(
|
||||
'href' => '/owners/edit/'.$package->getID().'/',
|
||||
'class' => 'button',
|
||||
),
|
||||
'Edit Package'));
|
||||
$panel->appendChild($table);
|
||||
|
||||
$nav = new AphrontSideNavView();
|
||||
$nav->appendChild($panel);
|
||||
$nav->addNavItem(
|
||||
phutil_render_tag(
|
||||
'a',
|
||||
array(
|
||||
'href' => '/owners/package/'.$package->getID().'/',
|
||||
'class' => 'aphront-side-nav-selected',
|
||||
),
|
||||
'Package Details'));
|
||||
|
||||
return $this->buildStandardPageResponse(
|
||||
$panel,
|
||||
$nav,
|
||||
array(
|
||||
'title' => $title,
|
||||
'title' => "Package '".$package->getName()."'",
|
||||
));
|
||||
}
|
||||
|
||||
|
|
|
@ -9,14 +9,13 @@
|
|||
phutil_require_module('phabricator', 'aphront/response/404');
|
||||
phutil_require_module('phabricator', 'applications/owners/controller/base');
|
||||
phutil_require_module('phabricator', 'applications/owners/storage/package');
|
||||
phutil_require_module('phabricator', 'applications/repository/storage/repository');
|
||||
phutil_require_module('phabricator', 'infrastructure/celerity/api');
|
||||
phutil_require_module('phabricator', 'infrastructure/javelin/api');
|
||||
phutil_require_module('phabricator', 'view/control/typeahead');
|
||||
phutil_require_module('phabricator', 'view/form/base');
|
||||
phutil_require_module('phabricator', 'view/form/control/submit');
|
||||
phutil_require_module('phabricator', 'applications/phid/handle/data');
|
||||
phutil_require_module('phabricator', 'infrastructure/javelin/markup');
|
||||
phutil_require_module('phabricator', 'view/control/table');
|
||||
phutil_require_module('phabricator', 'view/layout/panel');
|
||||
phutil_require_module('phabricator', 'view/layout/sidenav');
|
||||
|
||||
phutil_require_module('phutil', 'markup');
|
||||
phutil_require_module('phutil', 'utils');
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,258 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* Copyright 2011 Facebook, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
class PhabricatorOwnersEditController extends PhabricatorOwnersController {
|
||||
|
||||
private $id;
|
||||
|
||||
public function willProcessRequest(array $data) {
|
||||
$this->id = idx($data, 'id');
|
||||
}
|
||||
|
||||
public function processRequest() {
|
||||
$request = $this->getRequest();
|
||||
$user = $request->getUser();
|
||||
|
||||
if ($this->id) {
|
||||
$package = id(new PhabricatorOwnersPackage())->load($this->id);
|
||||
if (!$package) {
|
||||
return new Aphront404Response();
|
||||
}
|
||||
} else {
|
||||
$package = new PhabricatorOwnersPackage();
|
||||
$package->setPrimaryOwnerPHID($user->getPHID());
|
||||
}
|
||||
|
||||
$e_name = true;
|
||||
$e_primary = true;
|
||||
$e_owners = true;
|
||||
|
||||
$errors = array();
|
||||
|
||||
if ($request->isFormPost()) {
|
||||
$package->setName($request->getStr('name'));
|
||||
$package->setDescription($request->getStr('description'));
|
||||
|
||||
$primary = $request->getArr('primary');
|
||||
$primary = reset($primary);
|
||||
$package->setPrimaryOwnerPHID($primary);
|
||||
|
||||
$owners = $request->getArr('owners');
|
||||
if ($primary) {
|
||||
array_unshift($owners, $primary);
|
||||
}
|
||||
$owners = array_unique($owners);
|
||||
|
||||
$paths = $request->getArr('path');
|
||||
$repos = $request->getArr('repo');
|
||||
|
||||
$path_refs = array();
|
||||
for ($ii = 0; $ii < count($paths); $ii++) {
|
||||
if (empty($paths[$ii]) || empty($repos[$ii])) {
|
||||
continue;
|
||||
}
|
||||
$path_refs[] = array(
|
||||
'repositoryPHID' => $repos[$ii],
|
||||
'path' => $paths[$ii],
|
||||
);
|
||||
}
|
||||
|
||||
if (!strlen($package->getName())) {
|
||||
$e_name = 'Required';
|
||||
$errors[] = 'Package name is required.';
|
||||
} else {
|
||||
$e_name = null;
|
||||
}
|
||||
|
||||
if (!$package->getPrimaryOwnerPHID()) {
|
||||
$e_primary = 'Required';
|
||||
$errors[] = 'Package must have a primary owner.';
|
||||
} else {
|
||||
$e_primary = null;
|
||||
}
|
||||
|
||||
if (!$owners) {
|
||||
$e_owners = 'Required';
|
||||
$errors[] = 'Package must have at least one owner.';
|
||||
} else {
|
||||
$e_owners = null;
|
||||
}
|
||||
|
||||
if (!$path_refs) {
|
||||
$errors[] = 'Package must include at least one path.';
|
||||
}
|
||||
|
||||
if (!$errors) {
|
||||
$package->attachUnsavedOwners($owners);
|
||||
$package->attachUnsavedPaths($path_refs);
|
||||
try {
|
||||
$package->save();
|
||||
return id(new AphrontRedirectResponse())
|
||||
->setURI('/owners/package/'.$package->getID().'/');
|
||||
} catch (AphrontQueryDuplicateKeyException $ex) {
|
||||
$e_name = 'Duplicate';
|
||||
$errors[] = 'Package name must be unique.';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$owners = $package->loadOwners();
|
||||
$owners = mpull($owners, 'getUserPHID');
|
||||
|
||||
$paths = $package->loadPaths();
|
||||
$path_refs = array();
|
||||
foreach ($paths as $path) {
|
||||
$path_refs[] = array(
|
||||
'repositoryPHID' => $path->getRepositoryPHID(),
|
||||
'path' => $path->getPath(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$error_view = null;
|
||||
if ($errors) {
|
||||
$error_view = new AphrontErrorView();
|
||||
$error_view->setTitle('Package Errors');
|
||||
$error_view->setErrors($errors);
|
||||
}
|
||||
|
||||
$handles = id(new PhabricatorObjectHandleData($owners))
|
||||
->loadHandles();
|
||||
|
||||
$primary = $package->getPrimaryOwnerPHID();
|
||||
if ($primary && isset($handles[$primary])) {
|
||||
$token_primary_owner = array(
|
||||
$primary => $handles[$primary]->getFullName(),
|
||||
);
|
||||
} else {
|
||||
$token_primary_owner = array();
|
||||
}
|
||||
|
||||
$token_all_owners = array_select_keys($handles, $owners);
|
||||
$token_all_owners = mpull($token_all_owners, 'getFullName');
|
||||
|
||||
$title = $package->getID() ? 'Edit Package' : 'New Package';
|
||||
|
||||
$repos = id(new PhabricatorRepository())->loadAll();
|
||||
|
||||
$default_paths = array();
|
||||
foreach ($repos as $repo) {
|
||||
$default_path = $repo->getDetail('default-owners-path');
|
||||
if ($default_path) {
|
||||
$default_paths[$repo->getPHID()] = $default_path;
|
||||
}
|
||||
}
|
||||
|
||||
$repos = mpull($repos, 'getCallsign', 'getPHID');
|
||||
|
||||
$template = new AphrontTypeaheadTemplateView();
|
||||
$template = $template->render();
|
||||
|
||||
Javelin::initBehavior(
|
||||
'owners-path-editor',
|
||||
array(
|
||||
'root' => 'path-editor',
|
||||
'table' => 'paths',
|
||||
'add_button' => 'addpath',
|
||||
'repositories' => $repos,
|
||||
'input_template' => $template,
|
||||
'pathRefs' => $path_refs,
|
||||
|
||||
'completeURI' => '/diffusion/services/path/complete/',
|
||||
'validateURI' => '/diffusion/services/path/validate/',
|
||||
|
||||
'repositoryDefaultPaths' => $default_paths,
|
||||
));
|
||||
|
||||
require_celerity_resource('owners-path-editor-css');
|
||||
|
||||
$cancel_uri = $package->getID()
|
||||
? '/owners/package/'.$package->getID().'/'
|
||||
: '/owners/';
|
||||
|
||||
$form = id(new AphrontFormView())
|
||||
->setUser($user)
|
||||
->appendChild(
|
||||
id(new AphrontFormTextControl())
|
||||
->setLabel('Name')
|
||||
->setName('name')
|
||||
->setValue($package->getName())
|
||||
->setError($e_name))
|
||||
->appendChild(
|
||||
id(new AphrontFormTokenizerControl())
|
||||
->setDatasource('/typeahead/common/users/')
|
||||
->setLabel('Primary Owner')
|
||||
->setName('primary')
|
||||
->setLimit(1)
|
||||
->setValue($token_primary_owner)
|
||||
->setError($e_primary))
|
||||
->appendChild(
|
||||
id(new AphrontFormTokenizerControl())
|
||||
->setDatasource('/typeahead/common/users/')
|
||||
->setLabel('Owners')
|
||||
->setName('owners')
|
||||
->setValue($token_all_owners)
|
||||
->setError($e_owners))
|
||||
->appendChild(
|
||||
'<h1>Paths</h1>'.
|
||||
'<div class="aphront-form-inset" id="path-editor">'.
|
||||
'<div style="float: right;">'.
|
||||
javelin_render_tag(
|
||||
'a',
|
||||
array(
|
||||
'href' => '#',
|
||||
'class' => 'button green',
|
||||
'sigil' => 'addpath',
|
||||
'mustcapture' => true,
|
||||
),
|
||||
'Add New Path').
|
||||
'</div>'.
|
||||
'<p>Specify the files and directories which comprise this '.
|
||||
'package.</p>'.
|
||||
'<div style="clear: both;"></div>'.
|
||||
javelin_render_tag(
|
||||
'table',
|
||||
array(
|
||||
'class' => 'owners-path-editor-table',
|
||||
'sigil' => 'paths',
|
||||
),
|
||||
'').
|
||||
'</div>')
|
||||
->appendChild(
|
||||
id(new AphrontFormTextAreaControl())
|
||||
->setLabel('Description')
|
||||
->setName('description')
|
||||
->setValue($package->getDescription()))
|
||||
->appendChild(
|
||||
id(new AphrontFormSubmitControl())
|
||||
->addCancelButton($cancel_uri)
|
||||
->setValue('Save Package'));
|
||||
|
||||
$panel = new AphrontPanelView();
|
||||
$panel->setHeader($title);
|
||||
$panel->setWidth(AphrontPanelView::WIDTH_WIDE);
|
||||
$panel->appendChild($error_view);
|
||||
$panel->appendChild($form);
|
||||
|
||||
return $this->buildStandardPageResponse(
|
||||
$panel,
|
||||
array(
|
||||
'title' => $title,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
26
src/applications/owners/controller/edit/__init__.php
Normal file
26
src/applications/owners/controller/edit/__init__.php
Normal file
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
/**
|
||||
* This file is automatically generated. Lint this module to rebuild it.
|
||||
* @generated
|
||||
*/
|
||||
|
||||
|
||||
|
||||
phutil_require_module('phabricator', 'aphront/response/404');
|
||||
phutil_require_module('phabricator', 'aphront/response/redirect');
|
||||
phutil_require_module('phabricator', 'applications/owners/controller/base');
|
||||
phutil_require_module('phabricator', 'applications/owners/storage/package');
|
||||
phutil_require_module('phabricator', 'applications/phid/handle/data');
|
||||
phutil_require_module('phabricator', 'applications/repository/storage/repository');
|
||||
phutil_require_module('phabricator', 'infrastructure/celerity/api');
|
||||
phutil_require_module('phabricator', 'infrastructure/javelin/api');
|
||||
phutil_require_module('phabricator', 'view/control/typeahead');
|
||||
phutil_require_module('phabricator', 'view/form/base');
|
||||
phutil_require_module('phabricator', 'view/form/control/submit');
|
||||
phutil_require_module('phabricator', 'view/form/error');
|
||||
phutil_require_module('phabricator', 'view/layout/panel');
|
||||
|
||||
phutil_require_module('phutil', 'utils');
|
||||
|
||||
|
||||
phutil_require_source('PhabricatorOwnersEditController.php');
|
|
@ -58,18 +58,42 @@ class PhabricatorOwnersListController extends PhabricatorOwnersController {
|
|||
phutil_escape_html($name)));
|
||||
}
|
||||
|
||||
$package = new PhabricatorOwnersPackage();
|
||||
|
||||
switch ($this->view) {
|
||||
case 'search':
|
||||
$content = $this->renderPackageTable(array(), 'Search Results');
|
||||
$packages = array();
|
||||
|
||||
$header = 'Search Results';
|
||||
$nodata = 'No packages match your query.';
|
||||
break;
|
||||
case 'owned':
|
||||
$content = $this->renderOwnedView();
|
||||
$owner = new PhabricatorOwnersOwner();
|
||||
$data = queryfx_all(
|
||||
$package->establishConnection('r'),
|
||||
'SELECT p.* FROM %T p JOIN %T o ON p.id = o.packageID
|
||||
WHERE o.userPHID = %s GROUP BY p.id',
|
||||
$package->getTableName(),
|
||||
$owner->getTableName(),
|
||||
$user->getPHID());
|
||||
$packages = $package->loadAllFromArray($data);
|
||||
|
||||
$header = 'Owned Packages';
|
||||
$nodata = 'No owned packages';
|
||||
break;
|
||||
case 'all':
|
||||
$content = $this->renderAllView();
|
||||
$packages = $package->loadAll();
|
||||
|
||||
$header = 'All Packages';
|
||||
$nodata = 'There are no defined packages.';
|
||||
break;
|
||||
}
|
||||
|
||||
$content = $this->renderPackageTable(
|
||||
$packages,
|
||||
$header,
|
||||
$nodata);
|
||||
|
||||
$filter = new AphrontListFilterView();
|
||||
$filter->addButton(
|
||||
phutil_render_tag(
|
||||
|
@ -127,26 +151,68 @@ class PhabricatorOwnersListController extends PhabricatorOwnersController {
|
|||
));
|
||||
}
|
||||
|
||||
private function renderOwnedView() {
|
||||
$packages = array();
|
||||
private function renderPackageTable(array $packages, $header, $nodata) {
|
||||
|
||||
return $this->renderPackageTable($packages, 'Owned Packages');
|
||||
}
|
||||
if ($packages) {
|
||||
$package_ids = mpull($packages, 'getID');
|
||||
|
||||
private function renderAllView() {
|
||||
$packages = array();
|
||||
$owners = id(new PhabricatorOwnersOwner())->loadAllWhere(
|
||||
'packageID IN (%Ld)',
|
||||
$package_ids);
|
||||
|
||||
return $this->renderPackageTable($packages, 'All Packages');
|
||||
}
|
||||
$paths = id(new PhabricatorOwnersPath())->loadAllWhere(
|
||||
'packageID in (%Ld)',
|
||||
$package_ids);
|
||||
|
||||
private function renderPackageTable(array $packages, $header) {
|
||||
$phids = array();
|
||||
foreach ($owners as $owner) {
|
||||
$phids[$owner->getUserPHID()] = true;
|
||||
}
|
||||
foreach ($paths as $path) {
|
||||
$phids[$path->getRepositoryPHID()] = true;
|
||||
}
|
||||
$phids = array_keys($phids);
|
||||
|
||||
$handles = id(new PhabricatorObjectHandleData($phids))->loadHandles();
|
||||
|
||||
$owners = mgroup($owners, 'getPackageID');
|
||||
$paths = mgroup($paths, 'getPackageID');
|
||||
} else {
|
||||
$handles = array();
|
||||
$owners = array();
|
||||
$paths = array();
|
||||
}
|
||||
|
||||
$rows = array();
|
||||
foreach ($packages as $package) {
|
||||
|
||||
$pkg_owners = idx($owners, $package->getID(), array());
|
||||
foreach ($pkg_owners as $key => $owner) {
|
||||
$pkg_owners[$key] = $handles[$owner->getUserPHID()]->renderLink();
|
||||
if ($owner->getUserPHID() == $package->getPrimaryOwnerPHID()) {
|
||||
$pkg_owners[$key] = '<strong>'.$pkg_owners[$key].'</strong>';
|
||||
}
|
||||
}
|
||||
$pkg_owners = implode('<br />', $pkg_owners);
|
||||
|
||||
$pkg_paths = idx($paths, $package->getID(), array());
|
||||
foreach ($pkg_paths as $key => $path) {
|
||||
$repo = $handles[$path->getRepositoryPHID()]->getName();
|
||||
$pkg_paths[$key] =
|
||||
'<strong>'.$repo.'</strong> '.
|
||||
phutil_escape_html($path->getPath());
|
||||
}
|
||||
$pkg_paths = implode('<br />', $pkg_paths);
|
||||
|
||||
$rows[] = array(
|
||||
'x',
|
||||
'y',
|
||||
'z',
|
||||
phutil_render_tag(
|
||||
'a',
|
||||
array(
|
||||
'href' => '/owners/package/'.$package->getID().'/',
|
||||
),
|
||||
phutil_escape_html($package->getName())),
|
||||
$pkg_owners,
|
||||
$pkg_paths,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -159,7 +225,7 @@ class PhabricatorOwnersListController extends PhabricatorOwnersController {
|
|||
));
|
||||
$table->setColumnClasses(
|
||||
array(
|
||||
'',
|
||||
'pri',
|
||||
'',
|
||||
'wide wrap',
|
||||
));
|
||||
|
|
|
@ -7,7 +7,11 @@
|
|||
|
||||
|
||||
phutil_require_module('phabricator', 'applications/owners/controller/base');
|
||||
phutil_require_module('phabricator', 'applications/owners/storage/owner');
|
||||
phutil_require_module('phabricator', 'applications/owners/storage/package');
|
||||
phutil_require_module('phabricator', 'applications/owners/storage/path');
|
||||
phutil_require_module('phabricator', 'applications/phid/handle/data');
|
||||
phutil_require_module('phabricator', 'storage/queryfx');
|
||||
phutil_require_module('phabricator', 'view/control/table');
|
||||
phutil_require_module('phabricator', 'view/form/base');
|
||||
phutil_require_module('phabricator', 'view/form/control/submit');
|
||||
|
|
|
@ -24,7 +24,7 @@ class PhabricatorOwnersOwner extends PhabricatorOwnersDAO {
|
|||
public function getConfiguration() {
|
||||
return array(
|
||||
self::CONFIG_TIMESTAMPS => false,
|
||||
);
|
||||
) + parent::getConfiguration();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -23,16 +23,110 @@ class PhabricatorOwnersPackage extends PhabricatorOwnersDAO {
|
|||
protected $description;
|
||||
protected $primaryOwnerPHID;
|
||||
|
||||
private $unsavedOwners;
|
||||
private $unsavedPaths;
|
||||
|
||||
public function getConfiguration() {
|
||||
return array(
|
||||
// This information is better available from the history table.
|
||||
self::CONFIG_TIMESTAMPS => false,
|
||||
self::CONFIG_AUX_PHID => true,
|
||||
);
|
||||
) + parent::getConfiguration();
|
||||
}
|
||||
|
||||
public function generatePHID() {
|
||||
return PhabricatorPHID::generateNew('OPKG');
|
||||
return PhabricatorPHID::generateNewPHID('OPKG');
|
||||
}
|
||||
|
||||
public function attachUnsavedOwners(array $owners) {
|
||||
$this->unsavedOwners = $owners;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function attachUnsavedPaths(array $paths) {
|
||||
$this->unsavedPaths = $paths;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function loadOwners() {
|
||||
if (!$this->getID()) {
|
||||
return array();
|
||||
}
|
||||
return id(new PhabricatorOwnersOwner())->loadAllWhere(
|
||||
'packageID = %d',
|
||||
$this->getID());
|
||||
}
|
||||
|
||||
public function loadPaths() {
|
||||
if (!$this->getID()) {
|
||||
return array();
|
||||
}
|
||||
return id(new PhabricatorOwnersPath())->loadAllWhere(
|
||||
'packageID = %d',
|
||||
$this->getID());
|
||||
}
|
||||
|
||||
public function save() {
|
||||
|
||||
// TODO: Transactions!
|
||||
|
||||
$ret = parent::save();
|
||||
|
||||
if ($this->unsavedOwners) {
|
||||
$new_owners = array_fill_keys($this->unsavedOwners, true);
|
||||
$cur_owners = array();
|
||||
foreach ($this->loadOwners() as $owner) {
|
||||
if (empty($new_owners[$owner->getUserPHID()])) {
|
||||
$owner->delete();
|
||||
continue;
|
||||
}
|
||||
$cur_owners[$owner->getUserPHID()] = true;
|
||||
}
|
||||
$add_owners = array_diff_key($new_owners, $cur_owners);
|
||||
foreach ($add_owners as $phid => $ignored) {
|
||||
$owner = new PhabricatorOwnersOwner();
|
||||
$owner->setPackageID($this->getID());
|
||||
$owner->setUserPHID($phid);
|
||||
$owner->save();
|
||||
}
|
||||
unset($this->unsavedOwners);
|
||||
}
|
||||
|
||||
if ($this->unsavedPaths) {
|
||||
$new_paths = igroup($this->unsavedPaths, 'repositoryPHID', 'path');
|
||||
$cur_paths = $this->loadPaths();
|
||||
foreach ($cur_paths as $key => $path) {
|
||||
if (empty($new_paths[$path->getRepositoryPHID()][$path->getPath()])) {
|
||||
$path->delete();
|
||||
unset($cur_paths[$key]);
|
||||
}
|
||||
}
|
||||
$cur_paths = mgroup($cur_paths, 'getRepositoryPHID', 'getPath');
|
||||
foreach ($new_paths as $repository_phid => $paths) {
|
||||
foreach ($paths as $path => $ignored) {
|
||||
if (empty($cur_paths[$repository_phid][$path])) {
|
||||
$obj = new PhabricatorOwnersPath();
|
||||
$obj->setPackageID($this->getID());
|
||||
$obj->setRepositoryPHID($repository_phid);
|
||||
$obj->setPath($path);
|
||||
$obj->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
unset($this->unsavedPaths);
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
public function delete() {
|
||||
foreach ($this->loadOwners() as $owner) {
|
||||
$owner->delete();
|
||||
}
|
||||
foreach ($this->loadPaths() as $path) {
|
||||
$path->delete();
|
||||
}
|
||||
return parent::delete();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -7,7 +7,11 @@
|
|||
|
||||
|
||||
phutil_require_module('phabricator', 'applications/owners/storage/base');
|
||||
phutil_require_module('phabricator', 'applications/owners/storage/owner');
|
||||
phutil_require_module('phabricator', 'applications/owners/storage/path');
|
||||
phutil_require_module('phabricator', 'applications/phid/storage/phid');
|
||||
|
||||
phutil_require_module('phutil', 'utils');
|
||||
|
||||
|
||||
phutil_require_source('PhabricatorOwnersPackage.php');
|
||||
|
|
|
@ -25,7 +25,7 @@ class PhabricatorOwnersPath extends PhabricatorOwnersDAO {
|
|||
public function getConfiguration() {
|
||||
return array(
|
||||
self::CONFIG_TIMESTAMPS => false,
|
||||
);
|
||||
) + parent::getConfiguration();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -201,6 +201,28 @@ class PhabricatorObjectHandleData {
|
|||
$handles[$phid] = $handle;
|
||||
}
|
||||
break;
|
||||
case PhabricatorPHIDConstants::PHID_TYPE_REPO:
|
||||
$class = 'PhabricatorRepository';
|
||||
PhutilSymbolLoader::loadClass($class);
|
||||
$object = newv($class, array());
|
||||
|
||||
$repositories = $object->loadAllWhere('phid in (%Ls)', $phids);
|
||||
$repositories = mpull($repositories, null, 'getPHID');
|
||||
|
||||
foreach ($phids as $phid) {
|
||||
$handle = new PhabricatorObjectHandle();
|
||||
$handle->setPHID($phid);
|
||||
$handle->setType($type);
|
||||
if (empty($repositories[$phid])) {
|
||||
$handle->setName('Unknown Repository');
|
||||
} else {
|
||||
$repository = $repositories[$phid];
|
||||
$handle->setName($repository->getCallsign());
|
||||
$handle->setURI('/diffusion/'.$repository->getCallsign().'/');
|
||||
}
|
||||
$handles[$phid] = $handle;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
$loader = null;
|
||||
if (isset($external_loaders[$type])) {
|
||||
|
|
|
@ -22,20 +22,25 @@ final class AphrontPanelView extends AphrontView {
|
|||
const WIDTH_FORM = 'form';
|
||||
const WIDTH_WIDE = 'wide';
|
||||
|
||||
private $createButton;
|
||||
private $buttons = array();
|
||||
private $header;
|
||||
private $width;
|
||||
|
||||
public function setCreateButton($create_button, $href) {
|
||||
$this->addButton(
|
||||
phutil_render_tag(
|
||||
'a',
|
||||
array(
|
||||
'href' => $href,
|
||||
'class' => 'button green',
|
||||
),
|
||||
$create_button));
|
||||
|
||||
$this->createButton = phutil_render_tag(
|
||||
'a',
|
||||
array(
|
||||
'href' => $href,
|
||||
'class' => 'create-button button green',
|
||||
),
|
||||
$create_button);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addButton($button) {
|
||||
$this->buttons[] = $button;
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
@ -56,10 +61,12 @@ final class AphrontPanelView extends AphrontView {
|
|||
$header = null;
|
||||
}
|
||||
|
||||
if ($this->createButton !== null) {
|
||||
$button = $this->createButton;
|
||||
} else {
|
||||
$button = null;
|
||||
$buttons = null;
|
||||
if ($this->buttons) {
|
||||
$buttons =
|
||||
'<div class="aphront-panel-view-buttons">'.
|
||||
implode(" ", $this->buttons).
|
||||
'</div>';
|
||||
}
|
||||
|
||||
$table = $this->renderChildren();
|
||||
|
@ -73,7 +80,7 @@ final class AphrontPanelView extends AphrontView {
|
|||
|
||||
return
|
||||
'<div class="'.implode(' ', $class).'">'.
|
||||
$button.
|
||||
$buttons.
|
||||
$header.
|
||||
$table.
|
||||
'</div>';
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
background: #f6f6f6;
|
||||
border-bottom: 1px solid #bbbbbb;
|
||||
width: 100%;
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
|
||||
.aphront-list-filter-view-buttons {
|
||||
|
@ -24,7 +25,7 @@
|
|||
|
||||
.aphront-list-filter-view-controls .aphront-form-view {
|
||||
border-width: 0;
|
||||
padding: 0 0 6px;
|
||||
padding: 12px 0 6px;
|
||||
}
|
||||
|
||||
.aphront-list-filter-view-controls .aphront-form-view .aphront-form-label {
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
padding: 2px 0 8px;
|
||||
}
|
||||
|
||||
.aphront-panel-view a.create-button {
|
||||
.aphront-panel-view-buttons {
|
||||
float: right;
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue