feat(ui): toggle sortable arrays and blocks (#6008)
This commit is contained in:
@@ -57,6 +57,7 @@ In addition to the default [field admin config](/docs/fields/overview#admin-conf
|
|||||||
| ------------------------- | -------------------------------------------------------------------------------------------------------------------- |
|
| ------------------------- | -------------------------------------------------------------------------------------------------------------------- |
|
||||||
| **`initCollapsed`** | Set the initial collapsed state |
|
| **`initCollapsed`** | Set the initial collapsed state |
|
||||||
| **`components.RowLabel`** | Function or React component to be rendered as the label on the array row. Receives `({ data, index, path })` as args |
|
| **`components.RowLabel`** | Function or React component to be rendered as the label on the array row. Receives `({ data, index, path })` as args |
|
||||||
|
| **`isSortable`** | Disable order sorting by setting this value to `false` |
|
||||||
|
|
||||||
### Example
|
### Example
|
||||||
|
|
||||||
|
|||||||
@@ -53,9 +53,10 @@ _\* An asterisk denotes that a property is required._
|
|||||||
|
|
||||||
In addition to the default [field admin config](/docs/fields/overview#admin-config), you can adjust the following properties:
|
In addition to the default [field admin config](/docs/fields/overview#admin-config), you can adjust the following properties:
|
||||||
|
|
||||||
| Option | Description |
|
| Option | Description |
|
||||||
| ------------------- | ------------------------------- |
|
| ------------------- | ---------------------------------- |
|
||||||
| **`initCollapsed`** | Set the initial collapsed state |
|
| **`initCollapsed`** | Set the initial collapsed state |
|
||||||
|
| **`isSortable`** | Disable order sorting by setting this value to `false` |
|
||||||
|
|
||||||
### Block configs
|
### Block configs
|
||||||
|
|
||||||
|
|||||||
@@ -318,6 +318,7 @@ export const array = baseField.keys({
|
|||||||
RowLabel: componentSchema,
|
RowLabel: componentSchema,
|
||||||
})
|
})
|
||||||
.default({}),
|
.default({}),
|
||||||
|
isSortable: joi.boolean(),
|
||||||
})
|
})
|
||||||
.default({}),
|
.default({}),
|
||||||
dbName: joi.alternatives().try(joi.string(), joi.func()),
|
dbName: joi.alternatives().try(joi.string(), joi.func()),
|
||||||
@@ -415,6 +416,11 @@ export const relationship = baseField.keys({
|
|||||||
export const blocks = baseField.keys({
|
export const blocks = baseField.keys({
|
||||||
name: joi.string().required(),
|
name: joi.string().required(),
|
||||||
type: joi.string().valid('blocks').required(),
|
type: joi.string().valid('blocks').required(),
|
||||||
|
admin: baseAdminFields
|
||||||
|
.keys({
|
||||||
|
isSortable: joi.boolean(),
|
||||||
|
})
|
||||||
|
.default({}),
|
||||||
blocks: joi
|
blocks: joi
|
||||||
.array()
|
.array()
|
||||||
.items(
|
.items(
|
||||||
|
|||||||
@@ -613,6 +613,10 @@ export type ArrayField = FieldBase & {
|
|||||||
RowLabel?: RowLabel
|
RowLabel?: RowLabel
|
||||||
} & Admin['components']
|
} & Admin['components']
|
||||||
initCollapsed?: boolean
|
initCollapsed?: boolean
|
||||||
|
/**
|
||||||
|
* Disable drag and drop sorting
|
||||||
|
*/
|
||||||
|
isSortable?: boolean
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Customize the SQL table name
|
* Customize the SQL table name
|
||||||
@@ -684,6 +688,10 @@ export type Block = {
|
|||||||
export type BlockField = FieldBase & {
|
export type BlockField = FieldBase & {
|
||||||
admin?: Admin & {
|
admin?: Admin & {
|
||||||
initCollapsed?: boolean
|
initCollapsed?: boolean
|
||||||
|
/**
|
||||||
|
* Disable drag and drop sorting
|
||||||
|
*/
|
||||||
|
isSortable?: boolean
|
||||||
}
|
}
|
||||||
blocks: Block[]
|
blocks: Block[]
|
||||||
defaultValue?: unknown
|
defaultValue?: unknown
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export type Props = {
|
|||||||
duplicateRow: (current: number) => void
|
duplicateRow: (current: number) => void
|
||||||
hasMaxRows: boolean
|
hasMaxRows: boolean
|
||||||
index: number
|
index: number
|
||||||
|
isSortable?: boolean
|
||||||
moveRow: (from: number, to: number) => void
|
moveRow: (from: number, to: number) => void
|
||||||
removeRow: (index: number) => void
|
removeRow: (index: number) => void
|
||||||
rowCount: number
|
rowCount: number
|
||||||
@@ -27,6 +28,7 @@ export const ArrayAction: React.FC<Props> = ({
|
|||||||
duplicateRow,
|
duplicateRow,
|
||||||
hasMaxRows,
|
hasMaxRows,
|
||||||
index,
|
index,
|
||||||
|
isSortable,
|
||||||
moveRow,
|
moveRow,
|
||||||
removeRow,
|
removeRow,
|
||||||
rowCount,
|
rowCount,
|
||||||
@@ -42,7 +44,7 @@ export const ArrayAction: React.FC<Props> = ({
|
|||||||
render={({ close }) => {
|
render={({ close }) => {
|
||||||
return (
|
return (
|
||||||
<PopupList.ButtonGroup buttonSize="small">
|
<PopupList.ButtonGroup buttonSize="small">
|
||||||
{index !== 0 && (
|
{isSortable && index !== 0 && (
|
||||||
<PopupList.Button
|
<PopupList.Button
|
||||||
className={`${baseClass}__action ${baseClass}__move-up`}
|
className={`${baseClass}__action ${baseClass}__move-up`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -56,7 +58,7 @@ export const ArrayAction: React.FC<Props> = ({
|
|||||||
{t('general:moveUp')}
|
{t('general:moveUp')}
|
||||||
</PopupList.Button>
|
</PopupList.Button>
|
||||||
)}
|
)}
|
||||||
{index < rowCount - 1 && (
|
{isSortable && index < rowCount - 1 && (
|
||||||
<PopupList.Button
|
<PopupList.Button
|
||||||
className={`${baseClass}__action`}
|
className={`${baseClass}__action`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ type ArrayRowProps = UseDraggableSortableReturn & {
|
|||||||
forceRender?: boolean
|
forceRender?: boolean
|
||||||
hasMaxRows?: boolean
|
hasMaxRows?: boolean
|
||||||
indexPath: string
|
indexPath: string
|
||||||
|
isSortable?: boolean
|
||||||
labels: ArrayField['labels']
|
labels: ArrayField['labels']
|
||||||
moveRow: (fromIndex: number, toIndex: number) => void
|
moveRow: (fromIndex: number, toIndex: number) => void
|
||||||
path: string
|
path: string
|
||||||
@@ -50,6 +51,7 @@ export const ArrayRow: React.FC<ArrayRowProps> = ({
|
|||||||
forceRender = false,
|
forceRender = false,
|
||||||
hasMaxRows,
|
hasMaxRows,
|
||||||
indexPath,
|
indexPath,
|
||||||
|
isSortable,
|
||||||
labels,
|
labels,
|
||||||
listeners,
|
listeners,
|
||||||
moveRow,
|
moveRow,
|
||||||
@@ -100,6 +102,7 @@ export const ArrayRow: React.FC<ArrayRowProps> = ({
|
|||||||
duplicateRow={duplicateRow}
|
duplicateRow={duplicateRow}
|
||||||
hasMaxRows={hasMaxRows}
|
hasMaxRows={hasMaxRows}
|
||||||
index={rowIndex}
|
index={rowIndex}
|
||||||
|
isSortable={isSortable}
|
||||||
moveRow={moveRow}
|
moveRow={moveRow}
|
||||||
removeRow={removeRow}
|
removeRow={removeRow}
|
||||||
rowCount={rowCount}
|
rowCount={rowCount}
|
||||||
@@ -108,11 +111,15 @@ export const ArrayRow: React.FC<ArrayRowProps> = ({
|
|||||||
}
|
}
|
||||||
className={classNames}
|
className={classNames}
|
||||||
collapsibleStyle={fieldHasErrors ? 'error' : 'default'}
|
collapsibleStyle={fieldHasErrors ? 'error' : 'default'}
|
||||||
dragHandleProps={{
|
dragHandleProps={
|
||||||
id: row.id,
|
isSortable
|
||||||
attributes,
|
? {
|
||||||
listeners,
|
id: row.id,
|
||||||
}}
|
attributes,
|
||||||
|
listeners,
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
header={
|
header={
|
||||||
<div className={`${baseClass}__row-header`}>
|
<div className={`${baseClass}__row-header`}>
|
||||||
<RowLabel
|
<RowLabel
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export type ArrayFieldProps = FormFieldBase & {
|
|||||||
CustomRowLabel?: React.ReactNode
|
CustomRowLabel?: React.ReactNode
|
||||||
fieldMap: FieldMap
|
fieldMap: FieldMap
|
||||||
forceRender?: boolean
|
forceRender?: boolean
|
||||||
|
isSortable?: boolean
|
||||||
label?: FieldBase['label']
|
label?: FieldBase['label']
|
||||||
labels?: ArrayFieldType['labels']
|
labels?: ArrayFieldType['labels']
|
||||||
maxRows?: ArrayFieldType['maxRows']
|
maxRows?: ArrayFieldType['maxRows']
|
||||||
@@ -58,6 +59,7 @@ export const _ArrayField: React.FC<ArrayFieldProps> = (props) => {
|
|||||||
errorProps,
|
errorProps,
|
||||||
fieldMap,
|
fieldMap,
|
||||||
forceRender = false,
|
forceRender = false,
|
||||||
|
isSortable = true,
|
||||||
label,
|
label,
|
||||||
labelProps,
|
labelProps,
|
||||||
localized,
|
localized,
|
||||||
@@ -261,7 +263,7 @@ export const _ArrayField: React.FC<ArrayFieldProps> = (props) => {
|
|||||||
errorPath.startsWith(`${path}.${i}.`),
|
errorPath.startsWith(`${path}.${i}.`),
|
||||||
).length
|
).length
|
||||||
return (
|
return (
|
||||||
<DraggableSortableItem disabled={readOnly} id={row.id} key={row.id}>
|
<DraggableSortableItem disabled={readOnly || !isSortable} id={row.id} key={row.id}>
|
||||||
{(draggableSortableItemProps) => (
|
{(draggableSortableItemProps) => (
|
||||||
<ArrayRow
|
<ArrayRow
|
||||||
{...draggableSortableItemProps}
|
{...draggableSortableItemProps}
|
||||||
@@ -273,6 +275,7 @@ export const _ArrayField: React.FC<ArrayFieldProps> = (props) => {
|
|||||||
forceRender={forceRender}
|
forceRender={forceRender}
|
||||||
hasMaxRows={hasMaxRows}
|
hasMaxRows={hasMaxRows}
|
||||||
indexPath={indexPath}
|
indexPath={indexPath}
|
||||||
|
isSortable={isSortable}
|
||||||
labels={labels}
|
labels={labels}
|
||||||
moveRow={moveRow}
|
moveRow={moveRow}
|
||||||
path={path}
|
path={path}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ type BlockFieldProps = UseDraggableSortableReturn & {
|
|||||||
forceRender?: boolean
|
forceRender?: boolean
|
||||||
hasMaxRows?: boolean
|
hasMaxRows?: boolean
|
||||||
indexPath: string
|
indexPath: string
|
||||||
|
isSortable?: boolean
|
||||||
labels: Labels
|
labels: Labels
|
||||||
moveRow: (fromIndex: number, toIndex: number) => void
|
moveRow: (fromIndex: number, toIndex: number) => void
|
||||||
path: string
|
path: string
|
||||||
@@ -50,6 +51,7 @@ export const BlockRow: React.FC<BlockFieldProps> = ({
|
|||||||
errorCount,
|
errorCount,
|
||||||
forceRender,
|
forceRender,
|
||||||
hasMaxRows,
|
hasMaxRows,
|
||||||
|
isSortable,
|
||||||
labels,
|
labels,
|
||||||
listeners,
|
listeners,
|
||||||
moveRow,
|
moveRow,
|
||||||
@@ -97,6 +99,7 @@ export const BlockRow: React.FC<BlockFieldProps> = ({
|
|||||||
duplicateRow={duplicateRow}
|
duplicateRow={duplicateRow}
|
||||||
fieldMap={block.fieldMap}
|
fieldMap={block.fieldMap}
|
||||||
hasMaxRows={hasMaxRows}
|
hasMaxRows={hasMaxRows}
|
||||||
|
isSortable={isSortable}
|
||||||
labels={labels}
|
labels={labels}
|
||||||
moveRow={moveRow}
|
moveRow={moveRow}
|
||||||
removeRow={removeRow}
|
removeRow={removeRow}
|
||||||
@@ -107,11 +110,15 @@ export const BlockRow: React.FC<BlockFieldProps> = ({
|
|||||||
}
|
}
|
||||||
className={classNames}
|
className={classNames}
|
||||||
collapsibleStyle={fieldHasErrors ? 'error' : 'default'}
|
collapsibleStyle={fieldHasErrors ? 'error' : 'default'}
|
||||||
dragHandleProps={{
|
dragHandleProps={
|
||||||
id: row.id,
|
isSortable
|
||||||
attributes,
|
? {
|
||||||
listeners,
|
id: row.id,
|
||||||
}}
|
attributes,
|
||||||
|
listeners,
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
header={
|
header={
|
||||||
<div className={`${baseClass}__block-header`}>
|
<div className={`${baseClass}__block-header`}>
|
||||||
<span className={`${baseClass}__block-number`}>
|
<span className={`${baseClass}__block-number`}>
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export const RowActions: React.FC<{
|
|||||||
duplicateRow: (rowIndex: number, blockType: string) => void
|
duplicateRow: (rowIndex: number, blockType: string) => void
|
||||||
fieldMap: FieldMap
|
fieldMap: FieldMap
|
||||||
hasMaxRows?: boolean
|
hasMaxRows?: boolean
|
||||||
|
isSortable?: boolean
|
||||||
labels: Labels
|
labels: Labels
|
||||||
moveRow: (fromIndex: number, toIndex: number) => void
|
moveRow: (fromIndex: number, toIndex: number) => void
|
||||||
removeRow: (rowIndex: number) => void
|
removeRow: (rowIndex: number) => void
|
||||||
@@ -32,6 +33,7 @@ export const RowActions: React.FC<{
|
|||||||
blocks,
|
blocks,
|
||||||
duplicateRow,
|
duplicateRow,
|
||||||
hasMaxRows,
|
hasMaxRows,
|
||||||
|
isSortable,
|
||||||
labels,
|
labels,
|
||||||
moveRow,
|
moveRow,
|
||||||
removeRow,
|
removeRow,
|
||||||
@@ -67,6 +69,7 @@ export const RowActions: React.FC<{
|
|||||||
duplicateRow={() => duplicateRow(rowIndex, blockType)}
|
duplicateRow={() => duplicateRow(rowIndex, blockType)}
|
||||||
hasMaxRows={hasMaxRows}
|
hasMaxRows={hasMaxRows}
|
||||||
index={rowIndex}
|
index={rowIndex}
|
||||||
|
isSortable={isSortable}
|
||||||
moveRow={moveRow}
|
moveRow={moveRow}
|
||||||
removeRow={removeRow}
|
removeRow={removeRow}
|
||||||
rowCount={rowCount}
|
rowCount={rowCount}
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import type { FormFieldBase } from '../shared/index.js'
|
|||||||
export type BlocksFieldProps = FormFieldBase & {
|
export type BlocksFieldProps = FormFieldBase & {
|
||||||
blocks?: ReducedBlock[]
|
blocks?: ReducedBlock[]
|
||||||
forceRender?: boolean
|
forceRender?: boolean
|
||||||
|
isSortable?: boolean
|
||||||
label?: FieldBase['label']
|
label?: FieldBase['label']
|
||||||
labels?: BlockField['labels']
|
labels?: BlockField['labels']
|
||||||
maxRows?: number
|
maxRows?: number
|
||||||
@@ -62,6 +63,7 @@ const _BlocksField: React.FC<BlocksFieldProps> = (props) => {
|
|||||||
descriptionProps,
|
descriptionProps,
|
||||||
errorProps,
|
errorProps,
|
||||||
forceRender = false,
|
forceRender = false,
|
||||||
|
isSortable = true,
|
||||||
label,
|
label,
|
||||||
labelProps,
|
labelProps,
|
||||||
labels: labelsFromProps,
|
labels: labelsFromProps,
|
||||||
@@ -277,7 +279,7 @@ const _BlocksField: React.FC<BlocksFieldProps> = (props) => {
|
|||||||
errorPath.startsWith(`${path}.${i}`),
|
errorPath.startsWith(`${path}.${i}`),
|
||||||
).length
|
).length
|
||||||
return (
|
return (
|
||||||
<DraggableSortableItem disabled={readOnly} id={row.id} key={row.id}>
|
<DraggableSortableItem disabled={readOnly || !isSortable} id={row.id} key={row.id}>
|
||||||
{(draggableSortableItemProps) => (
|
{(draggableSortableItemProps) => (
|
||||||
<BlockRow
|
<BlockRow
|
||||||
{...draggableSortableItemProps}
|
{...draggableSortableItemProps}
|
||||||
@@ -289,6 +291,7 @@ const _BlocksField: React.FC<BlocksFieldProps> = (props) => {
|
|||||||
forceRender={forceRender}
|
forceRender={forceRender}
|
||||||
hasMaxRows={hasMaxRows}
|
hasMaxRows={hasMaxRows}
|
||||||
indexPath={indexPath}
|
indexPath={indexPath}
|
||||||
|
isSortable={isSortable}
|
||||||
labels={labels}
|
labels={labels}
|
||||||
moveRow={moveRow}
|
moveRow={moveRow}
|
||||||
path={path}
|
path={path}
|
||||||
|
|||||||
@@ -269,6 +269,7 @@ export const mapFields = (args: {
|
|||||||
parentPath: path,
|
parentPath: path,
|
||||||
readOnly: readOnlyOverride,
|
readOnly: readOnlyOverride,
|
||||||
}),
|
}),
|
||||||
|
isSortable: field.admin?.isSortable,
|
||||||
label: field?.label,
|
label: field?.label,
|
||||||
labels: field.labels,
|
labels: field.labels,
|
||||||
maxRows: field.maxRows,
|
maxRows: field.maxRows,
|
||||||
@@ -312,6 +313,7 @@ export const mapFields = (args: {
|
|||||||
blocks,
|
blocks,
|
||||||
className: field.admin?.className,
|
className: field.admin?.className,
|
||||||
disabled: field.admin?.disabled,
|
disabled: field.admin?.disabled,
|
||||||
|
isSortable: field.admin?.isSortable,
|
||||||
label: field?.label,
|
label: field?.label,
|
||||||
labels: field.labels,
|
labels: field.labels,
|
||||||
maxRows: field.maxRows,
|
maxRows: field.maxRows,
|
||||||
|
|||||||
@@ -139,6 +139,21 @@ const ArrayFields: CollectionConfig = {
|
|||||||
minRows: 2,
|
minRows: 2,
|
||||||
type: 'array',
|
type: 'array',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'disableSort',
|
||||||
|
defaultValue: arrayDefaultValue,
|
||||||
|
admin: {
|
||||||
|
isSortable: false,
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'text',
|
||||||
|
required: true,
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
type: 'array',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
slug: arrayFieldsSlug,
|
slug: arrayFieldsSlug,
|
||||||
versions: true,
|
versions: true,
|
||||||
|
|||||||
@@ -130,6 +130,14 @@ const BlockFields: CollectionConfig = {
|
|||||||
},
|
},
|
||||||
localized: true,
|
localized: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
...getBlocksField('localized'),
|
||||||
|
name: 'disableSort',
|
||||||
|
admin: {
|
||||||
|
isSortable: false,
|
||||||
|
},
|
||||||
|
localized: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
...getBlocksField('localized'),
|
...getBlocksField('localized'),
|
||||||
name: 'localizedBlocks',
|
name: 'localizedBlocks',
|
||||||
|
|||||||
@@ -24,7 +24,14 @@ import { POLL_TOPASS_TIMEOUT, TEST_TIMEOUT_LONG } from '../playwright.config.js'
|
|||||||
import { jsonDoc } from './collections/JSON/shared.js'
|
import { jsonDoc } from './collections/JSON/shared.js'
|
||||||
import { numberDoc } from './collections/Number/shared.js'
|
import { numberDoc } from './collections/Number/shared.js'
|
||||||
import { textDoc } from './collections/Text/shared.js'
|
import { textDoc } from './collections/Text/shared.js'
|
||||||
import { collapsibleFieldsSlug, pointFieldsSlug, tabsFieldsSlug, textFieldsSlug } from './slugs.js'
|
import {
|
||||||
|
arrayFieldsSlug,
|
||||||
|
blockFieldsSlug,
|
||||||
|
collapsibleFieldsSlug,
|
||||||
|
pointFieldsSlug,
|
||||||
|
tabsFieldsSlug,
|
||||||
|
textFieldsSlug,
|
||||||
|
} from './slugs.js'
|
||||||
const filename = fileURLToPath(import.meta.url)
|
const filename = fileURLToPath(import.meta.url)
|
||||||
const dirname = path.dirname(filename)
|
const dirname = path.dirname(filename)
|
||||||
|
|
||||||
@@ -536,6 +543,44 @@ describe('fields', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('sortable arrays', () => {
|
||||||
|
let url: AdminUrlUtil
|
||||||
|
beforeAll(() => {
|
||||||
|
url = new AdminUrlUtil(serverURL, arrayFieldsSlug)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should have disabled admin sorting', async () => {
|
||||||
|
await page.goto(url.create)
|
||||||
|
const field = page.locator('#field-disableSort .array-actions__action-chevron')
|
||||||
|
expect(await field.count()).toEqual(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('the drag handle should be hidden', async () => {
|
||||||
|
await page.goto(url.create)
|
||||||
|
const field = page.locator('#field-disableSort .collapsible__drag')
|
||||||
|
expect(await field.count()).toEqual(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('sortable blocks', () => {
|
||||||
|
let url: AdminUrlUtil
|
||||||
|
beforeAll(() => {
|
||||||
|
url = new AdminUrlUtil(serverURL, blockFieldsSlug)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should have disabled admin sorting', async () => {
|
||||||
|
await page.goto(url.create)
|
||||||
|
const field = page.locator('#field-disableSort .array-actions__action-chevron')
|
||||||
|
expect(await field.count()).toEqual(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('the drag handle should be hidden', async () => {
|
||||||
|
await page.goto(url.create)
|
||||||
|
const field = page.locator('#field-disableSort .collapsible__drag')
|
||||||
|
expect(await field.count()).toEqual(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('tabs', () => {
|
describe('tabs', () => {
|
||||||
let url: AdminUrlUtil
|
let url: AdminUrlUtil
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user