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 |
|
||||
| **`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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
| Option | Description |
|
||||
| ------------------- | ------------------------------- |
|
||||
| **`initCollapsed`** | Set the initial collapsed state |
|
||||
| Option | Description |
|
||||
| ------------------- | ---------------------------------- |
|
||||
| **`initCollapsed`** | Set the initial collapsed state |
|
||||
| **`isSortable`** | Disable order sorting by setting this value to `false` |
|
||||
|
||||
### Block configs
|
||||
|
||||
|
||||
@@ -318,6 +318,7 @@ export const array = baseField.keys({
|
||||
RowLabel: componentSchema,
|
||||
})
|
||||
.default({}),
|
||||
isSortable: joi.boolean(),
|
||||
})
|
||||
.default({}),
|
||||
dbName: joi.alternatives().try(joi.string(), joi.func()),
|
||||
@@ -415,6 +416,11 @@ export const relationship = baseField.keys({
|
||||
export const blocks = baseField.keys({
|
||||
name: joi.string().required(),
|
||||
type: joi.string().valid('blocks').required(),
|
||||
admin: baseAdminFields
|
||||
.keys({
|
||||
isSortable: joi.boolean(),
|
||||
})
|
||||
.default({}),
|
||||
blocks: joi
|
||||
.array()
|
||||
.items(
|
||||
|
||||
@@ -613,6 +613,10 @@ export type ArrayField = FieldBase & {
|
||||
RowLabel?: RowLabel
|
||||
} & Admin['components']
|
||||
initCollapsed?: boolean
|
||||
/**
|
||||
* Disable drag and drop sorting
|
||||
*/
|
||||
isSortable?: boolean
|
||||
}
|
||||
/**
|
||||
* Customize the SQL table name
|
||||
@@ -684,6 +688,10 @@ export type Block = {
|
||||
export type BlockField = FieldBase & {
|
||||
admin?: Admin & {
|
||||
initCollapsed?: boolean
|
||||
/**
|
||||
* Disable drag and drop sorting
|
||||
*/
|
||||
isSortable?: boolean
|
||||
}
|
||||
blocks: Block[]
|
||||
defaultValue?: unknown
|
||||
|
||||
@@ -17,6 +17,7 @@ export type Props = {
|
||||
duplicateRow: (current: number) => void
|
||||
hasMaxRows: boolean
|
||||
index: number
|
||||
isSortable?: boolean
|
||||
moveRow: (from: number, to: number) => void
|
||||
removeRow: (index: number) => void
|
||||
rowCount: number
|
||||
@@ -27,6 +28,7 @@ export const ArrayAction: React.FC<Props> = ({
|
||||
duplicateRow,
|
||||
hasMaxRows,
|
||||
index,
|
||||
isSortable,
|
||||
moveRow,
|
||||
removeRow,
|
||||
rowCount,
|
||||
@@ -42,7 +44,7 @@ export const ArrayAction: React.FC<Props> = ({
|
||||
render={({ close }) => {
|
||||
return (
|
||||
<PopupList.ButtonGroup buttonSize="small">
|
||||
{index !== 0 && (
|
||||
{isSortable && index !== 0 && (
|
||||
<PopupList.Button
|
||||
className={`${baseClass}__action ${baseClass}__move-up`}
|
||||
onClick={() => {
|
||||
@@ -56,7 +58,7 @@ export const ArrayAction: React.FC<Props> = ({
|
||||
{t('general:moveUp')}
|
||||
</PopupList.Button>
|
||||
)}
|
||||
{index < rowCount - 1 && (
|
||||
{isSortable && index < rowCount - 1 && (
|
||||
<PopupList.Button
|
||||
className={`${baseClass}__action`}
|
||||
onClick={() => {
|
||||
|
||||
@@ -27,6 +27,7 @@ type ArrayRowProps = UseDraggableSortableReturn & {
|
||||
forceRender?: boolean
|
||||
hasMaxRows?: boolean
|
||||
indexPath: string
|
||||
isSortable?: boolean
|
||||
labels: ArrayField['labels']
|
||||
moveRow: (fromIndex: number, toIndex: number) => void
|
||||
path: string
|
||||
@@ -50,6 +51,7 @@ export const ArrayRow: React.FC<ArrayRowProps> = ({
|
||||
forceRender = false,
|
||||
hasMaxRows,
|
||||
indexPath,
|
||||
isSortable,
|
||||
labels,
|
||||
listeners,
|
||||
moveRow,
|
||||
@@ -100,6 +102,7 @@ export const ArrayRow: React.FC<ArrayRowProps> = ({
|
||||
duplicateRow={duplicateRow}
|
||||
hasMaxRows={hasMaxRows}
|
||||
index={rowIndex}
|
||||
isSortable={isSortable}
|
||||
moveRow={moveRow}
|
||||
removeRow={removeRow}
|
||||
rowCount={rowCount}
|
||||
@@ -108,11 +111,15 @@ export const ArrayRow: React.FC<ArrayRowProps> = ({
|
||||
}
|
||||
className={classNames}
|
||||
collapsibleStyle={fieldHasErrors ? 'error' : 'default'}
|
||||
dragHandleProps={{
|
||||
id: row.id,
|
||||
attributes,
|
||||
listeners,
|
||||
}}
|
||||
dragHandleProps={
|
||||
isSortable
|
||||
? {
|
||||
id: row.id,
|
||||
attributes,
|
||||
listeners,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
header={
|
||||
<div className={`${baseClass}__row-header`}>
|
||||
<RowLabel
|
||||
|
||||
@@ -37,6 +37,7 @@ export type ArrayFieldProps = FormFieldBase & {
|
||||
CustomRowLabel?: React.ReactNode
|
||||
fieldMap: FieldMap
|
||||
forceRender?: boolean
|
||||
isSortable?: boolean
|
||||
label?: FieldBase['label']
|
||||
labels?: ArrayFieldType['labels']
|
||||
maxRows?: ArrayFieldType['maxRows']
|
||||
@@ -58,6 +59,7 @@ export const _ArrayField: React.FC<ArrayFieldProps> = (props) => {
|
||||
errorProps,
|
||||
fieldMap,
|
||||
forceRender = false,
|
||||
isSortable = true,
|
||||
label,
|
||||
labelProps,
|
||||
localized,
|
||||
@@ -261,7 +263,7 @@ export const _ArrayField: React.FC<ArrayFieldProps> = (props) => {
|
||||
errorPath.startsWith(`${path}.${i}.`),
|
||||
).length
|
||||
return (
|
||||
<DraggableSortableItem disabled={readOnly} id={row.id} key={row.id}>
|
||||
<DraggableSortableItem disabled={readOnly || !isSortable} id={row.id} key={row.id}>
|
||||
{(draggableSortableItemProps) => (
|
||||
<ArrayRow
|
||||
{...draggableSortableItemProps}
|
||||
@@ -273,6 +275,7 @@ export const _ArrayField: React.FC<ArrayFieldProps> = (props) => {
|
||||
forceRender={forceRender}
|
||||
hasMaxRows={hasMaxRows}
|
||||
indexPath={indexPath}
|
||||
isSortable={isSortable}
|
||||
labels={labels}
|
||||
moveRow={moveRow}
|
||||
path={path}
|
||||
|
||||
@@ -28,6 +28,7 @@ type BlockFieldProps = UseDraggableSortableReturn & {
|
||||
forceRender?: boolean
|
||||
hasMaxRows?: boolean
|
||||
indexPath: string
|
||||
isSortable?: boolean
|
||||
labels: Labels
|
||||
moveRow: (fromIndex: number, toIndex: number) => void
|
||||
path: string
|
||||
@@ -50,6 +51,7 @@ export const BlockRow: React.FC<BlockFieldProps> = ({
|
||||
errorCount,
|
||||
forceRender,
|
||||
hasMaxRows,
|
||||
isSortable,
|
||||
labels,
|
||||
listeners,
|
||||
moveRow,
|
||||
@@ -97,6 +99,7 @@ export const BlockRow: React.FC<BlockFieldProps> = ({
|
||||
duplicateRow={duplicateRow}
|
||||
fieldMap={block.fieldMap}
|
||||
hasMaxRows={hasMaxRows}
|
||||
isSortable={isSortable}
|
||||
labels={labels}
|
||||
moveRow={moveRow}
|
||||
removeRow={removeRow}
|
||||
@@ -107,11 +110,15 @@ export const BlockRow: React.FC<BlockFieldProps> = ({
|
||||
}
|
||||
className={classNames}
|
||||
collapsibleStyle={fieldHasErrors ? 'error' : 'default'}
|
||||
dragHandleProps={{
|
||||
id: row.id,
|
||||
attributes,
|
||||
listeners,
|
||||
}}
|
||||
dragHandleProps={
|
||||
isSortable
|
||||
? {
|
||||
id: row.id,
|
||||
attributes,
|
||||
listeners,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
header={
|
||||
<div className={`${baseClass}__block-header`}>
|
||||
<span className={`${baseClass}__block-number`}>
|
||||
|
||||
@@ -20,6 +20,7 @@ export const RowActions: React.FC<{
|
||||
duplicateRow: (rowIndex: number, blockType: string) => void
|
||||
fieldMap: FieldMap
|
||||
hasMaxRows?: boolean
|
||||
isSortable?: boolean
|
||||
labels: Labels
|
||||
moveRow: (fromIndex: number, toIndex: number) => void
|
||||
removeRow: (rowIndex: number) => void
|
||||
@@ -32,6 +33,7 @@ export const RowActions: React.FC<{
|
||||
blocks,
|
||||
duplicateRow,
|
||||
hasMaxRows,
|
||||
isSortable,
|
||||
labels,
|
||||
moveRow,
|
||||
removeRow,
|
||||
@@ -67,6 +69,7 @@ export const RowActions: React.FC<{
|
||||
duplicateRow={() => duplicateRow(rowIndex, blockType)}
|
||||
hasMaxRows={hasMaxRows}
|
||||
index={rowIndex}
|
||||
isSortable={isSortable}
|
||||
moveRow={moveRow}
|
||||
removeRow={removeRow}
|
||||
rowCount={rowCount}
|
||||
|
||||
@@ -39,6 +39,7 @@ import type { FormFieldBase } from '../shared/index.js'
|
||||
export type BlocksFieldProps = FormFieldBase & {
|
||||
blocks?: ReducedBlock[]
|
||||
forceRender?: boolean
|
||||
isSortable?: boolean
|
||||
label?: FieldBase['label']
|
||||
labels?: BlockField['labels']
|
||||
maxRows?: number
|
||||
@@ -62,6 +63,7 @@ const _BlocksField: React.FC<BlocksFieldProps> = (props) => {
|
||||
descriptionProps,
|
||||
errorProps,
|
||||
forceRender = false,
|
||||
isSortable = true,
|
||||
label,
|
||||
labelProps,
|
||||
labels: labelsFromProps,
|
||||
@@ -277,7 +279,7 @@ const _BlocksField: React.FC<BlocksFieldProps> = (props) => {
|
||||
errorPath.startsWith(`${path}.${i}`),
|
||||
).length
|
||||
return (
|
||||
<DraggableSortableItem disabled={readOnly} id={row.id} key={row.id}>
|
||||
<DraggableSortableItem disabled={readOnly || !isSortable} id={row.id} key={row.id}>
|
||||
{(draggableSortableItemProps) => (
|
||||
<BlockRow
|
||||
{...draggableSortableItemProps}
|
||||
@@ -289,6 +291,7 @@ const _BlocksField: React.FC<BlocksFieldProps> = (props) => {
|
||||
forceRender={forceRender}
|
||||
hasMaxRows={hasMaxRows}
|
||||
indexPath={indexPath}
|
||||
isSortable={isSortable}
|
||||
labels={labels}
|
||||
moveRow={moveRow}
|
||||
path={path}
|
||||
|
||||
@@ -269,6 +269,7 @@ export const mapFields = (args: {
|
||||
parentPath: path,
|
||||
readOnly: readOnlyOverride,
|
||||
}),
|
||||
isSortable: field.admin?.isSortable,
|
||||
label: field?.label,
|
||||
labels: field.labels,
|
||||
maxRows: field.maxRows,
|
||||
@@ -312,6 +313,7 @@ export const mapFields = (args: {
|
||||
blocks,
|
||||
className: field.admin?.className,
|
||||
disabled: field.admin?.disabled,
|
||||
isSortable: field.admin?.isSortable,
|
||||
label: field?.label,
|
||||
labels: field.labels,
|
||||
maxRows: field.maxRows,
|
||||
|
||||
@@ -139,6 +139,21 @@ const ArrayFields: CollectionConfig = {
|
||||
minRows: 2,
|
||||
type: 'array',
|
||||
},
|
||||
{
|
||||
name: 'disableSort',
|
||||
defaultValue: arrayDefaultValue,
|
||||
admin: {
|
||||
isSortable: false,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'text',
|
||||
required: true,
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
type: 'array',
|
||||
},
|
||||
],
|
||||
slug: arrayFieldsSlug,
|
||||
versions: true,
|
||||
|
||||
@@ -130,6 +130,14 @@ const BlockFields: CollectionConfig = {
|
||||
},
|
||||
localized: true,
|
||||
},
|
||||
{
|
||||
...getBlocksField('localized'),
|
||||
name: 'disableSort',
|
||||
admin: {
|
||||
isSortable: false,
|
||||
},
|
||||
localized: true,
|
||||
},
|
||||
{
|
||||
...getBlocksField('localized'),
|
||||
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 { numberDoc } from './collections/Number/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 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', () => {
|
||||
let url: AdminUrlUtil
|
||||
beforeAll(() => {
|
||||
|
||||
Reference in New Issue
Block a user