mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
DOC Document how to enable the preview panel for custom DataObjects. (#10124)
This commit is contained in:
parent
d8499a24d0
commit
8f1c68db42
@ -7,16 +7,14 @@ summary: How content previews work in the CMS
|
|||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
__Deprecated:__
|
|
||||||
The following documentation regarding JavaScript layouts and Entwine applies to legacy code only.
|
|
||||||
If you're developing new functionality in React powered sections please refer to
|
|
||||||
[ReactJS in Silverstripe CMS](./How_Tos/Extend_CMS_Interface.md#reactjs-in-silverstripe).
|
|
||||||
|
|
||||||
With the addition of side-by-side editing, the preview has the ability to appear
|
With the addition of side-by-side editing, the preview has the ability to appear
|
||||||
within the CMS window when editing content in the _Pages_ section of the CMS.
|
within the CMS window when editing content in the CMS. This is enabled by default
|
||||||
The site is rendered into an iframe. It will update itself whenever the content
|
in the _Pages_ section for `SiteTree` models, but as outlined below can be enabled
|
||||||
is saved, and relevant pages will be loaded for editing when the user navigates
|
in other sections and for other models as well.
|
||||||
around in the preview.
|
|
||||||
|
Within the preview panel, the site is rendered into an iframe. It will update
|
||||||
|
itself whenever the content is saved, and relevant pages will be loaded for editing
|
||||||
|
when the user navigates around in the preview.
|
||||||
|
|
||||||
The root element for preview is `.cms-preview` which maintains the internal
|
The root element for preview is `.cms-preview` which maintains the internal
|
||||||
states necessary for rendering within the entwine properties. It provides
|
states necessary for rendering within the entwine properties. It provides
|
||||||
@ -24,22 +22,380 @@ function calls for transitioning between these states and has the ability to
|
|||||||
update the appearance of the option selectors.
|
update the appearance of the option selectors.
|
||||||
|
|
||||||
In terms of backend support, it relies on `SilverStripeNavigator` to be rendered
|
In terms of backend support, it relies on `SilverStripeNavigator` to be rendered
|
||||||
into the `.cms-edit-form`. _LeftAndMain_ will automatically take care of
|
into the form. _LeftAndMain_ will automatically take care of generating it as long
|
||||||
generating it as long as the `*_SilverStripeNavigator` template is found -
|
as the `*_SilverStripeNavigator` template is found - first segment has to match the
|
||||||
first segment has to match current _LeftAndMain_-derived class (e.g.
|
current _LeftAndMain_-derived class (e.g. `LeftAndMain_SilverStripeNavigator`).
|
||||||
`LeftAndMain_SilverStripeNavigator`).
|
|
||||||
|
## PHP
|
||||||
|
For a DataObject to be previewed using the preview panel there are a few prerequisites:
|
||||||
|
|
||||||
|
- The class must implement the `CMSPreviewable` interface
|
||||||
|
- At least one preview state must be enabled for the class
|
||||||
|
- There must be some valid URL to use inside the preview panel
|
||||||
|
|
||||||
|
### CMSPreviewable
|
||||||
|
The `CMSPreviewable` interface has three methods: `PreviewLink`, `CMSEditLink`, and
|
||||||
|
`getMimeType`.
|
||||||
|
|
||||||
|
#### PreviewLink
|
||||||
|
The `PreviewLink` method is what determines the URL used inside the preview panel. If
|
||||||
|
your `DataObject` is intended to always belong to a page, you might want to preview the
|
||||||
|
item in the context of where it sits on the page using an anchor. You can also provide
|
||||||
|
some route specific for previewing this object, for example an action on the ModelAdmin
|
||||||
|
that is used to manage the object.
|
||||||
|
|
||||||
|
#### CMSEditLink
|
||||||
|
This method exists so that when a user clicks on a link in the preview panel, the CMS
|
||||||
|
edit form for the page the link leads to can be loaded. Unless your `DataObject` is
|
||||||
|
[acting like a page](https://www.silverstripe.org/learn/lessons/v4/controller-actions-dataobjects-as-pages-1)
|
||||||
|
this will likely not apply, but as this method is mandatory and public we may as well
|
||||||
|
set it up correctly.
|
||||||
|
|
||||||
|
If your object belongs to [a custom ModelAdmin](./01_ModelAdmin.md), the edit URL for the
|
||||||
|
object is predictable enough to construct and return from this method as you'll see below.
|
||||||
|
The format for that situation is always the same, with increasing complexity if you're
|
||||||
|
nesting `GridField`s. For the below examples it is assumed you aren't using nested
|
||||||
|
`GridField`s.
|
||||||
|
|
||||||
|
If your object belongs to a page, you can safely get away with returning `null` or an empty
|
||||||
|
string, as it won't be used. You can choose to return a valid edit link, but because of the
|
||||||
|
complexity of the way these links are generated it would be difficult to do so in a general,
|
||||||
|
reusable way.
|
||||||
|
|
||||||
|
#### getMimeType
|
||||||
|
In ~90% of cases will be 'text/html', but note it is also possible to display (for example)
|
||||||
|
an inline PDF document in the preview panel.
|
||||||
|
|
||||||
|
### Preview states
|
||||||
|
The preview state(s) you apply to your `DataObject` will depend primarily on whether it uses
|
||||||
|
the [Versioned](api:SilverStripe\Versioned\Versioned) extension or not.
|
||||||
|
|
||||||
|
#### Versioned DataObjects
|
||||||
|
If your class does use the `Versioned` extension, there are two different states available
|
||||||
|
to you. It is generally recommended that you enable both, so that content authors can toggle
|
||||||
|
between viewing the draft and the published content.
|
||||||
|
|
||||||
|
To enable the draft preview state, use the `$show_stage_link` configuration variable.
|
||||||
|
|
||||||
|
```php
|
||||||
|
private static $show_stage_link = true;
|
||||||
|
```
|
||||||
|
|
||||||
|
To enable the published preview state, use the `$show_live_link` configuration variable.
|
||||||
|
|
||||||
|
```php
|
||||||
|
private static $show_live_link = true;
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Unversioned DataObjects
|
||||||
|
If you are not using the `Versioned` extension for your class, there is only one preview
|
||||||
|
state you can use. This state will always be active once you enable it.
|
||||||
|
|
||||||
|
To enable the unversioned preview state, use the `$show_unversioned_preview_link`
|
||||||
|
configuration variable.
|
||||||
|
|
||||||
|
```php
|
||||||
|
private static $show_unversioned_preview_link = true;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Enabling preview for DataObjects in a ModelAdmin
|
||||||
|
For this example we will take the `Product` and `MyAdmin` classes from the
|
||||||
|
[ModelAdmin documentation](./01_ModelAdmin.md).
|
||||||
|
|
||||||
|
#### The DataObject implementation
|
||||||
|
As mentioned above, your `Product` class must implement the `CMSPreviewable` interface.
|
||||||
|
It also needs at least one preview state enabled. This example assumes we aren't using
|
||||||
|
the `Versioned` extension.
|
||||||
|
|
||||||
|
```php
|
||||||
|
use SilverStripe\ORM\CMSPreviewable;
|
||||||
|
use SilverStripe\ORM\DataObject;
|
||||||
|
|
||||||
|
class Product extends DataObject implements CMSPreviewable
|
||||||
|
{
|
||||||
|
private static $show_unversioned_preview_link = true;
|
||||||
|
|
||||||
|
// ...
|
||||||
|
|
||||||
|
public function PreviewLink($action = null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function CMSEditLink()
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMimeType()
|
||||||
|
{
|
||||||
|
return 'text/html';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
We will need to add a new action to the `ModelAdmin` to provide the actual preview itself.
|
||||||
|
For now, assume that action will be called `cmsPreview`. We can very easily craft a valid
|
||||||
|
URL using the `Link` method on the `MyAdmin` class.
|
||||||
|
|
||||||
|
Note that if you had set up this model to [act like a page](https://www.silverstripe.org/learn/lessons/v4/controller-actions-dataobjects-as-pages-1),
|
||||||
|
you could simply `return $this->Link($action)`. In that case the new action would not need
|
||||||
|
to be added to your `ModelAdmin`.
|
||||||
|
|
||||||
|
```php
|
||||||
|
public function PreviewLink($action = null)
|
||||||
|
{
|
||||||
|
$admin = MyAdmin::singleton();
|
||||||
|
return Controller::join_links(
|
||||||
|
$admin->Link(str_replace('\\', '-', $this->ClassName)),
|
||||||
|
'cmsPreview',
|
||||||
|
$this->ID
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `CMSEditLink` is also very easy to build, because the edit link used by `ModelAdmin`s
|
||||||
|
is predictable.
|
||||||
|
```php
|
||||||
|
public function CMSEditLink()
|
||||||
|
{
|
||||||
|
$admin = MyAdmin::singleton();
|
||||||
|
$sanitisedClassname = str_replace('\\', '-', $this->ClassName);
|
||||||
|
return Controller::join_links(
|
||||||
|
$admin->Link($sanitisedClassname),
|
||||||
|
'EditForm/field/',
|
||||||
|
$sanitisedClassname,
|
||||||
|
'item',
|
||||||
|
$this->ID
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Let's assume when you display this object on the front end you're just looping through a
|
||||||
|
list of items and indirectly calling `forTemplate` using the [`$Me` template variable](../01_Templates/01_Syntax.md#me).
|
||||||
|
This method will be used by the `cmsPreview` action in the `MyAdmin` class to tell the
|
||||||
|
CMS what to display in the preview panel.
|
||||||
|
|
||||||
|
The `forTemplate` method will probably look something like this:
|
||||||
|
|
||||||
|
```php
|
||||||
|
public function forTemplate()
|
||||||
|
{
|
||||||
|
// If the template for this DataObject is not an "Include" template, use the appropriate type here e.g. "Layout".
|
||||||
|
return $this->renderWith(['type' => 'Includes', self::class]);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### The ModelAdmin implementation
|
||||||
|
We need to add the `cmsPreview` action to the `MyAdmin` class, which will output the
|
||||||
|
content which should be displayed in the preview panel.
|
||||||
|
|
||||||
|
Because this is a public method called on a `ModelAdmin`, which will often be executed
|
||||||
|
in a back-end context using admin themes, it pays to ensure we're loading the front-end
|
||||||
|
themes whilst rendering out the preview content.
|
||||||
|
|
||||||
|
```php
|
||||||
|
use SilverStripe\Admin\ModelAdmin;
|
||||||
|
use SilverStripe\View\SSViewer;
|
||||||
|
|
||||||
|
class MyAdmin extends ModelAdmin
|
||||||
|
{
|
||||||
|
private static $managed_models = [
|
||||||
|
Product::class,
|
||||||
|
];
|
||||||
|
|
||||||
|
private static $url_segment = 'products';
|
||||||
|
|
||||||
|
private static $menu_title = 'Products';
|
||||||
|
|
||||||
|
private static $allowed_actions = [
|
||||||
|
'cmsPreview',
|
||||||
|
];
|
||||||
|
|
||||||
|
private static $url_handlers = [
|
||||||
|
'$ModelClass/cmsPreview/$ID' => 'cmsPreview',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function cmsPreview()
|
||||||
|
{
|
||||||
|
$id = $this->urlParams['ID'];
|
||||||
|
$obj = $this->modelClass::get_by_id($id);
|
||||||
|
if (!$obj || !$obj->exists()) {
|
||||||
|
return $this->httpError(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Include use of a front-end theme temporarily.
|
||||||
|
$oldThemes = SSViewer::get_themes();
|
||||||
|
SSViewer::set_themes(SSViewer::config()->get('themes'));
|
||||||
|
$preview = $obj->forTemplate();
|
||||||
|
|
||||||
|
// Make sure to set back to backend themes.
|
||||||
|
SSViewer::set_themes($oldThemes);
|
||||||
|
|
||||||
|
return $preview;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Enabling preview for DataObjects which belong to a page
|
||||||
|
If the `DataObject` you want to preview belongs to a specific page, for example
|
||||||
|
through a `has_one` or `has_many` relation, you will most likely want to preview
|
||||||
|
it in the context of the page it belongs to.
|
||||||
|
|
||||||
|
#### The Page implementation
|
||||||
|
For this example we will assume the `Product` class is `Versioned`.
|
||||||
|
|
||||||
|
As discussed above, the `CMSEditLink` method is used to load the correct edit form
|
||||||
|
in the CMS when you click on a link within the preview panel. This uses the
|
||||||
|
`x-page-id` and `x-cms-edit-link` meta tags in the head of the page (assuming your
|
||||||
|
page template calls `$MetaTags` in the `<head>` element). When a page loads,
|
||||||
|
these meta tags are checked and the appropriate form is loaded.
|
||||||
|
|
||||||
|
When rendering a full page in the preview panel to preview a `DataObject` on that
|
||||||
|
page, the meta tags for that page are present. When a content author toggles between
|
||||||
|
the draft and published preview states, those meta tags are checked and the page's
|
||||||
|
edit form would be loaded instead of the `DataObject`'s form. To avoid this
|
||||||
|
unexpected behaviour, you can include an extra GET parameter in the value returned
|
||||||
|
by `PreviewLink`. Then in the `MetaTags` method, when the extra parameter is
|
||||||
|
detected, omit the relevant meta tags.
|
||||||
|
|
||||||
|
Note that this is not necessary for unversioned `DataObjects` as they only have
|
||||||
|
one preview state.
|
||||||
|
|
||||||
|
```php
|
||||||
|
use SilverStripe\Control\Controller;
|
||||||
|
use SilverStripe\View\Parsers\HTML4Value;
|
||||||
|
|
||||||
|
class ProductPage extends Page
|
||||||
|
{
|
||||||
|
//...
|
||||||
|
|
||||||
|
private static $has_many = [
|
||||||
|
'Products' => Product::class,
|
||||||
|
];
|
||||||
|
|
||||||
|
public function MetaTags($includeTitle = true)
|
||||||
|
{
|
||||||
|
$tags = parent::MetaTags($includeTitle);
|
||||||
|
if (!Controller::has_curr()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// If the 'DataObjectPreview' GET parameter is present, remove 'x-page-id' and 'x-cms-edit-link' meta tags.
|
||||||
|
// This ensures that toggling between draft/published states doesn't revert the CMS to the page's edit form.
|
||||||
|
$controller = Controller::curr();
|
||||||
|
$request = $controller->getRequest();
|
||||||
|
if ($request->getVar('DataObjectPreview') !== null) {
|
||||||
|
$html = HTML4Value::create($tags);
|
||||||
|
$xpath = "//meta[@name='x-page-id' or @name='x-cms-edit-link']";
|
||||||
|
$removeTags = $html->query($xpath);
|
||||||
|
$body = $html->getBody();
|
||||||
|
foreach ($removeTags as $tag) {
|
||||||
|
$body->removeChild($tag);
|
||||||
|
}
|
||||||
|
$tags = $html->getContent();
|
||||||
|
}
|
||||||
|
return $tags;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### The DataObject Implementation
|
||||||
|
Make sure the Versioned `Product` class implements `CMSPreviewable` and enables
|
||||||
|
the draft and published preview states.
|
||||||
|
|
||||||
|
```php
|
||||||
|
use SilverStripe\ORM\CMSPreviewable;
|
||||||
|
use SilverStripe\ORM\DataObject;
|
||||||
|
use SilverStripe\Versioned\Versioned;
|
||||||
|
|
||||||
|
class Product extends DataObject implements CMSPreviewable
|
||||||
|
{
|
||||||
|
private static $show_stage_link = true;
|
||||||
|
private static $show_live_link = true;
|
||||||
|
|
||||||
|
private static $extensions = [
|
||||||
|
Versioned::class,
|
||||||
|
];
|
||||||
|
|
||||||
|
private static $has_one = [
|
||||||
|
'ProductPage' => ProductPage::class,
|
||||||
|
];
|
||||||
|
|
||||||
|
// ...
|
||||||
|
|
||||||
|
public function PreviewLink($action = null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function CMSEditLink()
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMimeType()
|
||||||
|
{
|
||||||
|
return 'text/html';
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Implement a method which gives you a unique repeatable anchor for each
|
||||||
|
distinct `Product` object.
|
||||||
|
|
||||||
|
```php
|
||||||
|
/**
|
||||||
|
* Used to generate the id for the product element in the template.
|
||||||
|
*/
|
||||||
|
public function getAnchor()
|
||||||
|
{
|
||||||
|
return 'product-' . $this->getUniqueKey();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
For the `PreviewLink`, append the `DataObjectPreview` GET parameter to the
|
||||||
|
page's frontend URL.
|
||||||
|
```php
|
||||||
|
public function PreviewLink($action = null)
|
||||||
|
{
|
||||||
|
// Let the page know it's being previewed from a DataObject edit form (see Page::MetaTags())
|
||||||
|
$action = $action . '?DataObjectPreview=' . mt_rand();
|
||||||
|
// Scroll the preview straight to where the object sits on the page.
|
||||||
|
if ($page = $this->ProductPage()) {
|
||||||
|
$link = $page->Link($action) . '#' . $this->getAnchor();
|
||||||
|
return $link;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The CMSEditLink doesn't matter so much for this implementation. It is required
|
||||||
|
by the `CMSPreviewable` interface so some implementation must be provided, but
|
||||||
|
you can safely return `null` or an empty string with no repercussions in this
|
||||||
|
situation.
|
||||||
|
|
||||||
|
#### The Page template
|
||||||
|
In your page template, make sure the anchor is used where you render the objects.
|
||||||
|
This allows the preview panel to be scrolled automatically to where the object
|
||||||
|
being edited sits on the page.
|
||||||
|
|
||||||
|
```ss
|
||||||
|
<%-- ... --%>
|
||||||
|
<% loop $Products %>
|
||||||
|
<div id="$Anchor">
|
||||||
|
<%-- ... --%>
|
||||||
|
</div>
|
||||||
|
<% end_loop %>
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Javascript
|
||||||
|
|
||||||
|
### Configuration and Defaults
|
||||||
|
|
||||||
We use `ss.preview` entwine namespace for all preview-related entwines.
|
We use `ss.preview` entwine namespace for all preview-related entwines.
|
||||||
|
|
||||||
[notice]
|
|
||||||
Caveat: `SilverStripeNavigator` and `CMSPreviewable` interface currently only
|
|
||||||
support SiteTree objects that are _Versioned_. They are not general enough for
|
|
||||||
using on any other DataObject. That pretty much limits the extendability of the
|
|
||||||
feature.
|
|
||||||
[/notice]
|
|
||||||
|
|
||||||
## Configuration and Defaults
|
|
||||||
|
|
||||||
Like most of the CMS, the preview UI is powered by
|
Like most of the CMS, the preview UI is powered by
|
||||||
[jQuery entwine](https://github.com/hafriedlander/jquery.entwine). This means
|
[jQuery entwine](https://github.com/hafriedlander/jquery.entwine). This means
|
||||||
its defaults are configured through JavaScript, by setting entwine properties.
|
its defaults are configured through JavaScript, by setting entwine properties.
|
||||||
@ -96,7 +452,7 @@ in the `silverstripe/admin` module.
|
|||||||
To understand how layouts are handled in the CMS UI, have a look at the
|
To understand how layouts are handled in the CMS UI, have a look at the
|
||||||
[CMS Architecture](cms_architecture) guide.
|
[CMS Architecture](cms_architecture) guide.
|
||||||
|
|
||||||
## Enabling preview
|
### Enabling preview
|
||||||
|
|
||||||
The frontend decides on the preview being enabled or disabled based on the
|
The frontend decides on the preview being enabled or disabled based on the
|
||||||
presence of the `.cms-previewable` class. If this class is not found the preview
|
presence of the `.cms-previewable` class. If this class is not found the preview
|
||||||
@ -113,7 +469,7 @@ The preview can be affected by calling `enablePreview` and `disablePreview`. You
|
|||||||
can check if the preview is active by inspecting the `IsPreviewEnabled` entwine
|
can check if the preview is active by inspecting the `IsPreviewEnabled` entwine
|
||||||
property.
|
property.
|
||||||
|
|
||||||
## Preview states
|
### Preview states
|
||||||
|
|
||||||
States are the site stages: _live_, _stage_ etc. Preview states are picked up
|
States are the site stages: _live_, _stage_ etc. Preview states are picked up
|
||||||
from the `SilverStripeNavigator`. You can invoke the state change by calling:
|
from the `SilverStripeNavigator`. You can invoke the state change by calling:
|
||||||
@ -133,7 +489,7 @@ You can get the current state by calling:
|
|||||||
$('.cms-preview').entwine('.ss.preview').getCurrentStateName();
|
$('.cms-preview').entwine('.ss.preview').getCurrentStateName();
|
||||||
```
|
```
|
||||||
|
|
||||||
## Preview sizes
|
### Preview sizes
|
||||||
|
|
||||||
This selector defines how the preview iframe is rendered, and try to emulate
|
This selector defines how the preview iframe is rendered, and try to emulate
|
||||||
different device sizes. The options are hardcoded. The option names map directly
|
different device sizes. The options are hardcoded. The option names map directly
|
||||||
@ -158,7 +514,7 @@ You can find out current size by calling:
|
|||||||
$('.cms-preview').entwine('.ss.preview').getCurrentSizeName();
|
$('.cms-preview').entwine('.ss.preview').getCurrentSizeName();
|
||||||
```
|
```
|
||||||
|
|
||||||
## Preview modes
|
### Preview modes
|
||||||
|
|
||||||
Preview modes map to the modes supported by the _threeColumnCompressor_ layout
|
Preview modes map to the modes supported by the _threeColumnCompressor_ layout
|
||||||
algorithm, see [layout reference](cms_layout) for more details. You
|
algorithm, see [layout reference](cms_layout) for more details. You
|
||||||
@ -183,7 +539,7 @@ preview is not visible. Currently CMS Actions are a separate area to the preview
|
|||||||
option selectors, even if they try to appear as one horizontal bar.
|
option selectors, even if they try to appear as one horizontal bar.
|
||||||
[/notice]
|
[/notice]
|
||||||
|
|
||||||
## Preview API
|
### Preview API
|
||||||
|
|
||||||
Namespace `ss.preview`, selector `.cms-preview`:
|
Namespace `ss.preview`, selector `.cms-preview`:
|
||||||
|
|
||||||
@ -197,6 +553,6 @@ Namespace `ss.preview`, selector `.cms-preview`:
|
|||||||
* **disablePreview**: deactivate the preview and switch to the _content_ mode. Preview will re-enable itself when new
|
* **disablePreview**: deactivate the preview and switch to the _content_ mode. Preview will re-enable itself when new
|
||||||
previewable content is loaded.
|
previewable content is loaded.
|
||||||
|
|
||||||
## Related
|
### Related
|
||||||
|
|
||||||
* [Reference: Layout](cms_layout)
|
* [Reference: Layout](cms_layout)
|
||||||
|
Loading…
Reference in New Issue
Block a user