Skip to content

Commit

Permalink
feat(core): global copy paste (#6856)
Browse files Browse the repository at this point in the history
* feat(form): copy paste of document and fields prototype

edx-1263

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

* feat(core): add global copy paste provider

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

* refactor(dev): use new copy paste api in actions

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

* refactor(core): move value transfer to own function (tbc)

* test(core): add value transfer test for global copy/paste

* refactor: prepare for using clipboard insted of LS

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

* refactor(core): use clipboard when handling onCopy/onPaste

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

* feat(core): add resolveSchemaTypeForPath utility

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

* fix(core): pass onChange as props instead of importing from structure

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

* test(core): add missing _type to test

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

* feat(core): add support for copy/paste via ctrl-c/v

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

* refactor(test-studio): use new copy paste signature in doc actions

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

* feat(structure): add copy/paste actions into structure

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

* refactor(core): use new value transfer function for copy/paste + tests (#6878)

* refactor(core): support multiple sources and targets for copy/paste

* fix(core): change copy on copy/paste messaging

This will use the correct field name in the copy confirmation message

* fix(core): set new keys on object for copy/paste

Create new _key (if exists) for transferred object value

* feat(core): add focus handling to reference previews

fixes edx-1450

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

* feat(core): allow focus on objects

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

* fix(core): skip handling copy event on selections

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

* feat(core): highlight border on focused objects

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

* fix(form): remove focus terminator from cdr focuspath

fixes edx-1510

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

* feat(core): move copy paste field actions to core

fixes edx-1512
fixes edx-1513

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

* fix(core): fix imports in actions

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

* feat(core): add telemetry to copy paste hook

fixes EDX-1508

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

* fix(core): limit object focus to children incld in array modals

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

* test(form): add tests for copy pasting fields

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

* refactor(core): don't interfere with native editable elements + use clipboardItem (#6931)

* fix(core): don't show paste field action on readonly fields

* fix(core): copy paste string field must account for string lists

* fix(core): don't iterate on target schemaType if sourceValue is empty

* fix(core): quote copied fields in toast msg

* fix(core): adjust for weak/hard refs when pasting ref. values

* fix(core): fallback to text/plain clipboard item for Webkit

* fix(core): add check for non-browser env in helper

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

* test(form): add e2e tests for copy paste

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

* feat(form): add test ids to field actions

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

* fix(form): remove blur handling from object

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

* fix(form): trigger keypress on object wrapper

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

* test(form): attempt to stabilise e2e tests for copy paste

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

* chore: upgrade playwright deps

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

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

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

* test(form): add component tests for copy paste

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

* fix(test): make sure fixure awaits setup

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

* fix(test): allow. copy pasting permissions in playwright-ct config

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

* test(form): stabilise copy paste e2e tests

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

* fix(test): fix clipboard.writeText mock

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

* refactor(test): remove e2e copypaste test in favour of component test

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

* fix(test): fix assertion texts

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

* refactor(form): align updated toast msg with the copy toast

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

* fix(test): await filling out string inputs

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

* chore(test): enable trace/video on retries in CI

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

* feat(playwright-ct): add debug fixture

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

* fix(test): more work to stabilise object tests

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

* fix(form): filter out epmty ref objects

fixes edx-1532

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

* fix(form): ignore copy event on text selection

fixes edx-1534

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

* refactor(form): move copy/paste into document field action

fixes edx-1451

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

* fix(form): prevent pasting image/file into the opposite type

fixes edx-1531

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

* fix(form): validate option.accept on paste

fixes edx-1518

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

* fix(form): fix styled import

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

* fix(core): added suite of tests + fixes for coercions

fixes edx-1517
fixes edx-1525

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

* feat(core): translate copy-paste

* feat(core): translate MIME type copy-paste validation messages

* test(core): update `valueTransfer` test

* fix(core): copy-paste unknown copy error translation

* fix(form): transform error path to string with path utils

fixes edx-1543

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

* fix(form): handle deeply nested paths in arrays

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

* test(form): add failing test for deeply nested arrays

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

* refactor(core): split out test schema

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

* test(core): add test for copying documents with booleans

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

* refactor(form): only copy and paste defined focus path

fixes edx-1548

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

* chore(form): rename valueTransfer -> transferValue

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

* fix(core): pass client options to remove notice

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

* refactor(core): make reference type and image mime checks for copy/paste recursive

* test(core): update tests for copy/paste

Client must be mocked for ref. type checks

* test(core): validate schema before test runs

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

* fix(core): skip pasting on empty focus path

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

* refactor(core): only check read-only on root level schema

also allow empty

fixes edx-1556
fixes edx-1555

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

* fix(core): fix type validation for primitive array target

fixes edx-1554

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

* fix(form): fix asset schema compatibility check on root

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

* fix(core): fix potential race condition when setting document meta

touches edx-1553

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

* fix(core): make a reference weak if _strengthenOnPublish is set

fixes edx-1558

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

* fix(core): retain relationship between marks and markDefs

fixes edx-1555

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

* feat(core): validate pasted reference against filter

fixes edx-1560

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

* refactor(core): clean up document pane event handler

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

* refactor(form): add subtle transition on object focus

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

* refactor(core): serialize clipboard into HTML for safari and firefox

* refactor(core): rename and simplify `CopyActionResult` to `SanityClipboardItem`

* refactor(core): remove unused `copyResult`

* refactor(core): lift onCopy and onPaste into Provider

* refactor(core): remove unexpected cases

* fix(core): update tests

* fix: add missing return

* test: skip copy/paste tests for now

* refactor: remove unused interface

* test: update field to prevent collisions

* test: update component test timeouts

---------

Signed-off-by: Fred Carlsen <[email protected]>
Co-authored-by: Per-Kristian Nordnes <[email protected]>
Co-authored-by: Herman Wikner <[email protected]>
Co-authored-by: Rico Kahler <[email protected]>
  • Loading branch information
4 people authored Jul 11, 2024
1 parent 27c13a5 commit 53aa46b
Show file tree
Hide file tree
Showing 74 changed files with 5,298 additions and 178 deletions.
4 changes: 1 addition & 3 deletions dev/studio-e2e-testing/sanity.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ import {muxInput} from 'sanity-plugin-mux-input'
import {imageAssetSource} from 'sanity-test-studio/assetSources'
import {resolveDocumentActions as documentActions} from 'sanity-test-studio/documentActions'
import {assistFieldActionGroup} from 'sanity-test-studio/fieldActions/assistFieldActionGroup'
import {copyAction} from 'sanity-test-studio/fieldActions/copyAction'
import {pasteAction} from 'sanity-test-studio/fieldActions/pasteAction'
import {resolveInitialValueTemplates} from 'sanity-test-studio/initialValueTemplates'
import {customInspector} from 'sanity-test-studio/inspectors/custom'
import {languageFilter} from 'sanity-test-studio/plugins/language-filter'
Expand Down Expand Up @@ -53,7 +51,7 @@ export default defineConfig({
},
unstable_fieldActions: (prev, ctx) => {
if (['fieldActionsTest', 'stringsTest'].includes(ctx.documentType)) {
return [...prev, assistFieldActionGroup, copyAction, pasteAction]
return [...prev, assistFieldActionGroup]
}

return prev
Expand Down
22 changes: 0 additions & 22 deletions dev/test-studio/fieldActions/copyAction.ts

This file was deleted.

22 changes: 0 additions & 22 deletions dev/test-studio/fieldActions/pasteAction.ts

This file was deleted.

8 changes: 4 additions & 4 deletions dev/test-studio/sanity.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,6 @@ import {
import {GoogleLogo, TailwindLogo, VercelLogo} from './components/workspaceLogos'
import {resolveDocumentActions as documentActions} from './documentActions'
import {assistFieldActionGroup} from './fieldActions/assistFieldActionGroup'
import {copyAction} from './fieldActions/copyAction'
import {pasteAction} from './fieldActions/pasteAction'
import {resolveInitialValueTemplates} from './initialValueTemplates'
import {customInspector} from './inspectors/custom'
import {testStudioLocaleBundles} from './locales'
Expand Down Expand Up @@ -89,11 +87,13 @@ const sharedSettings = definePlugin({
return prev
},
unstable_fieldActions: (prev, ctx) => {
const defaultActions = [...prev]

if (['fieldActionsTest', 'stringsTest'].includes(ctx.documentType)) {
return [...prev, assistFieldActionGroup, copyAction, pasteAction]
return [...defaultActions, assistFieldActionGroup]
}

return prev
return defaultActions
},
newDocumentOptions,
comments: {
Expand Down
32 changes: 27 additions & 5 deletions dev/test-studio/schema/standard/arrays.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {ImageIcon, OlistIcon} from '@sanity/icons'
import {defineField, defineType} from 'sanity'
import {defineArrayMember, defineField, defineType} from 'sanity'

export const topLevelArrayType = defineType({
name: 'topLevelArrayType',
Expand Down Expand Up @@ -143,7 +143,7 @@ export default defineType({
},
],
},
{
defineField({
name: 'arrayOfMultipleTypes',
title: 'Array of multiple types',
type: 'array',
Expand All @@ -155,7 +155,7 @@ export default defineType({
{
type: 'book',
},
{
defineArrayMember({
type: 'object',
name: 'color',
title: 'Color with a long title',
Expand All @@ -174,10 +174,32 @@ export default defineType({
name: 'name',
type: 'string',
},
defineField({
name: 'nestedArray',
title: 'Nested array',
type: 'array',
of: [
defineArrayMember({
type: 'object',
name: 'color',
title: 'Color with a long title',
fields: [
{
name: 'title',
type: 'string',
},
{
name: 'name',
type: 'string',
},
],
}),
],
}),
],
},
}),
],
},
}),
{
name: 'arrayOfMultipleTypesPopover',
title: 'Array of multiple types (modal.type=popover)',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -225,11 +225,31 @@ export const ptAllTheBellsAndWhistlesType = defineType({
}),
defineField({
title: 'Box Content',
name: 'body',
name: 'content',
type: 'array',
of: [{type: 'block'}],
validation: (rule) => rule.required().error('Must have content'),
}),
defineField({
title: 'Nested object',
name: 'nestedObject',
type: 'object',
fields: [
defineField({
name: 'title',
title: 'Title',
type: 'string',
validation: (rule) => rule.required().warning('Should have a title'),
}),
defineField({
title: 'Box Content',
name: 'body',
type: 'array',
of: [{type: 'block'}],
validation: (rule) => rule.required().error('Must have content'),
}),
],
}),
],
components: {
preview: InfoBoxPreview as any,
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@
"@bjoerge/mutiny": "^0.5.3",
"@google-cloud/storage": "^7.11.0",
"@jest/globals": "^29.7.0",
"@playwright/test": "1.41.2",
"@playwright/test": "1.44.1",
"@repo/package.config": "workspace:*",
"@repo/tsconfig": "workspace:*",
"@sanity/client": "^6.21.0",
Expand Down
12 changes: 12 additions & 0 deletions packages/@sanity/types/src/schema/asserters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {
type BooleanSchemaType,
type DeprecatedSchemaType,
type DeprecationConfiguration,
type FileSchemaType,
type ImageSchemaType,
type NumberSchemaType,
type ObjectSchemaType,
type ReferenceSchemaType,
Expand Down Expand Up @@ -108,6 +110,16 @@ export function isReferenceSchemaType(type: unknown): type is ReferenceSchemaTyp
return isRecord(type) && (type.name === 'reference' || isReferenceSchemaType(type.type))
}

/** @internal */
export function isImageSchemaType(type: unknown): type is ImageSchemaType {
return isRecord(type) && (type.name === 'image' || isImageSchemaType(type.type))
}

/** @internal */
export function isFileSchemaType(type: unknown): type is FileSchemaType {
return isRecord(type) && (type.name === 'file' || isFileSchemaType(type.type))
}

/** @internal */
export function isDeprecatedSchemaType<TSchemaType extends BaseSchemaType>(
type: TSchemaType,
Expand Down
4 changes: 2 additions & 2 deletions packages/sanity/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -256,8 +256,8 @@
"devDependencies": {
"@jest/expect": "^29.7.0",
"@jest/globals": "^29.7.0",
"@playwright/experimental-ct-react": "1.41.2",
"@playwright/test": "1.41.2",
"@playwright/experimental-ct-react": "1.44.1",
"@playwright/test": "1.44.1",
"@repo/package.config": "workspace:*",
"@sanity/codegen": "3.49.0",
"@sanity/generate-help-url": "^3.0.0",
Expand Down
33 changes: 28 additions & 5 deletions packages/sanity/playwright-ct.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {defineConfig, devices} from '@playwright/experimental-ct-react'
const TESTS_PATH = path.join(__dirname, 'playwright-ct', 'tests')
const HTML_REPORT_PATH = path.join(__dirname, 'playwright-ct', 'report')
const ARTIFACT_OUTPUT_PATH = path.join(__dirname, 'playwright-ct', 'results')
const isCI = !!process.env.CI

/**
* See https://playwright.dev/docs/test-configuration.
Expand Down Expand Up @@ -36,18 +37,19 @@ export default defineConfig({
],

/* Maximum time one test can run for. */
timeout: 10 * 1000,
timeout: 30 * 1000,
expect: {
// Maximum time expect() should wait for the condition to be met.
timeout: 5 * 1000,
timeout: 10 * 1000,
},

/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
actionTimeout: 40 * 1000,
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
trace: isCI ? 'on-all-retries' : 'retain-on-failure',
video: isCI ? 'on-first-retry' : 'retain-on-failure',
/* Port to use for Playwright component endpoint. */
ctPort: 3100,
/* Configure Playwright vite config */
Expand All @@ -69,8 +71,29 @@ export default defineConfig({

/* Configure projects for major browsers */
projects: [
{name: 'chromium', use: {...devices['Desktop Chrome']}},
{name: 'firefox', use: {...devices['Desktop Firefox']}},
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
permissions: ['clipboard-read', 'clipboard-write'],
contextOptions: {
// chromium-specific permissions
permissions: ['clipboard-read', 'clipboard-write'],
},
},
},
{
name: 'firefox',
use: {
...devices['Desktop Firefox'],
launchOptions: {
firefoxUserPrefs: {
'dom.events.asyncClipboard.readText': true,
'dom.events.testing.asyncClipboard': true,
},
},
},
},
{name: 'webkit', use: {...devices['Desktop Safari']}},
],
})
89 changes: 89 additions & 0 deletions packages/sanity/playwright-ct/tests/fixtures/copyPasteFixture.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import {test as base} from '@playwright/experimental-ct-react'

export const test = base.extend<{
getClipboardItemByMimeTypeAsText: (mimeType: string) => Promise<string | null>
setClipboardItems: (items: ClipboardItem[]) => Promise<void>
getClipboardItems: () => Promise<ClipboardItem[]>
getClipboardItemsAsText: () => Promise<string>
}>({
page: async ({page}, use) => {
const setupClipboardMocks = async () => {
await page.addInitScript(() => {
const mockClipboard = {
read: () => {
return Promise.resolve((window as any).__clipboardItems)
},
write: (newItems: ClipboardItem[]) => {
;(window as any).__clipboardItems = newItems

return Promise.resolve()
},
readText: () => {
const items = (window as any).__clipboardItems as ClipboardItem[]
const textItem = items.find((item) => item.types.includes('text/plain'))
return textItem
? textItem.getType('text/plain').then((blob: Blob) => blob.text())
: Promise.resolve('')
},
writeText: (text: string) => {
const textBlob = new Blob([text], {type: 'text/plain'})
;(window as any).__clipboardItems = [new ClipboardItem({'text/plain': textBlob})]
return Promise.resolve()
},
}
Object.defineProperty(Object.getPrototypeOf(navigator), 'clipboard', {
value: mockClipboard,
writable: false,
})
;(window as any).__clipboardItems = []
})
}

await setupClipboardMocks()

page.on('framenavigated', async () => {
await setupClipboardMocks()
})

await use(page)
},

setClipboardItems: async ({page}, use) => {
await use(async (items: ClipboardItem[]) => {
;(window as any).__clipboardItems = items
})
},

getClipboardItems: async ({page}, use) => {
await use(() => {
return page.evaluate(() => navigator.clipboard.read())
})
},

getClipboardItemsAsText: async ({page}, use) => {
await use(async () => {
return page.evaluate(async () => {
const items = await navigator.clipboard.read()
const textItem = items.find((item) => item.types.includes('text/plain'))

return textItem
? textItem.getType('text/plain').then((blob: Blob) => blob.text())
: Promise.resolve('')
})
})
},

getClipboardItemByMimeTypeAsText: async ({page}, use) => {
await use(async (mimeType: string) => {
return page.evaluate(async (mime) => {
const items = await navigator.clipboard.read()
const textItem = items.find((item) => item.types.includes(mime))
const content = textItem ? textItem.getType(mime).then((blob: Blob) => blob.text()) : null

return content
}, mimeType)
})
},
})

export const {expect} = test
Loading

0 comments on commit 53aa46b

Please sign in to comment.