From 9c4f1483f58d064e53c975db06d02bde749c55af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rge=20N=C3=A6ss?= Date: Mon, 14 Oct 2024 16:21:03 +0200 Subject: [PATCH 1/3] feat(form): add support for disabling various array input capabilities --- .../schema/debug/simpleArrayOfObjects.js | 6 +- .../types/src/schema/definition/type/array.ts | 39 ++++++++++ .../ArrayOfObjectsFunctions.tsx | 4 + .../ArrayOfObjectsInput/InsertMenuGroups.tsx | 2 +- .../InsertMenuMenuItems.tsx | 2 + .../ArrayOfObjectsInput/List/PreviewItem.tsx | 75 ++++++++++++------- .../ArrayOfPrimitivesFunctions.tsx | 4 + .../arrays/ArrayOfPrimitivesInput/ItemRow.tsx | 72 +++++++++++++----- 8 files changed, 158 insertions(+), 46 deletions(-) diff --git a/dev/test-studio/schema/debug/simpleArrayOfObjects.js b/dev/test-studio/schema/debug/simpleArrayOfObjects.js index 16d004219f8..e06eb162793 100644 --- a/dev/test-studio/schema/debug/simpleArrayOfObjects.js +++ b/dev/test-studio/schema/debug/simpleArrayOfObjects.js @@ -13,7 +13,11 @@ export const simpleArrayOfObjects = { }, { name: 'arrayWithObjects', - options: {collapsible: true, collapsed: true}, + options: { + collapsible: true, + collapsed: true, + disableActions: ['add'], + }, title: 'Array with named objects', description: 'This array contains objects of type as defined inline', type: 'array', diff --git a/packages/@sanity/types/src/schema/definition/type/array.ts b/packages/@sanity/types/src/schema/definition/type/array.ts index 2a966859728..536e556ab24 100644 --- a/packages/@sanity/types/src/schema/definition/type/array.ts +++ b/packages/@sanity/types/src/schema/definition/type/array.ts @@ -17,6 +17,38 @@ import { export type {InsertMenuOptions} +/** + * Types of array actions that can be performed + * @beta + */ +export type ArrayActionName = + /** + * Add any item to the array at any position + */ + | 'add' + /** + * Add item after an existing item + */ + | 'addBefore' + + /** + * Add item after an existing item + */ + | 'addAfter' + /** + * Remove any item + */ + | 'remove' + /** + * Duplicate item + */ + | 'duplicate' + + /** + * Copy item + */ + | 'copy' + /** @public */ export interface ArrayOptions extends SearchConfiguration, BaseSchemaTypeOptions { list?: TitledListValue[] | V[] @@ -35,6 +67,13 @@ export interface ArrayOptions extends SearchConfiguration, BaseSche * @deprecated tree editing beta feature has been disabled */ treeEditing?: boolean + + /** + * A list of array actions to disable + * Possible options are defined by {@link ArrayActionName} + * @beta + */ + disableActions?: ArrayActionName[] } /** @public */ diff --git a/packages/sanity/src/core/form/inputs/arrays/ArrayOfObjectsInput/ArrayOfObjectsFunctions.tsx b/packages/sanity/src/core/form/inputs/arrays/ArrayOfObjectsInput/ArrayOfObjectsFunctions.tsx index 532a9b11632..ea2a126968a 100644 --- a/packages/sanity/src/core/form/inputs/arrays/ArrayOfObjectsInput/ArrayOfObjectsFunctions.tsx +++ b/packages/sanity/src/core/form/inputs/arrays/ArrayOfObjectsInput/ArrayOfObjectsFunctions.tsx @@ -65,6 +65,10 @@ export function ArrayOfObjectsFunctions< }, }) + if (schemaType.options?.disableActions?.includes('add')) { + return null + } + if (readOnly) { return ( diff --git a/packages/sanity/src/core/form/inputs/arrays/ArrayOfObjectsInput/InsertMenuGroups.tsx b/packages/sanity/src/core/form/inputs/arrays/ArrayOfObjectsInput/InsertMenuGroups.tsx index 55a935ed191..ca8921927e0 100644 --- a/packages/sanity/src/core/form/inputs/arrays/ArrayOfObjectsInput/InsertMenuGroups.tsx +++ b/packages/sanity/src/core/form/inputs/arrays/ArrayOfObjectsInput/InsertMenuGroups.tsx @@ -41,7 +41,7 @@ export const InsertMenuGroups = memo(function InsertMenuGroups(props: Props) { ) }) -function InsertMenuGroup( +export function InsertMenuGroup( props: Props & { pos: 'before' | 'after' text: ComponentProps['text'] diff --git a/packages/sanity/src/core/form/inputs/arrays/ArrayOfObjectsInput/InsertMenuMenuItems.tsx b/packages/sanity/src/core/form/inputs/arrays/ArrayOfObjectsInput/InsertMenuMenuItems.tsx index cfb55f74107..59f99a5f036 100644 --- a/packages/sanity/src/core/form/inputs/arrays/ArrayOfObjectsInput/InsertMenuMenuItems.tsx +++ b/packages/sanity/src/core/form/inputs/arrays/ArrayOfObjectsInput/InsertMenuMenuItems.tsx @@ -78,6 +78,7 @@ export function useInsertMenuMenuItems(props: InsertMenuItemsProps) { () => types ? ( types ? ( (props: PreviewItemProps) { const { schemaType, @@ -150,9 +150,55 @@ export function PreviewItem(props: Preview referenceElement: contextMenuButtonElement, }) + const disableActions = parentSchemaType.options?.disableActions || EMPTY_ARRAY + + const menuItems = useMemo(() => { + return [ + !disableActions.includes('remove') && ( + + ), + !disableActions.includes('copy') && ( + + ), + !disableActions.includes('duplicate') && ( + + ), + !disableActions.includes('add') && + !disableActions.includes('addBefore') && + insertBefore.menuItem, + !disableActions.includes('add') && + !disableActions.includes('addAfter') && + insertAfter.menuItem, + ].filter(Boolean) + }, [ + disableActions, + handleCopy, + handleDuplicate, + insertAfter.menuItem, + insertBefore.menuItem, + onRemove, + t, + ]) + const menu = useMemo( () => - readOnly ? null : ( + readOnly || menuItems.length === 0 ? null : ( <> (props: Preview /> } id={`${props.inputId}-menuButton`} - menu={ - - - - - {insertBefore.menuItem} - {insertAfter.menuItem} - - } + menu={{menuItems}} popover={MENU_POPOVER_PROPS} /> {insertBefore.popover} {insertAfter.popover} ), - [readOnly, insertBefore, insertAfter, props.inputId, t, onRemove, handleCopy, handleDuplicate], + [menuItems, readOnly, insertBefore, insertAfter, props.inputId], ) const tone = getTone({readOnly, hasErrors, hasWarnings}) diff --git a/packages/sanity/src/core/form/inputs/arrays/ArrayOfPrimitivesInput/ArrayOfPrimitivesFunctions.tsx b/packages/sanity/src/core/form/inputs/arrays/ArrayOfPrimitivesInput/ArrayOfPrimitivesFunctions.tsx index 36f6b9cbe4e..30da871f565 100644 --- a/packages/sanity/src/core/form/inputs/arrays/ArrayOfPrimitivesInput/ArrayOfPrimitivesFunctions.tsx +++ b/packages/sanity/src/core/form/inputs/arrays/ArrayOfPrimitivesInput/ArrayOfPrimitivesFunctions.tsx @@ -39,6 +39,10 @@ export function ArrayOfPrimitivesFunctions< ? 'inputs.array.action.add-item-select-type' : 'inputs.array.action.add-item' + if (schemaType.options?.disableActions?.includes('add')) { + return null + } + if (readOnly) { return ( diff --git a/packages/sanity/src/core/form/inputs/arrays/ArrayOfPrimitivesInput/ItemRow.tsx b/packages/sanity/src/core/form/inputs/arrays/ArrayOfPrimitivesInput/ItemRow.tsx index b5f70aac64e..a1843317965 100644 --- a/packages/sanity/src/core/form/inputs/arrays/ArrayOfPrimitivesInput/ItemRow.tsx +++ b/packages/sanity/src/core/form/inputs/arrays/ArrayOfPrimitivesInput/ItemRow.tsx @@ -1,4 +1,4 @@ -import {AddDocumentIcon, CopyIcon, TrashIcon} from '@sanity/icons' +import {AddDocumentIcon, CopyIcon, InsertAboveIcon, InsertBelowIcon, TrashIcon} from '@sanity/icons' import {type SchemaType} from '@sanity/types' import {Box, Flex, Menu} from '@sanity/ui' import {type ForwardedRef, forwardRef, useCallback, useMemo} from 'react' @@ -9,7 +9,7 @@ import {useTranslation} from '../../../../i18n' import {FieldPresence} from '../../../../presence' import {FormFieldValidationStatus} from '../../../components/formField' import {type PrimitiveItemProps} from '../../../types/itemProps' -import {InsertMenuGroups} from '../ArrayOfObjectsInput/InsertMenuGroups' +import {InsertMenuGroup} from '../ArrayOfObjectsInput/InsertMenuGroups' import {RowLayout} from '../layouts/RowLayout' import {getEmptyValue} from './getEmptyValue' @@ -33,6 +33,7 @@ export const ItemRow = forwardRef(function ItemRow( onRemove, readOnly, inputId, + parentSchemaType, validation, children, presence, @@ -72,28 +73,61 @@ export const ItemRow = forwardRef(function ItemRow( const {t} = useTranslation() + const disableActions = parentSchemaType.options?.disableActions || [] + + const menuItems = [ + !disableActions.includes('remove') && ( + + ), + !disableActions.includes('copy') && ( + + ), + !disableActions.includes('duplicate') && ( + + ), + !(disableActions.includes('add') || disableActions.includes('addBefore')) && ( + + ), + !disableActions.includes('add') && + !(disableActions.includes('addAfter') && disableActions.includes('addBefore')) && ( + + ), + ] + const menu = ( } id={`${inputId}-menuButton`} popover={MENU_BUTTON_POPOVER_PROPS} - menu={ - - - - - - - } + menu={{menuItems}} /> ) From 0afea4cdf4fde8902e2e1c1c2f6ae5c3de9ac253 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rge=20N=C3=A6ss?= Date: Mon, 13 Jan 2025 14:20:12 +0100 Subject: [PATCH 2/3] fixup! feat(form): add support for disabling various array input capabilities --- .../schema/debug/arrayCapabilities.ts | 77 ++++++++++++ .../schema/debug/simpleArrayOfObjects.js | 6 +- dev/test-studio/schema/index.ts | 2 + dev/test-studio/structure/constants.ts | 1 + .../ArrayOfObjectsInput/Grid/GridItem.tsx | 68 ++++++---- .../arrays/ArrayOfPrimitivesInput/ItemRow.tsx | 118 ++++++++++-------- .../studio/inputResolver/fieldResolver.tsx | 17 ++- 7 files changed, 202 insertions(+), 87 deletions(-) create mode 100644 dev/test-studio/schema/debug/arrayCapabilities.ts diff --git a/dev/test-studio/schema/debug/arrayCapabilities.ts b/dev/test-studio/schema/debug/arrayCapabilities.ts new file mode 100644 index 00000000000..416e8ceba1c --- /dev/null +++ b/dev/test-studio/schema/debug/arrayCapabilities.ts @@ -0,0 +1,77 @@ +import {defineType} from '@sanity/types' + +const DISABLED_ACTIONS = ['add', 'addBefore', 'addAfter', 'duplicate', 'remove', 'copy'] as const + +export const arrayCapabilities = defineType({ + name: 'arrayCapabilitiesExample', + type: 'document', + title: 'Array Capabilities test', + // icon, + fields: [ + { + name: 'title', + title: 'Title', + type: 'string', + }, + { + name: 'objectArray', + options: { + collapsible: true, + collapsed: true, + disableActions: DISABLED_ACTIONS, + }, + title: 'Object array', + description: `With disabledActions: ${DISABLED_ACTIONS.join(', ')}`, + type: 'array', + of: [ + { + type: 'object', + name: 'something', + title: 'Something', + fields: [{name: 'first', type: 'string', title: 'First string'}], + }, + ], + }, + { + name: 'objectArrayAsGrid', + options: { + layout: 'grid', + collapsible: true, + collapsed: true, + disableActions: DISABLED_ACTIONS, + }, + title: 'Object array with grid layout', + description: `With disabledActions: ${DISABLED_ACTIONS.join(', ')}`, + type: 'array', + of: [ + { + type: 'object', + name: 'something', + title: 'Something', + fields: [{name: 'first', type: 'string', title: 'First string'}], + }, + ], + }, + { + name: 'primitiveArray', + options: { + collapsible: true, + collapsed: true, + disableActions: DISABLED_ACTIONS, + }, + title: 'Primitive array', + description: `With disabledActions: ${DISABLED_ACTIONS.join(', ')}`, + type: 'array', + of: [ + { + type: 'string', + title: 'A string', + }, + { + type: 'number', + title: 'A number', + }, + ], + }, + ], +}) diff --git a/dev/test-studio/schema/debug/simpleArrayOfObjects.js b/dev/test-studio/schema/debug/simpleArrayOfObjects.js index e06eb162793..16d004219f8 100644 --- a/dev/test-studio/schema/debug/simpleArrayOfObjects.js +++ b/dev/test-studio/schema/debug/simpleArrayOfObjects.js @@ -13,11 +13,7 @@ export const simpleArrayOfObjects = { }, { name: 'arrayWithObjects', - options: { - collapsible: true, - collapsed: true, - disableActions: ['add'], - }, + options: {collapsible: true, collapsed: true}, title: 'Array with named objects', description: 'This array contains objects of type as defined inline', type: 'array', diff --git a/dev/test-studio/schema/index.ts b/dev/test-studio/schema/index.ts index ebc53f54904..644be38a85f 100644 --- a/dev/test-studio/schema/index.ts +++ b/dev/test-studio/schema/index.ts @@ -6,6 +6,7 @@ import conditionalFieldset from './ci/conditionalFieldset' import validationTest from './ci/validationCI' import actions from './debug/actions' import {allNativeInputComponents} from './debug/allNativeInputComponents' +import {arrayCapabilities} from './debug/arrayCapabilities' import button from './debug/button' import {circularCrossDatasetReferenceTest} from './debug/circularCrossDatasetReference' import {collapsibleObjects} from './debug/collapsibleObjects' @@ -241,6 +242,7 @@ export const schemaTypes = [ recursivePopover, patchOnMountDebug, simpleArrayOfObjects, + arrayCapabilities, simpleReferences, reservedFieldNames, review, diff --git a/dev/test-studio/structure/constants.ts b/dev/test-studio/structure/constants.ts index 75b4b5ba8e5..e3dbe06ea45 100644 --- a/dev/test-studio/structure/constants.ts +++ b/dev/test-studio/structure/constants.ts @@ -81,6 +81,7 @@ export const DEBUG_INPUT_TYPES = [ 'scrollBug', 'select', 'simpleArrayOfObjects', + 'arrayCapabilities', 'simpleReferences', 'thesis', 'typeWithNoToplevelStrings', diff --git a/packages/sanity/src/core/form/inputs/arrays/ArrayOfObjectsInput/Grid/GridItem.tsx b/packages/sanity/src/core/form/inputs/arrays/ArrayOfObjectsInput/Grid/GridItem.tsx index 892713af995..f76629f47ca 100644 --- a/packages/sanity/src/core/form/inputs/arrays/ArrayOfObjectsInput/Grid/GridItem.tsx +++ b/packages/sanity/src/core/form/inputs/arrays/ArrayOfObjectsInput/Grid/GridItem.tsx @@ -62,7 +62,7 @@ function getTone({ return hasWarnings ? 'caution' : 'default' } const MENU_POPOVER_PROPS = {portal: true, tone: 'default'} as const - +const EMPTY_ARRAY: never[] = [] export function GridItem(props: GridItemProps) { const { schemaType, @@ -162,9 +162,48 @@ export function GridItem(props: GridItemPr referenceElement: contextMenuButtonElement, }) + const disableActions = parentSchemaType.options?.disableActions || EMPTY_ARRAY + + const menuItems = useMemo(() => { + return [ + !disableActions.includes('remove') && ( + + ), + !disableActions.includes('copy') && ( + + ), + !disableActions.includes('duplicate') && ( + + ), + !disableActions.includes('add') && + !disableActions.includes('addBefore') && + insertBefore.menuItem, + !disableActions.includes('add') && + !disableActions.includes('addAfter') && + insertAfter.menuItem, + ].filter(Boolean) + }, [ + disableActions, + handleCopy, + handleDuplicate, + insertAfter.menuItem, + insertBefore.menuItem, + onRemove, + t, + ]) + const menu = useMemo( () => - readOnly ? null : ( + readOnly || menuItems.length === 0 ? null : ( <> (props: GridItemPr /> } id={`${props.inputId}-menuButton`} - menu={ - - - - - {insertBefore.menuItem} - {insertAfter.menuItem} - - } + menu={{menuItems}} popover={MENU_POPOVER_PROPS} /> {insertBefore.popover} {insertAfter.popover} ), - [insertBefore, insertAfter, handleCopy, handleDuplicate, onRemove, props.inputId, readOnly, t], + [readOnly, insertBefore, insertAfter, props.inputId, menuItems], ) const tone = getTone({readOnly, hasErrors, hasWarnings}) diff --git a/packages/sanity/src/core/form/inputs/arrays/ArrayOfPrimitivesInput/ItemRow.tsx b/packages/sanity/src/core/form/inputs/arrays/ArrayOfPrimitivesInput/ItemRow.tsx index a1843317965..2a7aea9a85b 100644 --- a/packages/sanity/src/core/form/inputs/arrays/ArrayOfPrimitivesInput/ItemRow.tsx +++ b/packages/sanity/src/core/form/inputs/arrays/ArrayOfPrimitivesInput/ItemRow.tsx @@ -19,6 +19,7 @@ export type DefaultItemProps = Omit & { } const MENU_BUTTON_POPOVER_PROPS = {portal: true, tone: 'default'} as const +const EMPTY_ARRAY: never[] = [] export const ItemRow = forwardRef(function ItemRow( props: DefaultItemProps, @@ -73,69 +74,76 @@ export const ItemRow = forwardRef(function ItemRow( const {t} = useTranslation() - const disableActions = parentSchemaType.options?.disableActions || [] + const disableActions = parentSchemaType.options?.disableActions || EMPTY_ARRAY - const menuItems = [ - !disableActions.includes('remove') && ( - - ), - !disableActions.includes('copy') && ( - - ), - !disableActions.includes('duplicate') && ( - - ), - !(disableActions.includes('add') || disableActions.includes('addBefore')) && ( - - ), - !disableActions.includes('add') && - !(disableActions.includes('addAfter') && disableActions.includes('addBefore')) && ( - + [ + !disableActions.includes('remove') && ( + + ), + !disableActions.includes('copy') && ( + + ), + !disableActions.includes('duplicate') && ( + + ), + !(disableActions.includes('add') || disableActions.includes('addBefore')) && ( + + ), + !disableActions.includes('add') && + !(disableActions.includes('addAfter') && disableActions.includes('addBefore')) && ( + + ), + ].filter(Boolean), + [disableActions, handleCopy, handleDuplicate, handleInsert, insertableTypes, onRemove, t], + ) + + const menu = useMemo( + () => + readOnly || menuItems.length === 0 ? null : ( + } + id={`${inputId}-menuButton`} + popover={MENU_BUTTON_POPOVER_PROPS} + menu={{menuItems}} /> ), - ] - - const menu = ( - } - id={`${inputId}-menuButton`} - popover={MENU_BUTTON_POPOVER_PROPS} - menu={{menuItems}} - /> + [inputId, menuItems, readOnly], ) - return ( } validation={ diff --git a/packages/sanity/src/core/form/studio/inputResolver/fieldResolver.tsx b/packages/sanity/src/core/form/studio/inputResolver/fieldResolver.tsx index 614644723dc..f919869e9c8 100644 --- a/packages/sanity/src/core/form/studio/inputResolver/fieldResolver.tsx +++ b/packages/sanity/src/core/form/studio/inputResolver/fieldResolver.tsx @@ -5,7 +5,7 @@ import { isReferenceSchemaType, type SchemaType, } from '@sanity/types' -import {type ComponentType, useState} from 'react' +import {type ComponentType, useMemo, useState} from 'react' import {ChangeIndicator} from '../../../changeIndicators' import {type DocumentFieldActionNode} from '../../../config' @@ -81,11 +81,24 @@ function ObjectOrArrayField(field: ObjectFieldProps | ArrayFieldProps) { const documentId = usePublishedId() const focused = Boolean(field.inputProps.focused) + const disableActions = field.schemaType.options?.disableActions || EMPTY_ARRAY + + const actions = useMemo(() => { + return field.actions?.filter((a) => { + if (a.name === 'pasteField') { + return !disableActions.includes('add') + } + if (a.name === 'copyField') { + return !disableActions.includes('copy') + } + return true + }) + }, [disableActions, field.actions]) return ( <> {documentId && field.actions && field.actions.length > 0 && ( Date: Mon, 13 Jan 2025 15:40:23 +0100 Subject: [PATCH 3/3] test(e2e): add basic e2e for array capabilities --- .../ArrayOfObjectsInput/Grid/GridItem.tsx | 1 + .../ArrayOfObjectsInput/List/PreviewItem.tsx | 1 + .../ArrayOfPrimitivesFunctions.tsx | 10 +++- .../tests/inputs/array-capabilities.spec.ts | 46 +++++++++++++++++++ 4 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 test/e2e/tests/inputs/array-capabilities.spec.ts diff --git a/packages/sanity/src/core/form/inputs/arrays/ArrayOfObjectsInput/Grid/GridItem.tsx b/packages/sanity/src/core/form/inputs/arrays/ArrayOfObjectsInput/Grid/GridItem.tsx index f76629f47ca..873489c8394 100644 --- a/packages/sanity/src/core/form/inputs/arrays/ArrayOfObjectsInput/Grid/GridItem.tsx +++ b/packages/sanity/src/core/form/inputs/arrays/ArrayOfObjectsInput/Grid/GridItem.tsx @@ -213,6 +213,7 @@ export function GridItem(props: GridItemPr }} button={ } diff --git a/packages/sanity/src/core/form/inputs/arrays/ArrayOfObjectsInput/List/PreviewItem.tsx b/packages/sanity/src/core/form/inputs/arrays/ArrayOfObjectsInput/List/PreviewItem.tsx index 4f02bcca8cd..2817c5c884d 100644 --- a/packages/sanity/src/core/form/inputs/arrays/ArrayOfObjectsInput/List/PreviewItem.tsx +++ b/packages/sanity/src/core/form/inputs/arrays/ArrayOfObjectsInput/List/PreviewItem.tsx @@ -208,6 +208,7 @@ export function PreviewItem(props: Preview }} button={ } diff --git a/packages/sanity/src/core/form/inputs/arrays/ArrayOfPrimitivesInput/ArrayOfPrimitivesFunctions.tsx b/packages/sanity/src/core/form/inputs/arrays/ArrayOfPrimitivesInput/ArrayOfPrimitivesFunctions.tsx index 30da871f565..6d30c91c47d 100644 --- a/packages/sanity/src/core/form/inputs/arrays/ArrayOfPrimitivesInput/ArrayOfPrimitivesFunctions.tsx +++ b/packages/sanity/src/core/form/inputs/arrays/ArrayOfPrimitivesInput/ArrayOfPrimitivesFunctions.tsx @@ -47,7 +47,14 @@ export function ArrayOfPrimitivesFunctions< return ( -