mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-22 14:52:41 +01:00
Allow dashboard panels to be archived
Summary: Ref T5471. Adds an archived state for panels. Archived panels don't show up in the default query view or in the "Add Existing Panel" workflow. Test Plan: - Archived a panel. - Activated a panel. - Viewed / searched for archived/active panels. - Popped "Add Existing Panel" dropdown and saw it omit archived panels. Reviewers: chad Reviewed By: chad Subscribers: epriestley Maniphest Tasks: T5471 Differential Revision: https://secure.phabricator.com/D9779
This commit is contained in:
parent
ae4a687da3
commit
c9366acbec
13 changed files with 192 additions and 3 deletions
2
resources/sql/autopatches/20140629.dasharchive.1.sql
Normal file
2
resources/sql/autopatches/20140629.dasharchive.1.sql
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
ALTER TABLE {$NAMESPACE}_dashboard.dashboard_panel
|
||||||
|
ADD isArchived BOOL NOT NULL DEFAULT 0 AFTER editPolicy;
|
|
@ -1499,6 +1499,7 @@ phutil_register_library_map(array(
|
||||||
'PhabricatorDashboardPHIDTypeDashboard' => 'applications/dashboard/phid/PhabricatorDashboardPHIDTypeDashboard.php',
|
'PhabricatorDashboardPHIDTypeDashboard' => 'applications/dashboard/phid/PhabricatorDashboardPHIDTypeDashboard.php',
|
||||||
'PhabricatorDashboardPHIDTypePanel' => 'applications/dashboard/phid/PhabricatorDashboardPHIDTypePanel.php',
|
'PhabricatorDashboardPHIDTypePanel' => 'applications/dashboard/phid/PhabricatorDashboardPHIDTypePanel.php',
|
||||||
'PhabricatorDashboardPanel' => 'applications/dashboard/storage/PhabricatorDashboardPanel.php',
|
'PhabricatorDashboardPanel' => 'applications/dashboard/storage/PhabricatorDashboardPanel.php',
|
||||||
|
'PhabricatorDashboardPanelArchiveController' => 'applications/dashboard/controller/PhabricatorDashboardPanelArchiveController.php',
|
||||||
'PhabricatorDashboardPanelCoreCustomField' => 'applications/dashboard/customfield/PhabricatorDashboardPanelCoreCustomField.php',
|
'PhabricatorDashboardPanelCoreCustomField' => 'applications/dashboard/customfield/PhabricatorDashboardPanelCoreCustomField.php',
|
||||||
'PhabricatorDashboardPanelCustomField' => 'applications/dashboard/customfield/PhabricatorDashboardPanelCustomField.php',
|
'PhabricatorDashboardPanelCustomField' => 'applications/dashboard/customfield/PhabricatorDashboardPanelCustomField.php',
|
||||||
'PhabricatorDashboardPanelEditController' => 'applications/dashboard/controller/PhabricatorDashboardPanelEditController.php',
|
'PhabricatorDashboardPanelEditController' => 'applications/dashboard/controller/PhabricatorDashboardPanelEditController.php',
|
||||||
|
@ -4334,6 +4335,7 @@ phutil_register_library_map(array(
|
||||||
1 => 'PhabricatorPolicyInterface',
|
1 => 'PhabricatorPolicyInterface',
|
||||||
2 => 'PhabricatorCustomFieldInterface',
|
2 => 'PhabricatorCustomFieldInterface',
|
||||||
),
|
),
|
||||||
|
'PhabricatorDashboardPanelArchiveController' => 'PhabricatorDashboardController',
|
||||||
'PhabricatorDashboardPanelCoreCustomField' =>
|
'PhabricatorDashboardPanelCoreCustomField' =>
|
||||||
array(
|
array(
|
||||||
0 => 'PhabricatorDashboardPanelCustomField',
|
0 => 'PhabricatorDashboardPanelCustomField',
|
||||||
|
|
|
@ -42,6 +42,8 @@ final class PhabricatorApplicationDashboard extends PhabricatorApplication {
|
||||||
'create/' => 'PhabricatorDashboardPanelEditController',
|
'create/' => 'PhabricatorDashboardPanelEditController',
|
||||||
'edit/(?:(?P<id>\d+)/)?' => 'PhabricatorDashboardPanelEditController',
|
'edit/(?:(?P<id>\d+)/)?' => 'PhabricatorDashboardPanelEditController',
|
||||||
'render/(?P<id>\d+)/' => 'PhabricatorDashboardPanelRenderController',
|
'render/(?P<id>\d+)/' => 'PhabricatorDashboardPanelRenderController',
|
||||||
|
'archive/(?P<id>\d+)/' =>
|
||||||
|
'PhabricatorDashboardPanelArchiveController',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
@ -60,6 +60,7 @@ final class PhabricatorDashboardAddPanelController
|
||||||
|
|
||||||
$panels = id(new PhabricatorDashboardPanelQuery())
|
$panels = id(new PhabricatorDashboardPanelQuery())
|
||||||
->setViewer($viewer)
|
->setViewer($viewer)
|
||||||
|
->withArchived(false)
|
||||||
->execute();
|
->execute();
|
||||||
|
|
||||||
if (!$panels) {
|
if (!$panels) {
|
||||||
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class PhabricatorDashboardPanelArchiveController
|
||||||
|
extends PhabricatorDashboardController {
|
||||||
|
|
||||||
|
private $id;
|
||||||
|
|
||||||
|
public function willProcessRequest(array $data) {
|
||||||
|
$this->id = $data['id'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function processRequest() {
|
||||||
|
$request = $this->getRequest();
|
||||||
|
$viewer = $request->getUser();
|
||||||
|
|
||||||
|
$panel = id(new PhabricatorDashboardPanelQuery())
|
||||||
|
->setViewer($viewer)
|
||||||
|
->withIDs(array($this->id))
|
||||||
|
->requireCapabilities(
|
||||||
|
array(
|
||||||
|
PhabricatorPolicyCapability::CAN_VIEW,
|
||||||
|
PhabricatorPolicyCapability::CAN_EDIT,
|
||||||
|
))
|
||||||
|
->executeOne();
|
||||||
|
if (!$panel) {
|
||||||
|
return new Aphront404Response();
|
||||||
|
}
|
||||||
|
|
||||||
|
$next_uri = '/'.$panel->getMonogram();
|
||||||
|
|
||||||
|
if ($request->isFormPost()) {
|
||||||
|
$xactions = array();
|
||||||
|
$xactions[] = id(new PhabricatorDashboardPanelTransaction())
|
||||||
|
->setTransactionType(PhabricatorDashboardPanelTransaction::TYPE_ARCHIVE)
|
||||||
|
->setNewValue((int)!$panel->getIsArchived());
|
||||||
|
|
||||||
|
id(new PhabricatorDashboardPanelTransactionEditor())
|
||||||
|
->setActor($viewer)
|
||||||
|
->setContentSourceFromRequest($request)
|
||||||
|
->applyTransactions($panel, $xactions);
|
||||||
|
|
||||||
|
return id(new AphrontRedirectResponse())->setURI($next_uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($panel->getIsArchived()) {
|
||||||
|
$title = pht('Activate Panel?');
|
||||||
|
$body = pht(
|
||||||
|
'This panel will be reactivated and appear in other interfaces as '.
|
||||||
|
'an active panel.');
|
||||||
|
$submit_text = pht('Activate Panel');
|
||||||
|
} else {
|
||||||
|
$title = pht('Archive Panel?');
|
||||||
|
$body = pht(
|
||||||
|
'This panel will be archived and no longer appear in lists of active '.
|
||||||
|
'panels.');
|
||||||
|
$submit_text = pht('Archive Panel');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->newDialog()
|
||||||
|
->setTitle($title)
|
||||||
|
->appendParagraph($body)
|
||||||
|
->addSubmitButton($submit_text)
|
||||||
|
->addCancelButton($next_uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -96,6 +96,22 @@ final class PhabricatorDashboardPanelViewController
|
||||||
->setDisabled(!$can_edit)
|
->setDisabled(!$can_edit)
|
||||||
->setWorkflow(!$can_edit));
|
->setWorkflow(!$can_edit));
|
||||||
|
|
||||||
|
if (!$panel->getIsArchived()) {
|
||||||
|
$archive_text = pht('Archive Panel');
|
||||||
|
$archive_icon = 'fa-times';
|
||||||
|
} else {
|
||||||
|
$archive_text = pht('Activate Panel');
|
||||||
|
$archive_icon = 'fa-plus';
|
||||||
|
}
|
||||||
|
|
||||||
|
$actions->addAction(
|
||||||
|
id(new PhabricatorActionView())
|
||||||
|
->setName($archive_text)
|
||||||
|
->setIcon($archive_icon)
|
||||||
|
->setHref($this->getApplicationURI("panel/archive/{$id}/"))
|
||||||
|
->setDisabled(!$can_edit)
|
||||||
|
->setWorkflow(true));
|
||||||
|
|
||||||
$actions->addAction(
|
$actions->addAction(
|
||||||
id(new PhabricatorActionView())
|
id(new PhabricatorActionView())
|
||||||
->setName(pht('View Standalone'))
|
->setName(pht('View Standalone'))
|
||||||
|
|
|
@ -30,6 +30,10 @@ final class PhabricatorDashboardPanelTabsCustomField
|
||||||
}
|
}
|
||||||
|
|
||||||
public function renderEditControl(array $handles) {
|
public function renderEditControl(array $handles) {
|
||||||
|
// NOTE: This includes archived panels so we don't mutate the tabs
|
||||||
|
// when saving a tab panel that includes archied panels. This whole UI is
|
||||||
|
// hopefully temporary anyway.
|
||||||
|
|
||||||
$panels = id(new PhabricatorDashboardPanelQuery())
|
$panels = id(new PhabricatorDashboardPanelQuery())
|
||||||
->setViewer($this->getViewer())
|
->setViewer($this->getViewer())
|
||||||
->execute();
|
->execute();
|
||||||
|
|
|
@ -11,6 +11,7 @@ final class PhabricatorDashboardPanelTransactionEditor
|
||||||
$types[] = PhabricatorTransactions::TYPE_EDGE;
|
$types[] = PhabricatorTransactions::TYPE_EDGE;
|
||||||
|
|
||||||
$types[] = PhabricatorDashboardPanelTransaction::TYPE_NAME;
|
$types[] = PhabricatorDashboardPanelTransaction::TYPE_NAME;
|
||||||
|
$types[] = PhabricatorDashboardPanelTransaction::TYPE_ARCHIVE;
|
||||||
|
|
||||||
return $types;
|
return $types;
|
||||||
}
|
}
|
||||||
|
@ -24,6 +25,8 @@ final class PhabricatorDashboardPanelTransactionEditor
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return $object->getName();
|
return $object->getName();
|
||||||
|
case PhabricatorDashboardPanelTransaction::TYPE_ARCHIVE:
|
||||||
|
return (int)$object->getIsArchived();
|
||||||
}
|
}
|
||||||
|
|
||||||
return parent::getCustomTransactionOldValue($object, $xaction);
|
return parent::getCustomTransactionOldValue($object, $xaction);
|
||||||
|
@ -35,6 +38,8 @@ final class PhabricatorDashboardPanelTransactionEditor
|
||||||
switch ($xaction->getTransactionType()) {
|
switch ($xaction->getTransactionType()) {
|
||||||
case PhabricatorDashboardPanelTransaction::TYPE_NAME:
|
case PhabricatorDashboardPanelTransaction::TYPE_NAME:
|
||||||
return $xaction->getNewValue();
|
return $xaction->getNewValue();
|
||||||
|
case PhabricatorDashboardPanelTransaction::TYPE_ARCHIVE:
|
||||||
|
return (int)$xaction->getNewValue();
|
||||||
}
|
}
|
||||||
return parent::getCustomTransactionNewValue($object, $xaction);
|
return parent::getCustomTransactionNewValue($object, $xaction);
|
||||||
}
|
}
|
||||||
|
@ -46,6 +51,9 @@ final class PhabricatorDashboardPanelTransactionEditor
|
||||||
case PhabricatorDashboardPanelTransaction::TYPE_NAME:
|
case PhabricatorDashboardPanelTransaction::TYPE_NAME:
|
||||||
$object->setName($xaction->getNewValue());
|
$object->setName($xaction->getNewValue());
|
||||||
return;
|
return;
|
||||||
|
case PhabricatorDashboardPanelTransaction::TYPE_ARCHIVE:
|
||||||
|
$object->setIsArchived((int)$xaction->getNewValue());
|
||||||
|
return;
|
||||||
case PhabricatorTransactions::TYPE_VIEW_POLICY:
|
case PhabricatorTransactions::TYPE_VIEW_POLICY:
|
||||||
$object->setViewPolicy($xaction->getNewValue());
|
$object->setViewPolicy($xaction->getNewValue());
|
||||||
return;
|
return;
|
||||||
|
@ -63,6 +71,7 @@ final class PhabricatorDashboardPanelTransactionEditor
|
||||||
|
|
||||||
switch ($xaction->getTransactionType()) {
|
switch ($xaction->getTransactionType()) {
|
||||||
case PhabricatorDashboardPanelTransaction::TYPE_NAME:
|
case PhabricatorDashboardPanelTransaction::TYPE_NAME:
|
||||||
|
case PhabricatorDashboardPanelTransaction::TYPE_ARCHIVE:
|
||||||
case PhabricatorTransactions::TYPE_VIEW_POLICY:
|
case PhabricatorTransactions::TYPE_VIEW_POLICY:
|
||||||
case PhabricatorTransactions::TYPE_EDIT_POLICY:
|
case PhabricatorTransactions::TYPE_EDIT_POLICY:
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -38,6 +38,10 @@ final class PhabricatorDashboardPHIDTypePanel extends PhabricatorPHIDType {
|
||||||
$handle->setName($panel->getMonogram());
|
$handle->setName($panel->getMonogram());
|
||||||
$handle->setFullName("{$monogram} {$name}");
|
$handle->setFullName("{$monogram} {$name}");
|
||||||
$handle->setURI("/{$monogram}");
|
$handle->setURI("/{$monogram}");
|
||||||
|
|
||||||
|
if ($panel->getIsArchived()) {
|
||||||
|
$handle->setStatus(PhabricatorObjectHandleStatus::STATUS_CLOSED);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ final class PhabricatorDashboardPanelQuery
|
||||||
|
|
||||||
private $ids;
|
private $ids;
|
||||||
private $phids;
|
private $phids;
|
||||||
|
private $archived;
|
||||||
|
|
||||||
public function withIDs(array $ids) {
|
public function withIDs(array $ids) {
|
||||||
$this->ids = $ids;
|
$this->ids = $ids;
|
||||||
|
@ -16,6 +17,11 @@ final class PhabricatorDashboardPanelQuery
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function withArchived($archived) {
|
||||||
|
$this->archived = $archived;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
protected function loadPage() {
|
protected function loadPage() {
|
||||||
$table = new PhabricatorDashboardPanel();
|
$table = new PhabricatorDashboardPanel();
|
||||||
$conn_r = $table->establishConnection('r');
|
$conn_r = $table->establishConnection('r');
|
||||||
|
@ -34,20 +40,27 @@ final class PhabricatorDashboardPanelQuery
|
||||||
protected function buildWhereClause($conn_r) {
|
protected function buildWhereClause($conn_r) {
|
||||||
$where = array();
|
$where = array();
|
||||||
|
|
||||||
if ($this->ids) {
|
if ($this->ids !== null) {
|
||||||
$where[] = qsprintf(
|
$where[] = qsprintf(
|
||||||
$conn_r,
|
$conn_r,
|
||||||
'id IN (%Ld)',
|
'id IN (%Ld)',
|
||||||
$this->ids);
|
$this->ids);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->phids) {
|
if ($this->phids !== null) {
|
||||||
$where[] = qsprintf(
|
$where[] = qsprintf(
|
||||||
$conn_r,
|
$conn_r,
|
||||||
'phid IN (%Ls)',
|
'phid IN (%Ls)',
|
||||||
$this->phids);
|
$this->phids);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($this->archived !== null) {
|
||||||
|
$where[] = qsprintf(
|
||||||
|
$conn_r,
|
||||||
|
'isArchived = %d',
|
||||||
|
(int)$this->archived);
|
||||||
|
}
|
||||||
|
|
||||||
$where[] = $this->buildPagingClause($conn_r);
|
$where[] = $this->buildPagingClause($conn_r);
|
||||||
|
|
||||||
return $this->formatWhereClause($where);
|
return $this->formatWhereClause($where);
|
||||||
|
|
|
@ -14,19 +14,47 @@ final class PhabricatorDashboardPanelSearchEngine
|
||||||
public function buildSavedQueryFromRequest(AphrontRequest $request) {
|
public function buildSavedQueryFromRequest(AphrontRequest $request) {
|
||||||
$saved = new PhabricatorSavedQuery();
|
$saved = new PhabricatorSavedQuery();
|
||||||
|
|
||||||
|
$saved->setParameter('status', $request->getStr('status'));
|
||||||
|
|
||||||
return $saved;
|
return $saved;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) {
|
public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) {
|
||||||
$query = id(new PhabricatorDashboardPanelQuery());
|
$query = id(new PhabricatorDashboardPanelQuery());
|
||||||
|
|
||||||
|
$status = $saved->getParameter('status');
|
||||||
|
switch ($status) {
|
||||||
|
case 'active':
|
||||||
|
$query->withArchived(false);
|
||||||
|
break;
|
||||||
|
case 'archived':
|
||||||
|
$query->withArchived(true);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
return $query;
|
return $query;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function buildSearchForm(
|
public function buildSearchForm(
|
||||||
AphrontFormView $form,
|
AphrontFormView $form,
|
||||||
PhabricatorSavedQuery $saved_query) {
|
PhabricatorSavedQuery $saved_query) {
|
||||||
return;
|
|
||||||
|
$status = $saved_query->getParameter('status', '');
|
||||||
|
|
||||||
|
$form
|
||||||
|
->appendChild(
|
||||||
|
id(new AphrontFormSelectControl())
|
||||||
|
->setLabel(pht('Status'))
|
||||||
|
->setName('status')
|
||||||
|
->setValue($status)
|
||||||
|
->setOptions(
|
||||||
|
array(
|
||||||
|
'' => pht('(All Panels)'),
|
||||||
|
'active' => pht('Active Panels'),
|
||||||
|
'archived' => pht('Archived Panels'),
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function getURI($path) {
|
protected function getURI($path) {
|
||||||
|
@ -35,6 +63,7 @@ final class PhabricatorDashboardPanelSearchEngine
|
||||||
|
|
||||||
public function getBuiltinQueryNames() {
|
public function getBuiltinQueryNames() {
|
||||||
$names = array(
|
$names = array(
|
||||||
|
'active' => pht('Active Panels'),
|
||||||
'all' => pht('All Panels'),
|
'all' => pht('All Panels'),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -47,6 +76,8 @@ final class PhabricatorDashboardPanelSearchEngine
|
||||||
$query->setQueryKey($query_key);
|
$query->setQueryKey($query_key);
|
||||||
|
|
||||||
switch ($query_key) {
|
switch ($query_key) {
|
||||||
|
case 'active':
|
||||||
|
return $query->setParameter('status', 'active');
|
||||||
case 'all':
|
case 'all':
|
||||||
return $query;
|
return $query;
|
||||||
}
|
}
|
||||||
|
@ -70,6 +101,21 @@ final class PhabricatorDashboardPanelSearchEngine
|
||||||
->setHref('/'.$panel->getMonogram())
|
->setHref('/'.$panel->getMonogram())
|
||||||
->setObject($panel);
|
->setObject($panel);
|
||||||
|
|
||||||
|
$impl = $panel->getImplementation();
|
||||||
|
if ($impl) {
|
||||||
|
$type_text = $impl->getPanelTypeName();
|
||||||
|
$type_icon = 'none';
|
||||||
|
} else {
|
||||||
|
$type_text = nonempty($panel->getPanelType(), pht('Unknown Type'));
|
||||||
|
$type_icon = 'fa-question';
|
||||||
|
}
|
||||||
|
|
||||||
|
$item->addIcon($type_icon, $type_text);
|
||||||
|
|
||||||
|
if ($panel->getIsArchived()) {
|
||||||
|
$item->setDisabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
$list->addItem($item);
|
$list->addItem($item);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@ final class PhabricatorDashboardPanel
|
||||||
protected $panelType;
|
protected $panelType;
|
||||||
protected $viewPolicy;
|
protected $viewPolicy;
|
||||||
protected $editPolicy;
|
protected $editPolicy;
|
||||||
|
protected $isArchived = 0;
|
||||||
protected $properties = array();
|
protected $properties = array();
|
||||||
|
|
||||||
private $customFields = self::ATTACHABLE;
|
private $customFields = self::ATTACHABLE;
|
||||||
|
|
|
@ -4,6 +4,7 @@ final class PhabricatorDashboardPanelTransaction
|
||||||
extends PhabricatorApplicationTransaction {
|
extends PhabricatorApplicationTransaction {
|
||||||
|
|
||||||
const TYPE_NAME = 'dashpanel:name';
|
const TYPE_NAME = 'dashpanel:name';
|
||||||
|
const TYPE_ARCHIVE = 'dashboard:archive';
|
||||||
|
|
||||||
public function getApplicationName() {
|
public function getApplicationName() {
|
||||||
return 'dashboard';
|
return 'dashboard';
|
||||||
|
@ -36,6 +37,16 @@ final class PhabricatorDashboardPanelTransaction
|
||||||
$old,
|
$old,
|
||||||
$new);
|
$new);
|
||||||
}
|
}
|
||||||
|
case self::TYPE_ARCHIVE:
|
||||||
|
if ($new) {
|
||||||
|
return pht(
|
||||||
|
'%s archived this panel.',
|
||||||
|
$author_link);
|
||||||
|
} else {
|
||||||
|
return pht(
|
||||||
|
'%s activated this panel.',
|
||||||
|
$author_link);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return parent::getTitle();
|
return parent::getTitle();
|
||||||
|
@ -67,6 +78,18 @@ final class PhabricatorDashboardPanelTransaction
|
||||||
$old,
|
$old,
|
||||||
$new);
|
$new);
|
||||||
}
|
}
|
||||||
|
case self::TYPE_ARCHIVE:
|
||||||
|
if ($new) {
|
||||||
|
return pht(
|
||||||
|
'%s archived dashboard panel %s.',
|
||||||
|
$author_link,
|
||||||
|
$object_link);
|
||||||
|
} else {
|
||||||
|
return pht(
|
||||||
|
'%s activated dashboard panel %s.',
|
||||||
|
$author_link,
|
||||||
|
$object_link);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return parent::getTitleForFeed($story);
|
return parent::getTitleForFeed($story);
|
||||||
|
|
Loading…
Reference in a new issue