Skip to content

Commit 53aa46b

Browse files
sjelfullskogsmaskinhermanwiknerricokahler
authored
feat(core): global copy paste (#6856)
* feat(form): copy paste of document and fields prototype edx-1263 Signed-off-by: Fred Carlsen <fred@sjelfull.no> * feat(core): add global copy paste provider Signed-off-by: Fred Carlsen <fred@sjelfull.no> * refactor(dev): use new copy paste api in actions Signed-off-by: Fred Carlsen <fred@sjelfull.no> * 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 <fred@sjelfull.no> * refactor(core): use clipboard when handling onCopy/onPaste Signed-off-by: Fred Carlsen <fred@sjelfull.no> * feat(core): add resolveSchemaTypeForPath utility Signed-off-by: Fred Carlsen <fred@sjelfull.no> * fix(core): pass onChange as props instead of importing from structure Signed-off-by: Fred Carlsen <fred@sjelfull.no> * test(core): add missing _type to test Signed-off-by: Fred Carlsen <fred@sjelfull.no> * feat(core): add support for copy/paste via ctrl-c/v Signed-off-by: Fred Carlsen <fred@sjelfull.no> * refactor(test-studio): use new copy paste signature in doc actions Signed-off-by: Fred Carlsen <fred@sjelfull.no> * feat(structure): add copy/paste actions into structure Signed-off-by: Fred Carlsen <fred@sjelfull.no> * 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 <fred@sjelfull.no> * feat(core): allow focus on objects Signed-off-by: Fred Carlsen <fred@sjelfull.no> * fix(core): skip handling copy event on selections Signed-off-by: Fred Carlsen <fred@sjelfull.no> * feat(core): highlight border on focused objects Signed-off-by: Fred Carlsen <fred@sjelfull.no> * fix(form): remove focus terminator from cdr focuspath fixes edx-1510 Signed-off-by: Fred Carlsen <fred@sjelfull.no> * feat(core): move copy paste field actions to core fixes edx-1512 fixes edx-1513 Signed-off-by: Fred Carlsen <fred@sjelfull.no> * fix(core): fix imports in actions Signed-off-by: Fred Carlsen <fred@sjelfull.no> * feat(core): add telemetry to copy paste hook fixes EDX-1508 Signed-off-by: Fred Carlsen <fred@sjelfull.no> * fix(core): limit object focus to children incld in array modals Signed-off-by: Fred Carlsen <fred@sjelfull.no> * test(form): add tests for copy pasting fields Signed-off-by: Fred Carlsen <fred@sjelfull.no> * 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 <fred@sjelfull.no> * test(form): add e2e tests for copy paste Signed-off-by: Fred Carlsen <fred@sjelfull.no> * feat(form): add test ids to field actions Signed-off-by: Fred Carlsen <fred@sjelfull.no> * fix(form): remove blur handling from object Signed-off-by: Fred Carlsen <fred@sjelfull.no> * fix(form): trigger keypress on object wrapper Signed-off-by: Fred Carlsen <fred@sjelfull.no> * test(form): attempt to stabilise e2e tests for copy paste Signed-off-by: Fred Carlsen <fred@sjelfull.no> * chore: upgrade playwright deps Signed-off-by: Fred Carlsen <fred@sjelfull.no> Signed-off-by: Fred Carlsen <fred@sjelfull.no> Signed-off-by: Fred Carlsen <fred@sjelfull.no> * test(form): add component tests for copy paste Signed-off-by: Fred Carlsen <fred@sjelfull.no> * fix(test): make sure fixure awaits setup Signed-off-by: Fred Carlsen <fred@sjelfull.no> * fix(test): allow. copy pasting permissions in playwright-ct config Signed-off-by: Fred Carlsen <fred@sjelfull.no> * test(form): stabilise copy paste e2e tests Signed-off-by: Fred Carlsen <fred@sjelfull.no> * fix(test): fix clipboard.writeText mock Signed-off-by: Fred Carlsen <fred@sjelfull.no> * refactor(test): remove e2e copypaste test in favour of component test Signed-off-by: Fred Carlsen <fred@sjelfull.no> * fix(test): fix assertion texts Signed-off-by: Fred Carlsen <fred@sjelfull.no> * refactor(form): align updated toast msg with the copy toast Signed-off-by: Fred Carlsen <fred@sjelfull.no> * fix(test): await filling out string inputs Signed-off-by: Fred Carlsen <fred@sjelfull.no> * chore(test): enable trace/video on retries in CI Signed-off-by: Fred Carlsen <fred@sjelfull.no> * feat(playwright-ct): add debug fixture Signed-off-by: Fred Carlsen <fred@sjelfull.no> * fix(test): more work to stabilise object tests Signed-off-by: Fred Carlsen <fred@sjelfull.no> * fix(form): filter out epmty ref objects fixes edx-1532 Signed-off-by: Fred Carlsen <fred@sjelfull.no> * fix(form): ignore copy event on text selection fixes edx-1534 Signed-off-by: Fred Carlsen <fred@sjelfull.no> * refactor(form): move copy/paste into document field action fixes edx-1451 Signed-off-by: Fred Carlsen <fred@sjelfull.no> * fix(form): prevent pasting image/file into the opposite type fixes edx-1531 Signed-off-by: Fred Carlsen <fred@sjelfull.no> * fix(form): validate option.accept on paste fixes edx-1518 Signed-off-by: Fred Carlsen <fred@sjelfull.no> * fix(form): fix styled import Signed-off-by: Fred Carlsen <fred@sjelfull.no> * fix(core): added suite of tests + fixes for coercions fixes edx-1517 fixes edx-1525 Signed-off-by: Fred Carlsen <fred@sjelfull.no> * 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 <fred@sjelfull.no> * fix(form): handle deeply nested paths in arrays Signed-off-by: Fred Carlsen <fred@sjelfull.no> * test(form): add failing test for deeply nested arrays Signed-off-by: Fred Carlsen <fred@sjelfull.no> * refactor(core): split out test schema Signed-off-by: Fred Carlsen <fred@sjelfull.no> * test(core): add test for copying documents with booleans Signed-off-by: Fred Carlsen <fred@sjelfull.no> * refactor(form): only copy and paste defined focus path fixes edx-1548 Signed-off-by: Fred Carlsen <fred@sjelfull.no> * chore(form): rename valueTransfer -> transferValue Signed-off-by: Fred Carlsen <fred@sjelfull.no> * fix(core): pass client options to remove notice Signed-off-by: Fred Carlsen <fred@sjelfull.no> * 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 <fred@sjelfull.no> * fix(core): skip pasting on empty focus path Signed-off-by: Fred Carlsen <fred@sjelfull.no> * refactor(core): only check read-only on root level schema also allow empty fixes edx-1556 fixes edx-1555 Signed-off-by: Fred Carlsen <fred@sjelfull.no> * fix(core): fix type validation for primitive array target fixes edx-1554 Signed-off-by: Fred Carlsen <fred@sjelfull.no> * fix(form): fix asset schema compatibility check on root Signed-off-by: Fred Carlsen <fred@sjelfull.no> * fix(core): fix potential race condition when setting document meta touches edx-1553 Signed-off-by: Fred Carlsen <fred@sjelfull.no> * fix(core): make a reference weak if _strengthenOnPublish is set fixes edx-1558 Signed-off-by: Fred Carlsen <fred@sjelfull.no> * fix(core): retain relationship between marks and markDefs fixes edx-1555 Signed-off-by: Fred Carlsen <fred@sjelfull.no> * feat(core): validate pasted reference against filter fixes edx-1560 Signed-off-by: Fred Carlsen <fred@sjelfull.no> * refactor(core): clean up document pane event handler Signed-off-by: Fred Carlsen <fred@sjelfull.no> * refactor(form): add subtle transition on object focus Signed-off-by: Fred Carlsen <fred@sjelfull.no> * 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 <fred@sjelfull.no> Co-authored-by: Per-Kristian Nordnes <per.kristian.nordnes@gmail.com> Co-authored-by: Herman Wikner <wiknerherman@gmail.com> Co-authored-by: Rico Kahler <ricokahler@gmail.com>
1 parent 27c13a5 commit 53aa46b

File tree

74 files changed

+5298
-178
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

74 files changed

+5298
-178
lines changed

dev/studio-e2e-testing/sanity.config.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@ import {muxInput} from 'sanity-plugin-mux-input'
77
import {imageAssetSource} from 'sanity-test-studio/assetSources'
88
import {resolveDocumentActions as documentActions} from 'sanity-test-studio/documentActions'
99
import {assistFieldActionGroup} from 'sanity-test-studio/fieldActions/assistFieldActionGroup'
10-
import {copyAction} from 'sanity-test-studio/fieldActions/copyAction'
11-
import {pasteAction} from 'sanity-test-studio/fieldActions/pasteAction'
1210
import {resolveInitialValueTemplates} from 'sanity-test-studio/initialValueTemplates'
1311
import {customInspector} from 'sanity-test-studio/inspectors/custom'
1412
import {languageFilter} from 'sanity-test-studio/plugins/language-filter'
@@ -53,7 +51,7 @@ export default defineConfig({
5351
},
5452
unstable_fieldActions: (prev, ctx) => {
5553
if (['fieldActionsTest', 'stringsTest'].includes(ctx.documentType)) {
56-
return [...prev, assistFieldActionGroup, copyAction, pasteAction]
54+
return [...prev, assistFieldActionGroup]
5755
}
5856

5957
return prev

dev/test-studio/fieldActions/copyAction.ts

Lines changed: 0 additions & 22 deletions
This file was deleted.

dev/test-studio/fieldActions/pasteAction.ts

Lines changed: 0 additions & 22 deletions
This file was deleted.

dev/test-studio/sanity.config.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,6 @@ import {
3838
import {GoogleLogo, TailwindLogo, VercelLogo} from './components/workspaceLogos'
3939
import {resolveDocumentActions as documentActions} from './documentActions'
4040
import {assistFieldActionGroup} from './fieldActions/assistFieldActionGroup'
41-
import {copyAction} from './fieldActions/copyAction'
42-
import {pasteAction} from './fieldActions/pasteAction'
4341
import {resolveInitialValueTemplates} from './initialValueTemplates'
4442
import {customInspector} from './inspectors/custom'
4543
import {testStudioLocaleBundles} from './locales'
@@ -89,11 +87,13 @@ const sharedSettings = definePlugin({
8987
return prev
9088
},
9189
unstable_fieldActions: (prev, ctx) => {
90+
const defaultActions = [...prev]
91+
9292
if (['fieldActionsTest', 'stringsTest'].includes(ctx.documentType)) {
93-
return [...prev, assistFieldActionGroup, copyAction, pasteAction]
93+
return [...defaultActions, assistFieldActionGroup]
9494
}
9595

96-
return prev
96+
return defaultActions
9797
},
9898
newDocumentOptions,
9999
comments: {

dev/test-studio/schema/standard/arrays.tsx

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {ImageIcon, OlistIcon} from '@sanity/icons'
2-
import {defineField, defineType} from 'sanity'
2+
import {defineArrayMember, defineField, defineType} from 'sanity'
33

44
export const topLevelArrayType = defineType({
55
name: 'topLevelArrayType',
@@ -143,7 +143,7 @@ export default defineType({
143143
},
144144
],
145145
},
146-
{
146+
defineField({
147147
name: 'arrayOfMultipleTypes',
148148
title: 'Array of multiple types',
149149
type: 'array',
@@ -155,7 +155,7 @@ export default defineType({
155155
{
156156
type: 'book',
157157
},
158-
{
158+
defineArrayMember({
159159
type: 'object',
160160
name: 'color',
161161
title: 'Color with a long title',
@@ -174,10 +174,32 @@ export default defineType({
174174
name: 'name',
175175
type: 'string',
176176
},
177+
defineField({
178+
name: 'nestedArray',
179+
title: 'Nested array',
180+
type: 'array',
181+
of: [
182+
defineArrayMember({
183+
type: 'object',
184+
name: 'color',
185+
title: 'Color with a long title',
186+
fields: [
187+
{
188+
name: 'title',
189+
type: 'string',
190+
},
191+
{
192+
name: 'name',
193+
type: 'string',
194+
},
195+
],
196+
}),
197+
],
198+
}),
177199
],
178-
},
200+
}),
179201
],
180-
},
202+
}),
181203
{
182204
name: 'arrayOfMultipleTypesPopover',
183205
title: 'Array of multiple types (modal.type=popover)',

dev/test-studio/schema/standard/portableText/allTheBellsAndWhistles.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,11 +225,31 @@ export const ptAllTheBellsAndWhistlesType = defineType({
225225
}),
226226
defineField({
227227
title: 'Box Content',
228-
name: 'body',
228+
name: 'content',
229229
type: 'array',
230230
of: [{type: 'block'}],
231231
validation: (rule) => rule.required().error('Must have content'),
232232
}),
233+
defineField({
234+
title: 'Nested object',
235+
name: 'nestedObject',
236+
type: 'object',
237+
fields: [
238+
defineField({
239+
name: 'title',
240+
title: 'Title',
241+
type: 'string',
242+
validation: (rule) => rule.required().warning('Should have a title'),
243+
}),
244+
defineField({
245+
title: 'Box Content',
246+
name: 'body',
247+
type: 'array',
248+
of: [{type: 'block'}],
249+
validation: (rule) => rule.required().error('Must have content'),
250+
}),
251+
],
252+
}),
233253
],
234254
components: {
235255
preview: InfoBoxPreview as any,

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@
102102
"@bjoerge/mutiny": "^0.5.3",
103103
"@google-cloud/storage": "^7.11.0",
104104
"@jest/globals": "^29.7.0",
105-
"@playwright/test": "1.41.2",
105+
"@playwright/test": "1.44.1",
106106
"@repo/package.config": "workspace:*",
107107
"@repo/tsconfig": "workspace:*",
108108
"@sanity/client": "^6.21.0",

packages/@sanity/types/src/schema/asserters.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import {
1010
type BooleanSchemaType,
1111
type DeprecatedSchemaType,
1212
type DeprecationConfiguration,
13+
type FileSchemaType,
14+
type ImageSchemaType,
1315
type NumberSchemaType,
1416
type ObjectSchemaType,
1517
type ReferenceSchemaType,
@@ -108,6 +110,16 @@ export function isReferenceSchemaType(type: unknown): type is ReferenceSchemaTyp
108110
return isRecord(type) && (type.name === 'reference' || isReferenceSchemaType(type.type))
109111
}
110112

113+
/** @internal */
114+
export function isImageSchemaType(type: unknown): type is ImageSchemaType {
115+
return isRecord(type) && (type.name === 'image' || isImageSchemaType(type.type))
116+
}
117+
118+
/** @internal */
119+
export function isFileSchemaType(type: unknown): type is FileSchemaType {
120+
return isRecord(type) && (type.name === 'file' || isFileSchemaType(type.type))
121+
}
122+
111123
/** @internal */
112124
export function isDeprecatedSchemaType<TSchemaType extends BaseSchemaType>(
113125
type: TSchemaType,

packages/sanity/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -256,8 +256,8 @@
256256
"devDependencies": {
257257
"@jest/expect": "^29.7.0",
258258
"@jest/globals": "^29.7.0",
259-
"@playwright/experimental-ct-react": "1.41.2",
260-
"@playwright/test": "1.41.2",
259+
"@playwright/experimental-ct-react": "1.44.1",
260+
"@playwright/test": "1.44.1",
261261
"@repo/package.config": "workspace:*",
262262
"@sanity/codegen": "3.49.0",
263263
"@sanity/generate-help-url": "^3.0.0",

packages/sanity/playwright-ct.config.ts

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {defineConfig, devices} from '@playwright/experimental-ct-react'
66
const TESTS_PATH = path.join(__dirname, 'playwright-ct', 'tests')
77
const HTML_REPORT_PATH = path.join(__dirname, 'playwright-ct', 'report')
88
const ARTIFACT_OUTPUT_PATH = path.join(__dirname, 'playwright-ct', 'results')
9+
const isCI = !!process.env.CI
910

1011
/**
1112
* See https://playwright.dev/docs/test-configuration.
@@ -36,18 +37,19 @@ export default defineConfig({
3637
],
3738

3839
/* Maximum time one test can run for. */
39-
timeout: 10 * 1000,
40+
timeout: 30 * 1000,
4041
expect: {
4142
// Maximum time expect() should wait for the condition to be met.
42-
timeout: 5 * 1000,
43+
timeout: 10 * 1000,
4344
},
4445

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

7072
/* Configure projects for major browsers */
7173
projects: [
72-
{name: 'chromium', use: {...devices['Desktop Chrome']}},
73-
{name: 'firefox', use: {...devices['Desktop Firefox']}},
74+
{
75+
name: 'chromium',
76+
use: {
77+
...devices['Desktop Chrome'],
78+
permissions: ['clipboard-read', 'clipboard-write'],
79+
contextOptions: {
80+
// chromium-specific permissions
81+
permissions: ['clipboard-read', 'clipboard-write'],
82+
},
83+
},
84+
},
85+
{
86+
name: 'firefox',
87+
use: {
88+
...devices['Desktop Firefox'],
89+
launchOptions: {
90+
firefoxUserPrefs: {
91+
'dom.events.asyncClipboard.readText': true,
92+
'dom.events.testing.asyncClipboard': true,
93+
},
94+
},
95+
},
96+
},
7497
{name: 'webkit', use: {...devices['Desktop Safari']}},
7598
],
7699
})
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import {test as base} from '@playwright/experimental-ct-react'
2+
3+
export const test = base.extend<{
4+
getClipboardItemByMimeTypeAsText: (mimeType: string) => Promise<string | null>
5+
setClipboardItems: (items: ClipboardItem[]) => Promise<void>
6+
getClipboardItems: () => Promise<ClipboardItem[]>
7+
getClipboardItemsAsText: () => Promise<string>
8+
}>({
9+
page: async ({page}, use) => {
10+
const setupClipboardMocks = async () => {
11+
await page.addInitScript(() => {
12+
const mockClipboard = {
13+
read: () => {
14+
return Promise.resolve((window as any).__clipboardItems)
15+
},
16+
write: (newItems: ClipboardItem[]) => {
17+
;(window as any).__clipboardItems = newItems
18+
19+
return Promise.resolve()
20+
},
21+
readText: () => {
22+
const items = (window as any).__clipboardItems as ClipboardItem[]
23+
const textItem = items.find((item) => item.types.includes('text/plain'))
24+
return textItem
25+
? textItem.getType('text/plain').then((blob: Blob) => blob.text())
26+
: Promise.resolve('')
27+
},
28+
writeText: (text: string) => {
29+
const textBlob = new Blob([text], {type: 'text/plain'})
30+
;(window as any).__clipboardItems = [new ClipboardItem({'text/plain': textBlob})]
31+
return Promise.resolve()
32+
},
33+
}
34+
Object.defineProperty(Object.getPrototypeOf(navigator), 'clipboard', {
35+
value: mockClipboard,
36+
writable: false,
37+
})
38+
;(window as any).__clipboardItems = []
39+
})
40+
}
41+
42+
await setupClipboardMocks()
43+
44+
page.on('framenavigated', async () => {
45+
await setupClipboardMocks()
46+
})
47+
48+
await use(page)
49+
},
50+
51+
setClipboardItems: async ({page}, use) => {
52+
await use(async (items: ClipboardItem[]) => {
53+
;(window as any).__clipboardItems = items
54+
})
55+
},
56+
57+
getClipboardItems: async ({page}, use) => {
58+
await use(() => {
59+
return page.evaluate(() => navigator.clipboard.read())
60+
})
61+
},
62+
63+
getClipboardItemsAsText: async ({page}, use) => {
64+
await use(async () => {
65+
return page.evaluate(async () => {
66+
const items = await navigator.clipboard.read()
67+
const textItem = items.find((item) => item.types.includes('text/plain'))
68+
69+
return textItem
70+
? textItem.getType('text/plain').then((blob: Blob) => blob.text())
71+
: Promise.resolve('')
72+
})
73+
})
74+
},
75+
76+
getClipboardItemByMimeTypeAsText: async ({page}, use) => {
77+
await use(async (mimeType: string) => {
78+
return page.evaluate(async (mime) => {
79+
const items = await navigator.clipboard.read()
80+
const textItem = items.find((item) => item.types.includes(mime))
81+
const content = textItem ? textItem.getType(mime).then((blob: Blob) => blob.text()) : null
82+
83+
return content
84+
}, mimeType)
85+
})
86+
},
87+
})
88+
89+
export const {expect} = test

0 commit comments

Comments
 (0)