fix(widget-markdown): Hitting Enter key in a list item doesn't create a new list item (#5550)
This commit is contained in:
parent
3bd36776d6
commit
ab3e8e1f5a
@ -35,7 +35,7 @@ describe('Markdown widget', () => {
|
|||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('creates nested list when selection is collapsed in non-first block of list item', () => {
|
it('converts a list item to a paragraph block which is a sibling of the parent list', () => {
|
||||||
cy.clickUnorderedListButton()
|
cy.clickUnorderedListButton()
|
||||||
.type('foo')
|
.type('foo')
|
||||||
.enter()
|
.enter()
|
||||||
@ -44,44 +44,20 @@ describe('Markdown widget', () => {
|
|||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<p>foo</p>
|
<p>foo</p>
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<p></p>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
<p></p>
|
||||||
`)
|
`)
|
||||||
.type('bar')
|
|
||||||
.enter()
|
|
||||||
.clickUnorderedListButton()
|
|
||||||
.confirmMarkdownEditorContent(`
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<p>foo</p>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<p>bar</p>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<p></p>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
`);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('converts empty nested list item to empty block in parent list item', () => {
|
it('converts empty nested list item to empty paragraph block in parent list item', () => {
|
||||||
cy.clickUnorderedListButton()
|
cy.clickUnorderedListButton()
|
||||||
.type('foo')
|
.type('foo')
|
||||||
.enter()
|
.enter()
|
||||||
.clickUnorderedListButton()
|
.tabkey()
|
||||||
.type('bar')
|
.type('bar')
|
||||||
.enter()
|
.enter()
|
||||||
.clickUnorderedListButton()
|
.tabkey()
|
||||||
.confirmMarkdownEditorContent(`
|
.confirmMarkdownEditorContent(`
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
@ -129,10 +105,10 @@ describe('Markdown widget', () => {
|
|||||||
cy.clickUnorderedListButton()
|
cy.clickUnorderedListButton()
|
||||||
.type('foo')
|
.type('foo')
|
||||||
.enter()
|
.enter()
|
||||||
.clickUnorderedListButton()
|
.tabkey()
|
||||||
.type('bar')
|
.type('bar')
|
||||||
.enter()
|
.enter()
|
||||||
.clickUnorderedListButton()
|
.tabkey()
|
||||||
.type('baz')
|
.type('baz')
|
||||||
.clickUnorderedListButton()
|
.clickUnorderedListButton()
|
||||||
.confirmMarkdownEditorContent(`
|
.confirmMarkdownEditorContent(`
|
||||||
@ -228,7 +204,7 @@ describe('Markdown widget', () => {
|
|||||||
.up()
|
.up()
|
||||||
.enter()
|
.enter()
|
||||||
.type('qux')
|
.type('qux')
|
||||||
.clickUnorderedListButton()
|
.tabkey()
|
||||||
.confirmMarkdownEditorContent(`
|
.confirmMarkdownEditorContent(`
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
@ -250,7 +226,6 @@ describe('Markdown widget', () => {
|
|||||||
.up()
|
.up()
|
||||||
.enter()
|
.enter()
|
||||||
.type('quux')
|
.type('quux')
|
||||||
.clickUnorderedListButton()
|
|
||||||
.confirmMarkdownEditorContent(`
|
.confirmMarkdownEditorContent(`
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
@ -307,9 +282,9 @@ describe('Markdown widget', () => {
|
|||||||
it('affects only selected list items', () => {
|
it('affects only selected list items', () => {
|
||||||
cy.clickUnorderedListButton()
|
cy.clickUnorderedListButton()
|
||||||
.type('foo')
|
.type('foo')
|
||||||
.enter({ times: 2 })
|
.enter()
|
||||||
.type('bar')
|
.type('bar')
|
||||||
.enter({ times: 2 })
|
.enter()
|
||||||
.type('baz')
|
.type('baz')
|
||||||
.setSelection('bar')
|
.setSelection('bar')
|
||||||
.clickUnorderedListButton()
|
.clickUnorderedListButton()
|
||||||
@ -352,14 +327,69 @@ describe('Markdown widget', () => {
|
|||||||
`)
|
`)
|
||||||
.setSelection('baz')
|
.setSelection('baz')
|
||||||
.clickUnorderedListButton()
|
.clickUnorderedListButton()
|
||||||
|
.confirmMarkdownEditorContent(`
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<p>foo</p>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p>bar</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<p>baz</p>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
`)
|
||||||
.setCursorAfter('baz')
|
.setCursorAfter('baz')
|
||||||
.enter()
|
.enter()
|
||||||
.clickUnorderedListButton()
|
.tabkey()
|
||||||
.type('qux')
|
.type('qux')
|
||||||
|
.confirmMarkdownEditorContent(`
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<p>foo</p>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p>bar</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<p>baz</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<p>qux</p>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
`)
|
||||||
.setSelection('baz')
|
.setSelection('baz')
|
||||||
.clickOrderedListButton()
|
.clickOrderedListButton()
|
||||||
|
.confirmMarkdownEditorContent(`
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<p>foo</p>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p>bar</p>
|
||||||
|
<ol>
|
||||||
|
<li>
|
||||||
|
<p>baz</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<p>qux</p>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
`)
|
||||||
.setCursorAfter('qux')
|
.setCursorAfter('qux')
|
||||||
.enter({ times: 4 })
|
.enter({ times: 2 })
|
||||||
.clickUnorderedListButton()
|
.clickUnorderedListButton()
|
||||||
.confirmMarkdownEditorContent(`
|
.confirmMarkdownEditorContent(`
|
||||||
<ul>
|
<ul>
|
||||||
@ -398,7 +428,7 @@ describe('Markdown widget', () => {
|
|||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('creates a new paragraph in a non-empty paragraph within a list item', () => {
|
it('creates a new list item in a non-empty list', () => {
|
||||||
cy.clickUnorderedListButton()
|
cy.clickUnorderedListButton()
|
||||||
.type('foo')
|
.type('foo')
|
||||||
.enter()
|
.enter()
|
||||||
@ -406,6 +436,8 @@ describe('Markdown widget', () => {
|
|||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<p>foo</p>
|
<p>foo</p>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
<p></p>
|
<p></p>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@ -416,46 +448,21 @@ describe('Markdown widget', () => {
|
|||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<p>foo</p>
|
<p>foo</p>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
<p>bar</p>
|
<p>bar</p>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
<p></p>
|
<p></p>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('creates a new list item in an empty paragraph within a non-empty list item', () => {
|
it('creates a new default block below a list when hitting Enter twice on an empty list item of the list', () => {
|
||||||
cy.clickUnorderedListButton()
|
cy.clickUnorderedListButton()
|
||||||
.type('foo')
|
.type('foo')
|
||||||
.enter({ times: 2 })
|
.enter({ times: 2 })
|
||||||
.confirmMarkdownEditorContent(`
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<p>foo</p>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<p></p>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
`)
|
|
||||||
.type('bar')
|
|
||||||
.enter()
|
|
||||||
.confirmMarkdownEditorContent(`
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<p>foo</p>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<p>bar</p>
|
|
||||||
<p></p>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('creates a new block below list', () => {
|
|
||||||
cy.clickUnorderedListButton()
|
|
||||||
.type('foo')
|
|
||||||
.enter({ times: 3 })
|
|
||||||
.confirmMarkdownEditorContent(`
|
.confirmMarkdownEditorContent(`
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
@ -476,24 +483,10 @@ describe('Markdown widget', () => {
|
|||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('removes empty block in non-empty list item', () => {
|
|
||||||
cy.clickUnorderedListButton()
|
|
||||||
.type('foo')
|
|
||||||
.enter()
|
|
||||||
.backspace()
|
|
||||||
.confirmMarkdownEditorContent(`
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<p>foo</p>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('removes the list item if list not empty', () => {
|
it('removes the list item if list not empty', () => {
|
||||||
cy.clickUnorderedListButton()
|
cy.clickUnorderedListButton()
|
||||||
.type('foo')
|
.type('foo')
|
||||||
.enter({ times: 2 })
|
.enter()
|
||||||
.backspace()
|
.backspace()
|
||||||
.confirmMarkdownEditorContent(`
|
.confirmMarkdownEditorContent(`
|
||||||
<ul>
|
<ul>
|
||||||
@ -544,7 +537,7 @@ describe('Markdown widget', () => {
|
|||||||
it('indents nested list items', () => {
|
it('indents nested list items', () => {
|
||||||
cy.clickUnorderedListButton()
|
cy.clickUnorderedListButton()
|
||||||
.type('foo')
|
.type('foo')
|
||||||
.enter({ times: 2 })
|
.enter()
|
||||||
.type('bar')
|
.type('bar')
|
||||||
.tabkey()
|
.tabkey()
|
||||||
.confirmMarkdownEditorContent(`
|
.confirmMarkdownEditorContent(`
|
||||||
@ -559,7 +552,7 @@ describe('Markdown widget', () => {
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
`)
|
`)
|
||||||
.enter({ times: 2 })
|
.enter()
|
||||||
.tabkey()
|
.tabkey()
|
||||||
.confirmMarkdownEditorContent(`
|
.confirmMarkdownEditorContent(`
|
||||||
<ul>
|
<ul>
|
||||||
@ -583,8 +576,8 @@ describe('Markdown widget', () => {
|
|||||||
it('only nests up to one level down from the parent list', () => {
|
it('only nests up to one level down from the parent list', () => {
|
||||||
cy.clickUnorderedListButton()
|
cy.clickUnorderedListButton()
|
||||||
.type('foo')
|
.type('foo')
|
||||||
.enter({ times: 2 })
|
.enter()
|
||||||
.tabkey({ times: 5 })
|
.tabkey()
|
||||||
.confirmMarkdownEditorContent(`
|
.confirmMarkdownEditorContent(`
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
@ -602,7 +595,7 @@ describe('Markdown widget', () => {
|
|||||||
it('unindents nested list items with shift', () => {
|
it('unindents nested list items with shift', () => {
|
||||||
cy.clickUnorderedListButton()
|
cy.clickUnorderedListButton()
|
||||||
.type('foo')
|
.type('foo')
|
||||||
.enter({ times: 2 })
|
.enter()
|
||||||
.tabkey()
|
.tabkey()
|
||||||
.tabkey({ shift: true })
|
.tabkey({ shift: true })
|
||||||
.confirmMarkdownEditorContent(`
|
.confirmMarkdownEditorContent(`
|
||||||
@ -620,10 +613,10 @@ describe('Markdown widget', () => {
|
|||||||
it('indents and unindents from one level below parent back to document root', () => {
|
it('indents and unindents from one level below parent back to document root', () => {
|
||||||
cy.clickUnorderedListButton()
|
cy.clickUnorderedListButton()
|
||||||
.type('foo')
|
.type('foo')
|
||||||
.enter({ times: 2 })
|
.enter()
|
||||||
.tabkey()
|
.tabkey()
|
||||||
.type('bar')
|
.type('bar')
|
||||||
.enter({ times: 2 })
|
.enter()
|
||||||
.tabkey()
|
.tabkey()
|
||||||
.type('baz')
|
.type('baz')
|
||||||
.confirmMarkdownEditorContent(`
|
.confirmMarkdownEditorContent(`
|
||||||
|
@ -59,8 +59,8 @@ describe('Markdown widget', () => {
|
|||||||
.clickUnorderedListButton()
|
.clickUnorderedListButton()
|
||||||
.clickHeadingOneButton()
|
.clickHeadingOneButton()
|
||||||
.type('foo')
|
.type('foo')
|
||||||
.enter({ times: 3 })
|
.enter({ times: 2 }) // First Enter creates new list item. Second Enter turns that list item into a default block.
|
||||||
.clickQuoteButton()
|
.clickQuoteButton() // Unwrap the quote block.
|
||||||
.confirmMarkdownEditorContent(`
|
.confirmMarkdownEditorContent(`
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
@ -126,7 +126,7 @@ describe('Markdown widget', () => {
|
|||||||
cy.focused()
|
cy.focused()
|
||||||
.clickUnorderedListButton()
|
.clickUnorderedListButton()
|
||||||
.type('foo')
|
.type('foo')
|
||||||
.enter({ times: 2 })
|
.enter()
|
||||||
.type('bar')
|
.type('bar')
|
||||||
.setSelection('foo', 'bar')
|
.setSelection('foo', 'bar')
|
||||||
.clickQuoteButton()
|
.clickQuoteButton()
|
||||||
@ -154,7 +154,7 @@ describe('Markdown widget', () => {
|
|||||||
</ul>
|
</ul>
|
||||||
`)
|
`)
|
||||||
.setCursorAfter('bar')
|
.setCursorAfter('bar')
|
||||||
.enter({ times: 2 })
|
.enter()
|
||||||
.type('baz')
|
.type('baz')
|
||||||
.setSelection('bar', 'baz')
|
.setSelection('bar', 'baz')
|
||||||
.clickQuoteButton()
|
.clickQuoteButton()
|
||||||
@ -185,6 +185,38 @@ describe('Markdown widget', () => {
|
|||||||
.clickUnorderedListButton()
|
.clickUnorderedListButton()
|
||||||
.clickQuoteButton()
|
.clickQuoteButton()
|
||||||
.type('foo')
|
.type('foo')
|
||||||
|
// Content should contains 4 <blockquote> tags and 3 <ul> tags
|
||||||
|
.confirmMarkdownEditorContent(`
|
||||||
|
<blockquote>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<blockquote>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<blockquote>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<blockquote>
|
||||||
|
<p>foo</p>
|
||||||
|
</blockquote>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</blockquote>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</blockquote>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</blockquote>
|
||||||
|
`)
|
||||||
|
/*
|
||||||
|
* First Enter creates new paragraph within the innermost block quote.
|
||||||
|
* Second Enter moves that paragraph one level up to become sibling of the previous quote block and direct child of a list item.
|
||||||
|
* Third Enter to turn that paragraph into a list item and move it one level up.
|
||||||
|
* Repeat the circle for three more times to reach the second list item of the outermost list block.
|
||||||
|
* Then Enter again to turn that list item into a paragraph and move it one level up to become sibling of the outermost list and
|
||||||
|
* direct child of the outermost block quote.
|
||||||
|
*/
|
||||||
.enter({ times: 10 })
|
.enter({ times: 10 })
|
||||||
.type('bar')
|
.type('bar')
|
||||||
.confirmMarkdownEditorContent(`
|
.confirmMarkdownEditorContent(`
|
||||||
@ -211,7 +243,16 @@ describe('Markdown widget', () => {
|
|||||||
<p>bar</p>
|
<p>bar</p>
|
||||||
</blockquote>
|
</blockquote>
|
||||||
`)
|
`)
|
||||||
.backspace({ times: 12 })
|
/* The GOAL is to delete all the text content inside this deeply nested block quote and turn it into a default paragraph block on top level.
|
||||||
|
* We need:
|
||||||
|
* 3 Backspace to delete the word “bar”.
|
||||||
|
* 1 Backspace to remove the paragraph that contains bar and bring cursor to the end of the unordered list which is direct child of the outermost block quote.
|
||||||
|
* 3 Backspace to remove the word “foo”.
|
||||||
|
* 1 Backspace to remove the current block quote that the cursor is on, 1 Backspace to remove the list that wraps the block quote. Repeat this step for three times for a total of 6 Backspace until the cursor is on the outermost block quote.
|
||||||
|
* 1 Backspace to remove to toggle off the outermost block quote and turn it into a default paragraph.
|
||||||
|
* Total Backspaces required: 3 + 1 + 3 + ((1 + 1) * 3) + 1 = 14
|
||||||
|
*/
|
||||||
|
.backspace({ times: 14 })
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -268,9 +268,42 @@ function ListPlugin({ defaultType, unorderedListType, orderedListType }) {
|
|||||||
|
|
||||||
if (listOrListItem.type === 'list-item') {
|
if (listOrListItem.type === 'list-item') {
|
||||||
const listItem = listOrListItem;
|
const listItem = listOrListItem;
|
||||||
|
const {
|
||||||
|
value: { document },
|
||||||
|
} = editor;
|
||||||
// If focus is at start of list item, unwrap the entire list item.
|
// If focus is at start of list item, unwrap the entire list item.
|
||||||
if (editor.atStartOf(listItem)) {
|
if (editor.atStartOf(listItem)) {
|
||||||
|
/* If the list item in question has a grandparent list, this means it is a child of a nested list.
|
||||||
|
* Hitting Enter key on an empty nested list item like this should move that list item out of the nested list
|
||||||
|
* and into the grandparent list. The targeted list item becomes direct child of its grandparent list
|
||||||
|
* Example
|
||||||
|
* <ul> ----- GRANDPARENT LIST
|
||||||
|
* <li> ------ GRANDPARENT LIST ITEM
|
||||||
|
* <p>foo</p>
|
||||||
|
* <ul> ----- PARENT LIST
|
||||||
|
* <li>
|
||||||
|
* <p>bar</p>
|
||||||
|
* </li>
|
||||||
|
* <li> ------ LIST ITEM
|
||||||
|
* <p></p> ----- WHERE THE ENTER KEY HAPPENS
|
||||||
|
* </li>
|
||||||
|
* </ul>
|
||||||
|
* </li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
const parentList = document.getParent(listItem.key);
|
||||||
|
const grandparentListItem = document.getParent(parentList.key);
|
||||||
|
if (grandparentListItem.type === 'list-item') {
|
||||||
|
const grandparentList = document.getParent(grandparentListItem.key);
|
||||||
|
const indexOfGrandparentListItem = grandparentList.nodes.findIndex(
|
||||||
|
node => node.key === grandparentListItem.key,
|
||||||
|
);
|
||||||
|
return editor.moveNodeByKey(
|
||||||
|
listItem.key,
|
||||||
|
grandparentList.key,
|
||||||
|
indexOfGrandparentListItem + 1,
|
||||||
|
);
|
||||||
|
}
|
||||||
return editor.unwrapListItem(listItem);
|
return editor.unwrapListItem(listItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -285,7 +318,23 @@ function ListPlugin({ defaultType, unorderedListType, orderedListType }) {
|
|||||||
editor.wrapBlockAtRange(range, newListItem).unwrapNodeByKey(newListItem.key);
|
editor.wrapBlockAtRange(range, newListItem).unwrapNodeByKey(newListItem.key);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
const list = document.getParent(listItem.key);
|
||||||
|
if (LIST_TYPES.includes(list.type)) {
|
||||||
|
const newListItem = Block.create({
|
||||||
|
type: 'list-item',
|
||||||
|
nodes: [Block.create('paragraph')],
|
||||||
|
});
|
||||||
|
// Check if the targeted list item contains a nested list. If it does, insert a new list item in the beginning of that nested list.
|
||||||
|
const nestedList = listItem.findDescendant(block => LIST_TYPES.includes(block.type));
|
||||||
|
if (nestedList) {
|
||||||
|
return editor.insertNodeByKey(nestedList.key, 0, newListItem).moveForward(1); // Each list item is separated by a \n character. We need to move the cursor past this character so that it'd be on new list item that has just been inserted
|
||||||
|
}
|
||||||
|
// Find index of the list item block that receives Enter key
|
||||||
|
const previousListItemIndex = list.nodes.findIndex(block => block.key === listItem.key);
|
||||||
|
return editor
|
||||||
|
.insertNodeByKey(list.key, previousListItemIndex + 1, newListItem) // insert a new list item after the list item above
|
||||||
|
.moveForward(1);
|
||||||
|
}
|
||||||
return next();
|
return next();
|
||||||
} else {
|
} else {
|
||||||
const list = listOrListItem;
|
const list = listOrListItem;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user