mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Branch merge - 'main' into '1.8.x'.
This commit is contained in:
@@ -85,7 +85,7 @@
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<form on:submit|preventDefault={addFilterAndReset}>
|
||||
<form onsubmit={addFilterAndReset}>
|
||||
<Layout.Stack gap="s" direction="row" alignItems="flex-start">
|
||||
<InputSelect
|
||||
id="column"
|
||||
|
||||
@@ -17,13 +17,7 @@
|
||||
<header class="grid-header">
|
||||
<Typography.Title size="m">{title}</Typography.Title>
|
||||
<div class="u-flex u-gap-16 u-contents-mobile">
|
||||
<ViewSelector
|
||||
{view}
|
||||
{columns}
|
||||
{isCustomTable}
|
||||
{hideView}
|
||||
{hideColumns}
|
||||
{allowNoColumns} />
|
||||
<ViewSelector {view} {columns} {isCustomTable} {hideView} {hideColumns} {allowNoColumns} />
|
||||
<div class="grid-header-col-2">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
+2
-2
@@ -104,9 +104,9 @@
|
||||
bind:value={data.default} />
|
||||
{/if}
|
||||
<InputChoice id="required" label="Required" bind:value={data.required} disabled={data.array}>
|
||||
Indicate whether this is a required attribute
|
||||
Indicate whether this is a required column
|
||||
</InputChoice>
|
||||
<InputChoice id="array" label="Array" bind:value={data.array} disabled={data.required || editing}>
|
||||
Indicate whether this attribute should act as an array, with the default value set as an empty
|
||||
Indicate whether this column should act as an array, with the default value set as an empty
|
||||
array.
|
||||
</InputChoice>
|
||||
|
||||
+5
-5
@@ -2,13 +2,13 @@
|
||||
import { writable } from 'svelte/store';
|
||||
import type { Option } from './columns/store';
|
||||
|
||||
const createAttributeArgs = writable({
|
||||
const createColumnArgs = writable({
|
||||
showCreate: false,
|
||||
selectedOption: null as Option['name'] | null
|
||||
});
|
||||
|
||||
export const initCreateColumn = (option: Option['name']) => {
|
||||
createAttributeArgs.set({ showCreate: true, selectedOption: option });
|
||||
createColumnArgs.set({ showCreate: true, selectedOption: option });
|
||||
};
|
||||
|
||||
const showCreateIndex = writable(false);
|
||||
@@ -24,7 +24,7 @@
|
||||
import { onMount } from 'svelte';
|
||||
import { table } from './store';
|
||||
import { addSubPanel, registerCommands, updateCommandGroupRanks } from '$lib/commandCenter';
|
||||
import CreateAttribute from './createColumn.svelte';
|
||||
import CreateColumn from './createColumn.svelte';
|
||||
import { CreateColumnPanel } from '$lib/commandCenter/panels';
|
||||
import { database } from '../store';
|
||||
import { project } from '$routes/(console)/project-[region]-[project]/store';
|
||||
@@ -204,8 +204,8 @@
|
||||
|
||||
<slot />
|
||||
|
||||
{#if $createAttributeArgs.showCreate}
|
||||
<CreateAttribute {...$createAttributeArgs} />
|
||||
{#if $createColumnArgs.showCreate}
|
||||
<CreateColumn {...$createColumnArgs} />
|
||||
{/if}
|
||||
|
||||
{#if $showCreateIndex}
|
||||
|
||||
+2
-2
@@ -93,11 +93,11 @@
|
||||
label="Required"
|
||||
bind:checked={data.required}
|
||||
disabled={data.array}
|
||||
description="Indicate whether this attribute is required" />
|
||||
description="Indicate whether this column is required" />
|
||||
<Selector.Checkbox
|
||||
size="s"
|
||||
id="array"
|
||||
label="Array"
|
||||
bind:checked={data.array}
|
||||
disabled={data.required || editing}
|
||||
description="Indicate whether this attribute is an array. Defaults to an empty array." />
|
||||
description="Indicate whether this column is an array. Defaults to an empty array." />
|
||||
|
||||
+6
-6
@@ -14,26 +14,26 @@
|
||||
<CsvDisabled>
|
||||
<Button size="s" disabled>
|
||||
<Icon icon={IconPlus} slot="start" size="s" />
|
||||
Create attribute
|
||||
Create column
|
||||
</Button>
|
||||
</CsvDisabled>
|
||||
{:else}
|
||||
<Popover let:toggle padding="none" placement="bottom-start">
|
||||
<slot {toggle}>
|
||||
<Button on:click={toggle} event="create_attribute">
|
||||
<Button on:click={toggle} event="create_column">
|
||||
<Icon icon={IconPlus} slot="start" size="s" />
|
||||
Create column
|
||||
</Button>
|
||||
</slot>
|
||||
<ActionMenu.Root slot="tooltip">
|
||||
{#each columnOptions as attribute}
|
||||
{#each columnOptions as column}
|
||||
<ActionMenu.Item.Button
|
||||
leadingIcon={attribute.icon}
|
||||
leadingIcon={column.icon}
|
||||
on:click={() => {
|
||||
selectedOption = attribute.name;
|
||||
selectedOption = column.name;
|
||||
showCreate = true;
|
||||
}}>
|
||||
{attribute.name}
|
||||
{column.name}
|
||||
</ActionMenu.Item.Button>
|
||||
{/each}
|
||||
</ActionMenu.Root>
|
||||
|
||||
+2
-2
@@ -85,11 +85,11 @@
|
||||
label="Required"
|
||||
bind:checked={data.required}
|
||||
disabled={data.array}
|
||||
description="Indicate whether this attribute is required" />
|
||||
description="Indicate whether this column is required" />
|
||||
<Selector.Checkbox
|
||||
size="s"
|
||||
id="array"
|
||||
label="Array"
|
||||
bind:checked={data.array}
|
||||
disabled={data.required || editing}
|
||||
description="Indicate whether this attribute is an array. Defaults to an empty array." />
|
||||
description="Indicate whether this column is an array. Defaults to an empty array." />
|
||||
|
||||
+1
-1
@@ -53,7 +53,7 @@
|
||||
}
|
||||
|
||||
$: onShow(showEdit);
|
||||
$: title = `Update ${columnOptions.find((v) => v.name === option.name)?.sentenceName ?? ''} attribute`;
|
||||
$: title = `Update ${columnOptions.find((v) => v.name === option.name)?.sentenceName ?? ''} column`;
|
||||
|
||||
function onShow(show: boolean) {
|
||||
if (show) {
|
||||
|
||||
+2
-2
@@ -86,11 +86,11 @@
|
||||
label="Required"
|
||||
bind:checked={data.required}
|
||||
disabled={data.array}
|
||||
description="Indicate whether this attribute is required" />
|
||||
description="Indicate whether this column is required" />
|
||||
<Selector.Checkbox
|
||||
size="s"
|
||||
id="array"
|
||||
label="Array"
|
||||
bind:checked={data.array}
|
||||
disabled={data.required || editing}
|
||||
description="Indicate whether this attribute is an array. Defaults to an empty array." />
|
||||
description="Indicate whether this column is an array. Defaults to an empty array." />
|
||||
|
||||
+2
-2
@@ -117,11 +117,11 @@
|
||||
label="Required"
|
||||
bind:checked={data.required}
|
||||
disabled={data.array}
|
||||
description="Indicate whether this attribute is required" />
|
||||
description="Indicate whether this column is required" />
|
||||
<Selector.Checkbox
|
||||
size="s"
|
||||
id="array"
|
||||
label="Array"
|
||||
bind:checked={data.array}
|
||||
disabled={data.required || editing}
|
||||
description="Indicate whether this attribute is an array. Defaults to an empty array." />
|
||||
description="Indicate whether this column is an array. Defaults to an empty array." />
|
||||
|
||||
+2
-2
@@ -115,11 +115,11 @@
|
||||
label="Required"
|
||||
bind:checked={data.required}
|
||||
disabled={data.array}
|
||||
description="Indicate whether this attribute is required" />
|
||||
description="Indicate whether this column is required" />
|
||||
<Selector.Checkbox
|
||||
size="s"
|
||||
id="array"
|
||||
label="Array"
|
||||
bind:checked={data.array}
|
||||
disabled={data.required || editing}
|
||||
description="Indicate whether this attribute is an array. Defaults to an empty array." />
|
||||
description="Indicate whether this column is an array. Defaults to an empty array." />
|
||||
|
||||
+2
-2
@@ -113,11 +113,11 @@
|
||||
label="Required"
|
||||
bind:checked={data.required}
|
||||
disabled={data.array}
|
||||
description="Indicate whether this attribute is required" />
|
||||
description="Indicate whether this column is required" />
|
||||
<Selector.Checkbox
|
||||
size="s"
|
||||
id="array"
|
||||
label="Array"
|
||||
bind:checked={data.array}
|
||||
disabled={data.required || editing}
|
||||
description="Indicate whether this attribute is an array. Defaults to an empty array." />
|
||||
description="Indicate whether this column is an array. Defaults to an empty array." />
|
||||
|
||||
+2
-2
@@ -85,11 +85,11 @@
|
||||
label="Required"
|
||||
bind:checked={data.required}
|
||||
disabled={data.array}
|
||||
description="Indicate whether this attribute is required" />
|
||||
description="Indicate whether this column is required" />
|
||||
<Selector.Checkbox
|
||||
size="s"
|
||||
id="array"
|
||||
label="Array"
|
||||
bind:checked={data.array}
|
||||
disabled={data.required || editing}
|
||||
description="Indicate whether this attribute is an array. Defaults to an empty array." />
|
||||
description="Indicate whether this column is an array. Defaults to an empty array." />
|
||||
|
||||
+2
-2
@@ -108,8 +108,8 @@
|
||||
|
||||
function updateKeyName() {
|
||||
if (!editing) {
|
||||
const collection = tableList.tables.find((n) => n.$id === data.relatedTable);
|
||||
data.key = camelize(collection.name);
|
||||
const table = tableList.tables.find((n) => n.$id === data.relatedTable);
|
||||
data.key = camelize(table.name);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -159,8 +159,8 @@
|
||||
</Layout.Stack>
|
||||
</button>
|
||||
<Typography.Text color="--fgcolor-neutral-tertiary">
|
||||
Protect attribute against data leaks for best privacy compliance. Encrypted
|
||||
attributes cannot be queried.
|
||||
Protect column against data leaks for best privacy compliance. Encrypted
|
||||
columns cannot be queried.
|
||||
</Typography.Text>
|
||||
</Layout.Stack>
|
||||
|
||||
|
||||
+2
-2
@@ -86,11 +86,11 @@
|
||||
label="Required"
|
||||
bind:checked={data.required}
|
||||
disabled={data.array}
|
||||
description="Indicate whether this attribute is required" />
|
||||
description="Indicate whether this column is required" />
|
||||
<Selector.Checkbox
|
||||
size="s"
|
||||
id="array"
|
||||
label="Array"
|
||||
bind:checked={data.array}
|
||||
disabled={data.required || editing}
|
||||
description="Indicate whether this attribute is an array. Defaults to an empty array." />
|
||||
description="Indicate whether this column is an array. Defaults to an empty array." />
|
||||
|
||||
+3
-3
@@ -22,7 +22,7 @@
|
||||
let formComponent: Form;
|
||||
let isSubmitting = writable(false);
|
||||
|
||||
type CreateDocument = {
|
||||
type CreateRow = {
|
||||
id?: string;
|
||||
row: object;
|
||||
permissions: string[];
|
||||
@@ -44,7 +44,7 @@
|
||||
columns: availableColumns
|
||||
};
|
||||
|
||||
return writable<CreateDocument>({ ...initial });
|
||||
return writable<CreateRow>({ ...initial });
|
||||
}
|
||||
|
||||
const createRow = createRowWritable();
|
||||
@@ -66,7 +66,7 @@
|
||||
type: 'success'
|
||||
});
|
||||
trackEvent(Submit.RowCreate, {
|
||||
customId: !!$createRow.id // todo: @itznotabug - change store name
|
||||
customId: !!$createRow.id
|
||||
});
|
||||
goto(
|
||||
`${base}/project-${page.params.region}-${page.params.project}/databases/database-${page.params.database}/table-${page.params.table}/row-${$id}`
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@
|
||||
import { Table } from '@appwrite.io/pink-svelte';
|
||||
|
||||
export let show = false;
|
||||
export let data: Partial<Models.Document>[];
|
||||
export let data: Partial<Models.Row>[];
|
||||
export let selectedRelationship: Models.ColumnRelationship = null;
|
||||
const databaseId = page.params.database;
|
||||
const limit = 10;
|
||||
|
||||
+1
-1
@@ -7,8 +7,8 @@ import type { Columns } from '../store';
|
||||
import { buildWildcardColumnsQuery } from './columns/store';
|
||||
|
||||
export const load: LayoutLoad = async ({ params, parent, depends }) => {
|
||||
depends(Dependencies.ROW);
|
||||
const { table } = await parent();
|
||||
depends(Dependencies.ROW);
|
||||
|
||||
const row = await sdk
|
||||
.forProject(params.region, params.project)
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { type Models, Query } from '@appwrite.io/console';
|
||||
import type { Columns } from '../../store';
|
||||
import { type Models, Query } from '@appwrite.io/console';
|
||||
|
||||
export function isRelationshipToMany(column: Models.ColumnRelationship) {
|
||||
if (!column) return false;
|
||||
|
||||
+14
-14
@@ -45,17 +45,17 @@
|
||||
'restrict' = 'Row cannot be deleted'
|
||||
}
|
||||
|
||||
$: relColumns = $columns?.filter(
|
||||
(attribute) =>
|
||||
isRelationship(attribute) &&
|
||||
$: relatedColumns = $columns?.filter(
|
||||
(column) =>
|
||||
isRelationship(column) &&
|
||||
// One-to-One are always included
|
||||
(attribute.relationType === 'oneToOne' ||
|
||||
(column.relationType === 'oneToOne' ||
|
||||
// One-to-Many: Only if parent is deleted
|
||||
(attribute.relationType === 'oneToMany' && attribute.side === 'parent') ||
|
||||
(column.relationType === 'oneToMany' && column.side === 'parent') ||
|
||||
// Many-to-One: Only include if child is deleted
|
||||
(attribute.relationType === 'manyToOne' && attribute.side === 'child') ||
|
||||
(column.relationType === 'manyToOne' && column.side === 'child') ||
|
||||
// Many-to-Many: Only include if the parent is being deleted
|
||||
(isRelationshipToMany(attribute) && attribute.side === 'parent'))
|
||||
(isRelationshipToMany(column) && column.side === 'parent'))
|
||||
) as Models.ColumnRelationship[];
|
||||
</script>
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
Are you sure you want to delete <b>the row from <span data-private>{$table.name}</span></b>?
|
||||
</p>
|
||||
|
||||
{#if relColumns?.length}
|
||||
{#if relatedColumns?.length}
|
||||
<p class="text">This row contains the following relationships:</p>
|
||||
<Table.Root
|
||||
let:root
|
||||
@@ -78,23 +78,23 @@
|
||||
<Table.Header.Cell column="setting" {root}>Setting</Table.Header.Cell>
|
||||
<Table.Header.Cell column="desc" {root} />
|
||||
</svelte:fragment>
|
||||
{#each relColumns as attr}
|
||||
{#each relatedColumns as column}
|
||||
<Table.Row.Base {root}>
|
||||
<Table.Cell column="relations" {root}>
|
||||
<span class="u-flex u-cross-center u-gap-8">
|
||||
{#if attr.twoWay}
|
||||
{#if column.twoWay}
|
||||
<span class="icon-switch-horizontal"></span>
|
||||
{:else}
|
||||
<span class="icon-arrow-sm-right"></span>
|
||||
{/if}
|
||||
<Trim>{attr.key}</Trim>
|
||||
<Trim>{column.key}</Trim>
|
||||
</span>
|
||||
</Table.Cell>
|
||||
<Table.Cell column="setting" {root}>
|
||||
{attr.onDelete}
|
||||
{column.onDelete}
|
||||
</Table.Cell>
|
||||
<Table.Cell column="desc" {root}>
|
||||
{Deletion[attr.onDelete]}
|
||||
{Deletion[column.onDelete]}
|
||||
</Table.Cell>
|
||||
</Table.Row.Base>
|
||||
{/each}
|
||||
@@ -109,6 +109,6 @@
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
<Button text on:click={() => (showDelete = false)}>Cancel</Button>
|
||||
<Button danger submit disabled={relColumns?.length && !checked}>Delete</Button>
|
||||
<Button danger submit disabled={relatedColumns?.length && !checked}>Delete</Button>
|
||||
</svelte:fragment>
|
||||
</Confirm>
|
||||
|
||||
+3
-3
@@ -64,7 +64,7 @@
|
||||
}));
|
||||
}
|
||||
|
||||
const addAttributeDisabled = $derived(
|
||||
const addColumnDisabled = $derived(
|
||||
names?.length >= 5 || (names?.length && !names[names?.length - 1])
|
||||
);
|
||||
|
||||
@@ -117,12 +117,12 @@
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
<!-- show only when options don't have all the attributes -->
|
||||
<!-- show only when options don't have all the columns -->
|
||||
{#if !hasExhaustedOptions}
|
||||
<div>
|
||||
<Button
|
||||
compact
|
||||
disabled={addAttributeDisabled}
|
||||
disabled={addColumnDisabled}
|
||||
on:click={() => {
|
||||
names[names.length] = null;
|
||||
names = names;
|
||||
|
||||
+2
-2
@@ -19,7 +19,7 @@
|
||||
enabled ??= $table.enabled;
|
||||
});
|
||||
|
||||
async function toggleCollection() {
|
||||
async function toggleTable() {
|
||||
try {
|
||||
await sdk
|
||||
.forProject(page.params.region, page.params.project)
|
||||
@@ -63,6 +63,6 @@
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<Button disabled={enabled === $table.enabled} on:click={toggleCollection}>Update</Button>
|
||||
<Button disabled={enabled === $table.enabled} on:click={toggleTable}>Update</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
|
||||
+4
-4
@@ -1,7 +1,7 @@
|
||||
import { page } from '$app/stores';
|
||||
import { derived, writable } from 'svelte/store';
|
||||
import type { Column } from '$lib/helpers/types';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import type { Column as TableColumn } from '$lib/helpers/types';
|
||||
import { derived, writable } from 'svelte/store';
|
||||
|
||||
export type Columns =
|
||||
| Models.ColumnBoolean
|
||||
@@ -20,8 +20,8 @@ type Table = Omit<Models.Table, 'columns'> & {
|
||||
|
||||
export const table = derived(page, ($page) => $page.data.table as Table);
|
||||
export const columns = derived(page, ($page) => $page.data.table.columns as Columns[]);
|
||||
export const indexes = derived(page, ($page) => $page.data.table.indexes as Models.ColumnIndex[]);
|
||||
export const indexes = derived(page, ($page) => $page.data.table.indexes as Models.Index[]);
|
||||
|
||||
export const tableColumns = writable<TableColumn[]>([]);
|
||||
export const tableColumns = writable<Column[]>([]);
|
||||
|
||||
export const isCsvImportInProgress = writable(false);
|
||||
|
||||
+4
-6
@@ -24,9 +24,7 @@
|
||||
data?.allTables?.tables?.slice().sort((a, b) => a.name.localeCompare(b.name))
|
||||
);
|
||||
|
||||
const selectedTable = $derived.by(() =>
|
||||
sortedTables?.find((collection) => collection.$id === tableId)
|
||||
);
|
||||
const selectedTable = $derived.by(() => sortedTables?.find((table) => table.$id === tableId));
|
||||
|
||||
let openBottomSheet = $state(false);
|
||||
|
||||
@@ -111,11 +109,11 @@
|
||||
bind:isOpen={openBottomSheet}
|
||||
menu={{
|
||||
top: {
|
||||
items: sortedTables.slice(0, 10).map((collection) => {
|
||||
items: sortedTables.slice(0, 10).map((table) => {
|
||||
return {
|
||||
name: collection.name,
|
||||
name: table.name,
|
||||
leadingIcon: IconTable,
|
||||
href: `${base}/project-${region}-${project}/databases/database-${databaseId}/table-${collection.$id}`
|
||||
href: `${base}/project-${region}-${project}/databases/database-${databaseId}/table-${table.$id}`
|
||||
};
|
||||
})
|
||||
},
|
||||
|
||||
+14
-14
@@ -37,7 +37,7 @@
|
||||
let displayNames = {};
|
||||
let showRelationships = false;
|
||||
let selectedRelationship: Models.ColumnRelationship = null;
|
||||
let relationshipData: Partial<Models.Document>[];
|
||||
let relationshipData: Partial<Models.Row>[];
|
||||
|
||||
onMount(async () => {
|
||||
displayNames = preferences.getDisplayNames();
|
||||
@@ -135,17 +135,17 @@
|
||||
'restrict' = 'Row cannot be deleted'
|
||||
}
|
||||
|
||||
$: relAttributes = $columns?.filter(
|
||||
(attribute) =>
|
||||
isRelationship(attribute) &&
|
||||
$: relatedColumns = $columns?.filter(
|
||||
(column) =>
|
||||
isRelationship(column) &&
|
||||
// One-to-One are always included
|
||||
(attribute.relationType === 'oneToOne' ||
|
||||
(column.relationType === 'oneToOne' ||
|
||||
// One-to-Many: Only if parent is deleted
|
||||
(attribute.relationType === 'oneToMany' && attribute.side === 'parent') ||
|
||||
(column.relationType === 'oneToMany' && column.side === 'parent') ||
|
||||
// Many-to-One: Only include if child is deleted
|
||||
(attribute.relationType === 'manyToOne' && attribute.side === 'child') ||
|
||||
(column.relationType === 'manyToOne' && column.side === 'child') ||
|
||||
// Many-to-Many: Only include if the parent is being deleted
|
||||
(isRelationshipToMany(attribute) && attribute.side === 'parent'))
|
||||
(isRelationshipToMany(column) && column.side === 'parent'))
|
||||
) as Models.ColumnRelationship[];
|
||||
|
||||
let checked = false;
|
||||
@@ -233,14 +233,14 @@
|
||||
{:else}
|
||||
{@const datetime = row[id]}
|
||||
{@const formatted = formatColumn(row[id])}
|
||||
{@const isDatetimeAttribute = column.type === 'datetime'}
|
||||
{@const isEncryptedAttribute = isString(column) && column.encrypt}
|
||||
{#if isDatetimeAttribute}
|
||||
{@const isDatetimeColumn = column.type === 'datetime'}
|
||||
{@const isEncryptedColumn = isString(column) && column.encrypt}
|
||||
{#if isDatetimeColumn}
|
||||
<DualTimeView time={datetime}>
|
||||
<span slot="title">Timestamp</span>
|
||||
{toLocaleDateTime(datetime, true)}
|
||||
</DualTimeView>
|
||||
{:else if isEncryptedAttribute}
|
||||
{:else if isEncryptedColumn}
|
||||
<button on:click={(e) => e.preventDefault()}>
|
||||
<InteractiveText
|
||||
copy={false}
|
||||
@@ -303,7 +303,7 @@
|
||||
Are you sure you want to delete <b>{selectedRows.length}</b>
|
||||
{selectedRows.length > 1 ? 'rows' : 'row'}?
|
||||
|
||||
{#if relAttributes?.length}
|
||||
{#if relatedColumns?.length}
|
||||
<Table.Root
|
||||
let:root
|
||||
columns={[
|
||||
@@ -316,7 +316,7 @@
|
||||
<Table.Header.Cell column="setting" {root}>Setting</Table.Header.Cell>
|
||||
<Table.Header.Cell column="desc" {root} />
|
||||
</svelte:fragment>
|
||||
{#each relAttributes as attr}
|
||||
{#each relatedColumns as attr}
|
||||
<Table.Row.Base {root}>
|
||||
<Table.Cell column="relation" {root}>
|
||||
<span class="u-flex u-cross-center u-gap-8">
|
||||
|
||||
Reference in New Issue
Block a user