feat(ui): toggle sortable arrays and blocks (#6008)

This commit is contained in:
Kendell Joseph
2024-05-08 13:28:26 -04:00
committed by GitHub
parent dc8c099d9e
commit 4c6aaafe88
14 changed files with 129 additions and 18 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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(

View File

@@ -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

View File

@@ -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={() => {

View File

@@ -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

View File

@@ -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}

View File

@@ -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`}>

View File

@@ -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}

View File

@@ -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}

View File

@@ -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,

View File

@@ -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,

View File

@@ -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',

View File

@@ -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(() => {