mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-30 02:32:42 +01:00
Make token UI stronger and more consistent
Summary: Ref T4100. Overall: - Use token background color to communicate token type (blue = object, yellow = function, grey = disabled/closed, red = invalid). - Use token icon color to make color choices consistent (specifically, use project icon colors in project tokens). - For functions, use token icon to communicate function result type (e.g., viewer() has a user icon; members(...) has a group icon), since we don't need the icon to indicate "this is a function" anymore. Test Plan: {F374615} {F374616} {F374617} Reviewers: chad, btrahan Reviewed By: btrahan Subscribers: epriestley Maniphest Tasks: T4100 Differential Revision: https://secure.phabricator.com/D12446
This commit is contained in:
parent
845466b49b
commit
76448a75de
13 changed files with 210 additions and 29 deletions
|
@ -1206,6 +1206,7 @@ phutil_register_library_map(array(
|
||||||
'PHUITimelineEventView' => 'view/phui/PHUITimelineEventView.php',
|
'PHUITimelineEventView' => 'view/phui/PHUITimelineEventView.php',
|
||||||
'PHUITimelineExample' => 'applications/uiexample/examples/PHUITimelineExample.php',
|
'PHUITimelineExample' => 'applications/uiexample/examples/PHUITimelineExample.php',
|
||||||
'PHUITimelineView' => 'view/phui/PHUITimelineView.php',
|
'PHUITimelineView' => 'view/phui/PHUITimelineView.php',
|
||||||
|
'PHUITypeaheadExample' => 'applications/uiexample/examples/PHUITypeaheadExample.php',
|
||||||
'PHUIWorkboardView' => 'view/phui/PHUIWorkboardView.php',
|
'PHUIWorkboardView' => 'view/phui/PHUIWorkboardView.php',
|
||||||
'PHUIWorkpanelView' => 'view/phui/PHUIWorkpanelView.php',
|
'PHUIWorkpanelView' => 'view/phui/PHUIWorkpanelView.php',
|
||||||
'PackageCreateMail' => 'applications/owners/mail/PackageCreateMail.php',
|
'PackageCreateMail' => 'applications/owners/mail/PackageCreateMail.php',
|
||||||
|
@ -4489,6 +4490,7 @@ phutil_register_library_map(array(
|
||||||
'PHUITimelineEventView' => 'AphrontView',
|
'PHUITimelineEventView' => 'AphrontView',
|
||||||
'PHUITimelineExample' => 'PhabricatorUIExample',
|
'PHUITimelineExample' => 'PhabricatorUIExample',
|
||||||
'PHUITimelineView' => 'AphrontView',
|
'PHUITimelineView' => 'AphrontView',
|
||||||
|
'PHUITypeaheadExample' => 'PhabricatorUIExample',
|
||||||
'PHUIWorkboardView' => 'AphrontTagView',
|
'PHUIWorkboardView' => 'AphrontTagView',
|
||||||
'PHUIWorkpanelView' => 'AphrontTagView',
|
'PHUIWorkpanelView' => 'AphrontTagView',
|
||||||
'PackageCreateMail' => 'PackageMail',
|
'PackageCreateMail' => 'PackageMail',
|
||||||
|
|
|
@ -50,6 +50,7 @@ final class PhabricatorViewerDatasource
|
||||||
return $this->newFunctionResult()
|
return $this->newFunctionResult()
|
||||||
->setName(pht('Current Viewer'))
|
->setName(pht('Current Viewer'))
|
||||||
->setPHID('viewer()')
|
->setPHID('viewer()')
|
||||||
|
->setIcon('fa-user')
|
||||||
->setUnique(true);
|
->setUnique(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -56,9 +56,17 @@ final class PhabricatorObjectHandle
|
||||||
if ($this->tagColor) {
|
if ($this->tagColor) {
|
||||||
return $this->tagColor;
|
return $this->tagColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'blue';
|
return 'blue';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getIconColor() {
|
||||||
|
if ($this->tagColor) {
|
||||||
|
return $this->tagColor;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
public function getTypeIcon() {
|
public function getTypeIcon() {
|
||||||
if ($this->getPHIDType()) {
|
if ($this->getPHIDType()) {
|
||||||
return $this->getPHIDType()->getTypeIcon();
|
return $this->getPHIDType()->getTypeIcon();
|
||||||
|
|
|
@ -63,7 +63,7 @@ final class PhabricatorProjectDatasource
|
||||||
->setDisplayType('Project')
|
->setDisplayType('Project')
|
||||||
->setURI('/tag/'.$proj->getPrimarySlug().'/')
|
->setURI('/tag/'.$proj->getPrimarySlug().'/')
|
||||||
->setPHID($proj->getPHID())
|
->setPHID($proj->getPHID())
|
||||||
->setIcon($proj->getIcon().' bluegrey')
|
->setIcon($proj->getIcon().' '.$proj->getColor())
|
||||||
->setPriorityType('proj')
|
->setPriorityType('proj')
|
||||||
->setClosed($closed);
|
->setClosed($closed);
|
||||||
|
|
||||||
|
|
|
@ -116,6 +116,7 @@ final class PhabricatorProjectMembersDatasource
|
||||||
->setDisplayName(pht('Members: %s', $project->getName()))
|
->setDisplayName(pht('Members: %s', $project->getName()))
|
||||||
->setURI('/tag/'.$project->getPrimarySlug().'/')
|
->setURI('/tag/'.$project->getPrimarySlug().'/')
|
||||||
->setPHID('members('.$project->getPHID().')')
|
->setPHID('members('.$project->getPHID().')')
|
||||||
|
->setIcon('fa-users')
|
||||||
->setClosed($closed);
|
->setClosed($closed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -187,16 +187,16 @@ abstract class PhabricatorTypeaheadDatasource extends Phobject {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function newFunctionResult() {
|
protected function newFunctionResult() {
|
||||||
// TODO: Find a more consistent design.
|
|
||||||
return id(new PhabricatorTypeaheadResult())
|
return id(new PhabricatorTypeaheadResult())
|
||||||
->setIcon('fa-magic indigo');
|
->setTokenType(PhabricatorTypeaheadTokenView::TYPE_FUNCTION)
|
||||||
|
->setIcon('fa-asterisk');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function newInvalidToken($name) {
|
public function newInvalidToken($name) {
|
||||||
return id(new PhabricatorTypeaheadTokenView())
|
return id(new PhabricatorTypeaheadTokenView())
|
||||||
->setKey(PhabricatorTypeaheadTokenView::KEY_INVALID)
|
|
||||||
->setValue($name)
|
->setValue($name)
|
||||||
->setIcon('fa-exclamation-circle red');
|
->setIcon('fa-exclamation-circle')
|
||||||
|
->setTokenType(PhabricatorTypeaheadTokenView::TYPE_INVALID);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -( Token Functions )---------------------------------------------------- */
|
/* -( Token Functions )---------------------------------------------------- */
|
||||||
|
|
|
@ -13,6 +13,7 @@ final class PhabricatorTypeaheadResult {
|
||||||
private $imageSprite;
|
private $imageSprite;
|
||||||
private $icon;
|
private $icon;
|
||||||
private $closed;
|
private $closed;
|
||||||
|
private $tokenType;
|
||||||
private $unique;
|
private $unique;
|
||||||
|
|
||||||
public function setIcon($icon) {
|
public function setIcon($icon) {
|
||||||
|
@ -91,6 +92,18 @@ final class PhabricatorTypeaheadResult {
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function setTokenType($type) {
|
||||||
|
$this->tokenType = $type;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTokenType() {
|
||||||
|
if ($this->closed && !$this->tokenType) {
|
||||||
|
return PhabricatorTypeaheadTokenView::TYPE_DISABLED;
|
||||||
|
}
|
||||||
|
return $this->tokenType;
|
||||||
|
}
|
||||||
|
|
||||||
public function getSortKey() {
|
public function getSortKey() {
|
||||||
// Put unique results (special parameter functions) ahead of other
|
// Put unique results (special parameter functions) ahead of other
|
||||||
// results.
|
// results.
|
||||||
|
@ -116,6 +129,7 @@ final class PhabricatorTypeaheadResult {
|
||||||
$this->getIcon(),
|
$this->getIcon(),
|
||||||
$this->closed,
|
$this->closed,
|
||||||
$this->imageSprite ? (string)$this->imageSprite : null,
|
$this->imageSprite ? (string)$this->imageSprite : null,
|
||||||
|
$this->tokenType,
|
||||||
$this->unique ? 1 : null,
|
$this->unique ? 1 : null,
|
||||||
);
|
);
|
||||||
while (end($data) === null) {
|
while (end($data) === null) {
|
||||||
|
|
|
@ -3,12 +3,16 @@
|
||||||
final class PhabricatorTypeaheadTokenView
|
final class PhabricatorTypeaheadTokenView
|
||||||
extends AphrontTagView {
|
extends AphrontTagView {
|
||||||
|
|
||||||
|
const TYPE_OBJECT = 'object';
|
||||||
|
const TYPE_DISABLED = 'disabled';
|
||||||
|
const TYPE_FUNCTION = 'function';
|
||||||
|
const TYPE_INVALID = 'invalid';
|
||||||
|
|
||||||
private $key;
|
private $key;
|
||||||
private $icon;
|
private $icon;
|
||||||
private $inputName;
|
private $inputName;
|
||||||
private $value;
|
private $value;
|
||||||
|
private $tokenType = self::TYPE_OBJECT;
|
||||||
const KEY_INVALID = '<invalid>';
|
|
||||||
|
|
||||||
public static function newFromTypeaheadResult(
|
public static function newFromTypeaheadResult(
|
||||||
PhabricatorTypeaheadResult $result) {
|
PhabricatorTypeaheadResult $result) {
|
||||||
|
@ -16,16 +20,24 @@ final class PhabricatorTypeaheadTokenView
|
||||||
return id(new PhabricatorTypeaheadTokenView())
|
return id(new PhabricatorTypeaheadTokenView())
|
||||||
->setKey($result->getPHID())
|
->setKey($result->getPHID())
|
||||||
->setIcon($result->getIcon())
|
->setIcon($result->getIcon())
|
||||||
->setValue($result->getDisplayName());
|
->setValue($result->getDisplayName())
|
||||||
|
->setTokenType($result->getTokenType());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function newFromHandle(
|
public static function newFromHandle(
|
||||||
PhabricatorObjectHandle $handle) {
|
PhabricatorObjectHandle $handle) {
|
||||||
|
|
||||||
return id(new PhabricatorTypeaheadTokenView())
|
$token = id(new PhabricatorTypeaheadTokenView())
|
||||||
->setKey($handle->getPHID())
|
->setKey($handle->getPHID())
|
||||||
->setValue($handle->getFullName())
|
->setValue($handle->getFullName())
|
||||||
->setIcon($handle->getIcon());
|
->setIcon(rtrim($handle->getIcon().' '.$handle->getIconColor()));
|
||||||
|
|
||||||
|
if ($handle->isDisabled() ||
|
||||||
|
$handle->getStatus() == PhabricatorObjectHandleStatus::STATUS_CLOSED) {
|
||||||
|
$token->setTokenType(self::TYPE_DISABLED);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $token;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setKey($key) {
|
public function setKey($key) {
|
||||||
|
@ -37,6 +49,15 @@ final class PhabricatorTypeaheadTokenView
|
||||||
return $this->key;
|
return $this->key;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function setTokenType($token_type) {
|
||||||
|
$this->tokenType = $token_type;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTokenType() {
|
||||||
|
return $this->tokenType;
|
||||||
|
}
|
||||||
|
|
||||||
public function setInputName($input_name) {
|
public function setInputName($input_name) {
|
||||||
$this->inputName = $input_name;
|
$this->inputName = $input_name;
|
||||||
return $this;
|
return $this;
|
||||||
|
@ -69,8 +90,25 @@ final class PhabricatorTypeaheadTokenView
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function getTagAttributes() {
|
protected function getTagAttributes() {
|
||||||
|
$classes = array();
|
||||||
|
$classes[] = 'jx-tokenizer-token';
|
||||||
|
switch ($this->getTokenType()) {
|
||||||
|
case self::TYPE_FUNCTION:
|
||||||
|
$classes[] = 'jx-tokenizer-token-function';
|
||||||
|
break;
|
||||||
|
case self::TYPE_INVALID:
|
||||||
|
$classes[] = 'jx-tokenizer-token-invalid';
|
||||||
|
break;
|
||||||
|
case self::TYPE_DISABLED:
|
||||||
|
$classes[] = 'jx-tokenizer-token-disabled';
|
||||||
|
break;
|
||||||
|
case self::TYPE_OBJECT:
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
return array(
|
return array(
|
||||||
'class' => 'jx-tokenizer-token',
|
'class' => $classes,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
57
src/applications/uiexample/examples/PHUITypeaheadExample.php
Normal file
57
src/applications/uiexample/examples/PHUITypeaheadExample.php
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class PHUITypeaheadExample extends PhabricatorUIExample {
|
||||||
|
|
||||||
|
public function getName() {
|
||||||
|
return 'Typeaheads';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDescription() {
|
||||||
|
return pht('Typeaheads, tokenizers and tokens.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function renderExample() {
|
||||||
|
|
||||||
|
$token_list = array();
|
||||||
|
|
||||||
|
$token_list[] = id(new PhabricatorTypeaheadTokenView())
|
||||||
|
->setValue(pht('Normal Object'))
|
||||||
|
->setIcon('fa-user');
|
||||||
|
|
||||||
|
$token_list[] = id(new PhabricatorTypeaheadTokenView())
|
||||||
|
->setValue(pht('Disabled Object'))
|
||||||
|
->setTokenType(PhabricatorTypeaheadTokenView::TYPE_DISABLED)
|
||||||
|
->setIcon('fa-user');
|
||||||
|
|
||||||
|
$token_list[] = id(new PhabricatorTypeaheadTokenView())
|
||||||
|
->setValue(pht('Custom Object'))
|
||||||
|
->setIcon('fa-tag green');
|
||||||
|
|
||||||
|
$token_list[] = id(new PhabricatorTypeaheadTokenView())
|
||||||
|
->setValue(pht('Function Token'))
|
||||||
|
->setTokenType(PhabricatorTypeaheadTokenView::TYPE_FUNCTION)
|
||||||
|
->setIcon('fa-users');
|
||||||
|
|
||||||
|
$token_list[] = id(new PhabricatorTypeaheadTokenView())
|
||||||
|
->setValue(pht('Invalid Token'))
|
||||||
|
->setTokenType(PhabricatorTypeaheadTokenView::TYPE_INVALID)
|
||||||
|
->setIcon('fa-exclamation-circle');
|
||||||
|
|
||||||
|
|
||||||
|
$token_list = phutil_tag(
|
||||||
|
'div',
|
||||||
|
array(
|
||||||
|
'class' => 'grouped',
|
||||||
|
'style' => 'padding: 8px',
|
||||||
|
),
|
||||||
|
$token_list);
|
||||||
|
|
||||||
|
$output = array();
|
||||||
|
|
||||||
|
$output[] = id(new PHUIObjectBoxView())
|
||||||
|
->setHeaderText('Tokens')
|
||||||
|
->appendChild($token_list);
|
||||||
|
|
||||||
|
return $output;
|
||||||
|
}
|
||||||
|
}
|
|
@ -51,7 +51,9 @@ final class AphrontFormTokenizerControl extends AphrontFormControl {
|
||||||
}
|
}
|
||||||
|
|
||||||
$datasource = $this->datasource;
|
$datasource = $this->datasource;
|
||||||
|
if ($datasource) {
|
||||||
$datasource->setViewer($this->getUser());
|
$datasource->setViewer($this->getUser());
|
||||||
|
}
|
||||||
|
|
||||||
$placeholder = null;
|
$placeholder = null;
|
||||||
if (!strlen($this->placeholder)) {
|
if (!strlen($this->placeholder)) {
|
||||||
|
@ -84,7 +86,8 @@ final class AphrontFormTokenizerControl extends AphrontFormControl {
|
||||||
$token = $datasource->newInvalidToken($name);
|
$token = $datasource->newInvalidToken($name);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($token->getKey() == PhabricatorTypeaheadTokenView::KEY_INVALID) {
|
$type = $token->getTokenType();
|
||||||
|
if ($type == PhabricatorTypeaheadTokenView::TYPE_INVALID) {
|
||||||
$token->setKey($value);
|
$token->setKey($value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -121,6 +124,7 @@ final class AphrontFormTokenizerControl extends AphrontFormControl {
|
||||||
'src' => $datasource_uri,
|
'src' => $datasource_uri,
|
||||||
'value' => mpull($tokens, 'getValue', 'getKey'),
|
'value' => mpull($tokens, 'getValue', 'getKey'),
|
||||||
'icons' => mpull($tokens, 'getIcon', 'getKey'),
|
'icons' => mpull($tokens, 'getIcon', 'getKey'),
|
||||||
|
'types' => mpull($tokens, 'getTokenType', 'getKey'),
|
||||||
'limit' => $this->limit,
|
'limit' => $this->limit,
|
||||||
'username' => $username,
|
'username' => $username,
|
||||||
'placeholder' => $placeholder,
|
'placeholder' => $placeholder,
|
||||||
|
|
|
@ -83,6 +83,51 @@ a.jx-tokenizer-token:hover {
|
||||||
color: {$bluetext};
|
color: {$bluetext};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a.jx-tokenizer-token-function {
|
||||||
|
border-color: {$sh-lightyellowborder};
|
||||||
|
background: {$sh-yellowbackground};
|
||||||
|
color: {$sh-yellowtext};
|
||||||
|
}
|
||||||
|
|
||||||
|
a.jx-tokenizer-token-function:hover {
|
||||||
|
border-color: {$sh-yellowborder};
|
||||||
|
background: {$lightyellow};
|
||||||
|
}
|
||||||
|
|
||||||
|
.jx-tokenizer-token-function .phui-icon-view {
|
||||||
|
color: {$sh-yellowicon};
|
||||||
|
}
|
||||||
|
|
||||||
|
a.jx-tokenizer-token-disabled {
|
||||||
|
border-color: {$sh-lightgreyborder};
|
||||||
|
background: {$sh-greybackground};
|
||||||
|
color: {$sh-greytext};
|
||||||
|
}
|
||||||
|
|
||||||
|
a.jx-tokenizer-token-disabled:hover {
|
||||||
|
border-color: {$sh-greyborder};
|
||||||
|
background: {$greybackground};
|
||||||
|
}
|
||||||
|
|
||||||
|
.jx-tokenizer-token-disabled .phui-icon-view {
|
||||||
|
color: {$sh-greyicon};
|
||||||
|
}
|
||||||
|
|
||||||
|
a.jx-tokenizer-token-invalid {
|
||||||
|
border-color: {$sh-lightredborder};
|
||||||
|
background: {$sh-redbackground};
|
||||||
|
color: {$sh-redtext};
|
||||||
|
}
|
||||||
|
|
||||||
|
a.jx-tokenizer-token-invalid:hover {
|
||||||
|
border-color: {$sh-redborder};
|
||||||
|
background: {$lightred};
|
||||||
|
}
|
||||||
|
|
||||||
|
.jx-tokenizer-token-invalid .phui-icon-view {
|
||||||
|
color: {$sh-redicon};
|
||||||
|
}
|
||||||
|
|
||||||
.tokenizer-result {
|
.tokenizer-result {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 5px 8px 5px 28px;
|
padding: 5px 8px 5px 28px;
|
||||||
|
|
|
@ -348,16 +348,22 @@ JX.install('Tokenizer', {
|
||||||
}, '\u00d7'); // U+00D7 multiplication sign
|
}, '\u00d7'); // U+00D7 multiplication sign
|
||||||
|
|
||||||
var display_token = value;
|
var display_token = value;
|
||||||
var render_callback = this.getRenderTokenCallback();
|
|
||||||
if (render_callback) {
|
|
||||||
display_token = render_callback(value, key);
|
|
||||||
}
|
|
||||||
|
|
||||||
return JX.$N('a', {
|
var attrs = {
|
||||||
className: 'jx-tokenizer-token',
|
className: 'jx-tokenizer-token',
|
||||||
sigil: 'token',
|
sigil: 'token',
|
||||||
meta: {key: key}
|
meta: {key: key}
|
||||||
}, [display_token, input, remove]);
|
};
|
||||||
|
var container = JX.$N('a', attrs);
|
||||||
|
|
||||||
|
var render_callback = this.getRenderTokenCallback();
|
||||||
|
if (render_callback) {
|
||||||
|
display_token = render_callback(value, key, container);
|
||||||
|
}
|
||||||
|
|
||||||
|
JX.DOM.setContent(container, [display_token, input, remove]);
|
||||||
|
|
||||||
|
return container;
|
||||||
},
|
},
|
||||||
|
|
||||||
getTokens : function() {
|
getTokens : function() {
|
||||||
|
|
|
@ -172,23 +172,27 @@ JX.install('Prefab', {
|
||||||
|
|
||||||
var tokenizer = new JX.Tokenizer(root);
|
var tokenizer = new JX.Tokenizer(root);
|
||||||
tokenizer.setTypeahead(typeahead);
|
tokenizer.setTypeahead(typeahead);
|
||||||
tokenizer.setRenderTokenCallback(function(value, key) {
|
tokenizer.setRenderTokenCallback(function(value, key, container) {
|
||||||
var result = datasource.getResult(key);
|
var result = datasource.getResult(key);
|
||||||
|
|
||||||
var icon;
|
var icon;
|
||||||
|
var type;
|
||||||
if (result) {
|
if (result) {
|
||||||
icon = result.icon;
|
icon = result.icon;
|
||||||
value = result.displayName;
|
value = result.displayName;
|
||||||
|
type = result.tokenType;
|
||||||
} else {
|
} else {
|
||||||
icon = config.icons[key];
|
icon = config.icons[key];
|
||||||
|
type = config.types[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (icon) {
|
if (icon) {
|
||||||
icon = JX.Prefab._renderIcon(icon);
|
icon = JX.Prefab._renderIcon(icon);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Maybe we should render these closed tags in grey? Figure out
|
if (type) {
|
||||||
// how we're going to use color.
|
JX.DOM.alterClass(container, 'jx-tokenizer-token-' + type, true);
|
||||||
|
}
|
||||||
|
|
||||||
return [icon, value];
|
return [icon, value];
|
||||||
});
|
});
|
||||||
|
@ -288,7 +292,8 @@ JX.install('Prefab', {
|
||||||
closed: closed,
|
closed: closed,
|
||||||
type: fields[5],
|
type: fields[5],
|
||||||
sprite: fields[10],
|
sprite: fields[10],
|
||||||
unique: fields[11] || false
|
tokenType: fields[11],
|
||||||
|
unique: fields[12] || false
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue