Skip to content

Commit

Permalink
feat(core): support pasting object into array + copying individual ar…
Browse files Browse the repository at this point in the history
…ray items (#7292)

* feat(core): support pasting object into array

touches edx-1432

Signed-off-by: Fred Carlsen <[email protected]>

* fix(core): validate references in documents on paste

fixes edx-1584

Signed-off-by: Fred Carlsen <[email protected]>

* fix(core): add missing translation key

Signed-off-by: Fred Carlsen <[email protected]>

Signed-off-by: Fred Carlsen <[email protected]>

* feat(core): add support for copying single array items

Signed-off-by: Fred Carlsen <[email protected]>

* fix(core): handle first index segments (0) when getting value

Signed-off-by: Fred Carlsen <[email protected]>

* test(core): add tests for copy paste hook

This makes sure we at least cover more of the studio side of the logic, and don’t rely solely on the unit test for the transferValue logic

Signed-off-by: Fred Carlsen <[email protected]>

* chore(deps): add dependencies

Signed-off-by: Fred Carlsen <[email protected]>

* fix(core): fix flaky test for recentSearches

The underlying issue is that the `getRecentSearches()` function will mutate state, and the state will not necessarily be updated before the test continues. The workaround here is to force a rerender any time we change the state before continuing. This might point to some underlying implementation of `recentSearches`, but I did not dig too much into that.

Signed-off-by: Fred Carlsen <[email protected]>

Signed-off-by: Fred Carlsen <[email protected]>

* chore(core): update comment about handling inline objects

Signed-off-by: Fred Carlsen <[email protected]>

---------

Signed-off-by: Fred Carlsen <[email protected]>
  • Loading branch information
sjelfull authored Aug 13, 2024
1 parent c2e4eb3 commit ea55826
Show file tree
Hide file tree
Showing 30 changed files with 2,065 additions and 303 deletions.
17 changes: 16 additions & 1 deletion dev/test-studio/schema/standard/objects.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// import {FaPuzzlePiece as icon} from 'react-icons/fa'

import {useCallback} from 'react'
import {defineType, type FormPatch, set, TransformPatches} from 'sanity'
import {defineField, defineType, type FormPatch, set, TransformPatches} from 'sanity'

export const myObject = defineType({
type: 'object',
Expand Down Expand Up @@ -108,6 +108,21 @@ export default defineType({
title: 'MyObject',
description: 'The first field here should be the title',
},
defineField({
type: 'object',
name: 'color',
title: 'Color with a long title',
fields: [
{
name: 'title',
type: 'string',
},
{
name: 'name',
type: 'string',
},
],
}),
{
name: 'fieldWithObjectType',
title: 'Field of object type',
Expand Down
3 changes: 2 additions & 1 deletion packages/sanity/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@
"@sanity/ui-workshop": "^1.2.11",
"@sentry/types": "^8.12.0",
"@testing-library/jest-dom": "^6.4.8",
"@testing-library/react": "^13.4.0",
"@testing-library/react": "^15.0.6",
"@testing-library/user-event": "^13.5.0",
"@types/archiver": "^6.0.2",
"@types/arrify": "^1.0.4",
Expand All @@ -297,6 +297,7 @@
"@types/semver": "^6.2.3",
"@types/tar-fs": "^2.0.1",
"@vvo/tzdb": "6.137.0",
"blob-polyfill": "^9.0.20240710",
"date-fns-tz": "2.0.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
Expand Down
2 changes: 1 addition & 1 deletion packages/sanity/src/core/field/paths/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export function pathToString(path: Path): string {
/** @internal */
export function getValueAtPath(rootValue: unknown, path: Path): unknown {
const segment = path[0]
if (!segment) {
if (typeof segment === 'undefined') {
return rootValue
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
AddDocumentIcon,
CloseIcon,
CopyIcon as DuplicateIcon,
CopyIcon,
LaunchIcon as OpenInNewTabIcon,
SyncIcon as ReplaceIcon,
TrashIcon,
Expand Down Expand Up @@ -81,6 +82,7 @@ export function ReferenceItem<Item extends ReferenceItemValue = ReferenceItemVal
value,
open,
onInsert,
onCopy,
presence,
validation,
inputId,
Expand Down Expand Up @@ -126,6 +128,12 @@ export function ReferenceItem<Item extends ReferenceItemValue = ReferenceItemVal
})
}, [onInsert, value])

const handleCopy = useCallback(() => {
onCopy({
items: [{...value, _key: randomKey()}],
})
}, [onCopy, value])

const handleInsert = useCallback(
(pos: 'before' | 'after', insertType: SchemaType) => {
onInsert({
Expand Down Expand Up @@ -237,9 +245,14 @@ export function ReferenceItem<Item extends ReferenceItemValue = ReferenceItemVal
icon={hasRef && isEditing ? CloseIcon : ReplaceIcon}
onClick={handleReplace}
/>
<MenuItem
text={t('inputs.reference.action.copy')}
icon={CopyIcon}
onClick={handleCopy}
/>
<MenuItem
text={t('inputs.reference.action.duplicate')}
icon={DuplicateIcon}
icon={AddDocumentIcon}
onClick={handleDuplicate}
/>
{insertBefore.menuItem}
Expand All @@ -266,6 +279,7 @@ export function ReferenceItem<Item extends ReferenceItemValue = ReferenceItemVal
),
[
OpenLink,
handleCopy,
handleDuplicate,
handleReplace,
hasRef,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {CopyIcon as DuplicateIcon, TrashIcon} from '@sanity/icons'
import {AddDocumentIcon, CopyIcon, TrashIcon} from '@sanity/icons'
import {type SchemaType} from '@sanity/types'
import {Box, Card, type CardTone, Menu} from '@sanity/ui'
import {useCallback, useMemo, useRef, useState} from 'react'
Expand Down Expand Up @@ -73,6 +73,7 @@ export function GridItem<Item extends ObjectItem = ObjectItem>(props: GridItemPr
value,
open,
onInsert,
onCopy,
onFocus,
onOpen,
onClose,
Expand Down Expand Up @@ -116,6 +117,12 @@ export function GridItem<Item extends ObjectItem = ObjectItem>(props: GridItemPr
})
}, [onInsert, value])

const handleCopy = useCallback(() => {
onCopy({
items: [{...value, _key: randomKey()}],
})
}, [onCopy, value])

const handleInsert = useCallback(
(pos: 'before' | 'after', insertType: SchemaType) => {
onInsert({
Expand Down Expand Up @@ -177,9 +184,14 @@ export function GridItem<Item extends ObjectItem = ObjectItem>(props: GridItemPr
icon={TrashIcon}
onClick={onRemove}
/>
<MenuItem
text={t('inputs.array.action.copy')}
icon={CopyIcon}
onClick={handleCopy}
/>
<MenuItem
text={t('inputs.array.action.duplicate')}
icon={DuplicateIcon}
icon={AddDocumentIcon}
onClick={handleDuplicate}
/>
{insertBefore.menuItem}
Expand All @@ -192,7 +204,7 @@ export function GridItem<Item extends ObjectItem = ObjectItem>(props: GridItemPr
{insertAfter.popover}
</>
),
[insertBefore, insertAfter, handleDuplicate, onRemove, props.inputId, readOnly, t],
[insertBefore, insertAfter, handleCopy, handleDuplicate, onRemove, props.inputId, readOnly, t],
)

const tone = getTone({readOnly, hasErrors, hasWarnings})
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* eslint-disable no-nested-ternary, react/jsx-no-bind */
import {CopyIcon as DuplicateIcon, TrashIcon} from '@sanity/icons'
import {AddDocumentIcon, CopyIcon, TrashIcon} from '@sanity/icons'
import {type SchemaType} from '@sanity/types'
import {Box, Card, type CardTone, Menu} from '@sanity/ui'
import {useCallback, useMemo, useRef, useState} from 'react'
Expand Down Expand Up @@ -57,6 +57,7 @@ export function PreviewItem<Item extends ObjectItem = ObjectItem>(props: Preview
value,
open,
onInsert,
onCopy,
onFocus,
onOpen,
onClose,
Expand Down Expand Up @@ -100,6 +101,12 @@ export function PreviewItem<Item extends ObjectItem = ObjectItem>(props: Preview
})
}, [onInsert, value])

const handleCopy = useCallback(() => {
onCopy({
items: [{...value, _key: randomKey()}],
})
}, [onCopy, value])

const handleInsert = useCallback(
(pos: 'before' | 'after', insertType: SchemaType) => {
onInsert({
Expand Down Expand Up @@ -161,9 +168,14 @@ export function PreviewItem<Item extends ObjectItem = ObjectItem>(props: Preview
icon={TrashIcon}
onClick={onRemove}
/>
<MenuItem
text={t('inputs.array.action.copy')}
icon={CopyIcon}
onClick={handleCopy}
/>
<MenuItem
text={t('inputs.array.action.duplicate')}
icon={DuplicateIcon}
icon={AddDocumentIcon}
onClick={handleDuplicate}
/>
{insertBefore.menuItem}
Expand All @@ -176,7 +188,7 @@ export function PreviewItem<Item extends ObjectItem = ObjectItem>(props: Preview
{insertAfter.popover}
</>
),
[insertBefore, insertAfter, handleDuplicate, onRemove, props.inputId, readOnly, t],
[readOnly, insertBefore, insertAfter, props.inputId, t, onRemove, handleCopy, handleDuplicate],
)

const tone = getTone({readOnly, hasErrors, hasWarnings})
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {CopyIcon as DuplicateIcon, TrashIcon} from '@sanity/icons'
import {AddDocumentIcon, CopyIcon, 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'
Expand Down Expand Up @@ -29,6 +29,7 @@ export const ItemRow = forwardRef(function ItemRow(
value,
insertableTypes,
onInsert,
onCopy,
onRemove,
readOnly,
inputId,
Expand All @@ -52,6 +53,12 @@ export const ItemRow = forwardRef(function ItemRow(
if (value) onInsert({position: 'after', items: [value]})
}, [onInsert, value])

const handleCopy = useCallback(() => {
onCopy({
items: [value],
})
}, [onCopy, value])

const tone = useMemo(() => {
if (hasError) {
return 'critical'
Expand All @@ -78,9 +85,10 @@ export const ItemRow = forwardRef(function ItemRow(
icon={TrashIcon}
onClick={onRemove}
/>
<MenuItem text={t('inputs.array.action.copy')} icon={CopyIcon} onClick={handleCopy} />
<MenuItem
text={t('inputs.array.action.duplicate')}
icon={DuplicateIcon}
icon={AddDocumentIcon}
onClick={handleDuplicate}
/>
<InsertMenuGroups types={insertableTypes} onInsert={handleInsert} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,17 @@ import {tap} from 'rxjs/operators'

import {useTranslation} from '../../../../i18n'
import {useResolveInitialValueForType} from '../../../../store'
import {useCopyPaste} from '../../../../studio'
import {useGetFormValue} from '../../../contexts/GetFormValue'
import {useDidUpdate} from '../../../hooks/useDidUpdate'
import {insert, type PatchArg, PatchEvent, setIfMissing, unset} from '../../../patch'
import {type ArrayOfObjectsItemMember} from '../../../store'
import {isEmptyItem} from '../../../store/utils/isEmptyItem'
import {FormCallbacksProvider, useFormCallbacks} from '../../../studio/contexts/FormCallbacks'
import {
type ArrayInputCopyEvent,
type ArrayInputInsertEvent,
type FormDocumentValue,
type ObjectInputProps,
type ObjectItem,
type ObjectItemProps,
Expand Down Expand Up @@ -72,6 +76,8 @@ export function ArrayOfObjectsItem(props: MemberItemProps) {
onFieldGroupSelect,
} = useFormCallbacks()
const resolveInitialValue = useResolveInitialValueForType()
const getFormValue = useGetFormValue()
const {onCopy} = useCopyPaste()

useDidUpdate(member.item.focused, (hadFocus, hasFocus) => {
if (!hadFocus && hasFocus) {
Expand Down Expand Up @@ -154,6 +160,17 @@ export function ArrayOfObjectsItem(props: MemberItemProps) {
],
)

const handleCopy = useCallback(
(_: Omit<ArrayInputCopyEvent<ObjectItem>, 'referenceItem'>) => {
const documentValue = getFormValue([]) as FormDocumentValue
onCopy(member.item.path, documentValue, {
context: {source: 'arrayItem'},
patchType: 'append',
})
},
[getFormValue, onCopy, member.item.path],
)

const handleBlur = useCallback(() => {
onPathBlur(member.item.path)
}, [member.item.path, onPathBlur])
Expand Down Expand Up @@ -336,6 +353,7 @@ export function ArrayOfObjectsItem(props: MemberItemProps) {
schemaType: member.item.schemaType,
parentSchemaType: member.parentSchemaType,
onInsert: handleInsert,
onCopy: handleCopy,
onRemove,
presence: member.item.presence,
validation: member.item.validation,
Expand All @@ -360,7 +378,6 @@ export function ArrayOfObjectsItem(props: MemberItemProps) {
member.item.level,
member.item.value,
member.item.schemaType,
member.parentSchemaType,
member.item.presence,
member.item.validation,
member.item.readOnly,
Expand All @@ -370,8 +387,10 @@ export function ArrayOfObjectsItem(props: MemberItemProps) {
member.item.changed,
member.collapsible,
member.collapsed,
member.parentSchemaType,
member.open,
handleInsert,
handleCopy,
onRemove,
handleOpen,
handleClose,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@ import {isBooleanSchemaType, isNumberSchemaType, type SchemaType} from '@sanity/
import {type ChangeEvent, type FocusEvent, useCallback, useMemo, useRef, useState} from 'react'

import {type FIXME} from '../../../../FIXME'
import {useCopyPaste} from '../../../../studio'
import {useGetFormValue} from '../../../contexts/GetFormValue'
import {useDidUpdate} from '../../../hooks/useDidUpdate'
import {getEmptyValue} from '../../../inputs/arrays/ArrayOfPrimitivesInput/getEmptyValue'
import {insert, type PatchArg, PatchEvent, set, unset} from '../../../patch'
import {type ArrayOfPrimitivesItemMember} from '../../../store'
import {useFormCallbacks} from '../../../studio/contexts/FormCallbacks'
import {
type ArrayInputCopyEvent,
type FormDocumentValue,
type PrimitiveInputProps,
type PrimitiveItemProps,
type RenderArrayOfPrimitivesItemCallback,
Expand Down Expand Up @@ -39,6 +43,8 @@ export function ArrayOfPrimitivesItem(props: PrimitiveMemberItemProps) {
const [localValue, setLocalValue] = useState<undefined | string>()

const {onPathBlur, onPathFocus, onChange} = useFormCallbacks()
const getFormValue = useGetFormValue()
const {onCopy} = useCopyPaste()

useDidUpdate(member.item.focused, (hadFocus, hasFocus) => {
if (!hadFocus && hasFocus) {
Expand Down Expand Up @@ -169,6 +175,17 @@ export function ArrayOfPrimitivesItem(props: PrimitiveMemberItemProps) {
[member.index, onChange],
)

const handleCopy = useCallback(
(_: ArrayInputCopyEvent<unknown>) => {
const documentValue = getFormValue([]) as FormDocumentValue
onCopy(member.item.path, documentValue, {
context: {source: 'arrayItem'},
patchType: 'append',
})
},
[getFormValue, member.item.path, onCopy],
)

const itemProps = useMemo((): Omit<PrimitiveItemProps, 'renderDefault'> => {
return {
key: member.key,
Expand All @@ -180,6 +197,7 @@ export function ArrayOfPrimitivesItem(props: PrimitiveMemberItemProps) {
schemaType: member.item.schemaType as FIXME,
parentSchemaType: member.parentSchemaType,
onInsert,
onCopy: handleCopy,
onRemove,
presence: member.item.presence,
validation: member.item.validation,
Expand All @@ -205,6 +223,7 @@ export function ArrayOfPrimitivesItem(props: PrimitiveMemberItemProps) {
member.item.path,
member.parentSchemaType,
onInsert,
handleCopy,
onRemove,
handleFocus,
handleBlur,
Expand Down
Loading

0 comments on commit ea55826

Please sign in to comment.