From 511c8d368bf17cd984926c08cc207f45ee72a34e Mon Sep 17 00:00:00 2001 From: Brainslug Date: Thu, 15 Dec 2022 10:40:43 +0100 Subject: [PATCH] Fix json serialization (#16558) * fix copying json fields * fixed preset filter type * handling fallback in copyToClipboard function * add test * try parsing json content on paste Co-authored-by: Azri Kahar <42867097+azrikahar@users.noreply.github.com> --- app/src/components/v-form/form-field.vue | 9 +- app/src/composables/use-clipboard.test.ts | 124 ++++++++++++++++++ app/src/composables/use-clipboard.ts | 5 +- .../modules/settings/routes/presets/item.vue | 2 +- 4 files changed, 135 insertions(+), 5 deletions(-) create mode 100644 app/src/composables/use-clipboard.test.ts diff --git a/app/src/components/v-form/form-field.vue b/app/src/components/v-form/form-field.vue index 8f8f8129f1..a1968527b4 100644 --- a/app/src/components/v-form/form-field.vue +++ b/app/src/components/v-form/form-field.vue @@ -81,6 +81,7 @@ import FormFieldMenu from './form-field-menu.vue'; import { formatFieldFunction } from '@/utils/format-field-function'; import { useClipboard } from '@/composables/use-clipboard'; import FormFieldRawEditor from './form-field-raw-editor.vue'; +import { parseJSON } from '@directus/shared/utils'; interface Props { field: Field; @@ -178,8 +179,12 @@ function useRaw() { async function pasteRaw() { const pastedValue = await pasteFromClipboard(); if (!pastedValue) return; - internalValue.value = pastedValue; - emitValue(pastedValue); + try { + internalValue.value = parseJSON(pastedValue); + } catch (e) { + internalValue.value = pastedValue; + } + emitValue(internalValue.value); } return { showRaw, copyRaw, pasteRaw, onRawValueSubmit }; diff --git a/app/src/composables/use-clipboard.test.ts b/app/src/composables/use-clipboard.test.ts new file mode 100644 index 0000000000..2c5a397871 --- /dev/null +++ b/app/src/composables/use-clipboard.test.ts @@ -0,0 +1,124 @@ +import { mount } from '@vue/test-utils'; +import { GlobalMountOptions } from '@vue/test-utils/dist/types'; +import { afterEach, beforeAll, describe, expect, test, vi } from 'vitest'; +import { defineComponent, h, unref } from 'vue'; +import { createI18n } from 'vue-i18n'; + +import { useClipboard } from './use-clipboard'; + +vi.mock('@/utils/notify', () => ({ + notify: vi.fn(), +})); + +const i18n = createI18n({ legacy: false }); + +const global: GlobalMountOptions = { + plugins: [i18n], +}; + +const testComponent = defineComponent({ + setup() { + return useClipboard(); + }, + render: () => h('div'), +}); + +describe('useClipboard', () => { + beforeAll(() => { + vi.spyOn(i18n.global, 't').mockImplementation((key) => key as any); + }); + + afterEach(() => { + vi.resetAllMocks(); + }); + + test.each([ + { value: { writeText: vi.fn() }, expectedResult: true }, + { value: {}, expectedResult: false }, + ])('isCopySupported should be $expectedResult', ({ value, expectedResult }) => { + Object.defineProperty(navigator, 'clipboard', { value, configurable: true }); + + const wrapper = mount(testComponent, { global }); + + expect(unref(wrapper.vm.isCopySupported)).toBe(expectedResult); + }); + + test.each([ + { value: { readText: vi.fn() }, expectedResult: true }, + { value: {}, expectedResult: false }, + ])('isPasteSupported should be $expectedResult', ({ value, expectedResult }) => { + Object.defineProperty(navigator, 'clipboard', { value, configurable: true }); + + const wrapper = mount(testComponent, { global }); + + expect(unref(wrapper.vm.isPasteSupported)).toBe(expectedResult); + }); + + test('copyToClipboard with string value returns true', async () => { + Object.defineProperty(navigator, 'clipboard', { value: { writeText: vi.fn() }, configurable: true }); + const writeTextSpy = vi.spyOn(navigator.clipboard, 'writeText'); + const copyValue = 'test'; + + const wrapper = mount(testComponent, { global }); + + const isCopied = await wrapper.vm.copyToClipboard(copyValue); + + expect(writeTextSpy).toHaveBeenCalledWith(copyValue); + expect(isCopied).toBe(true); + }); + + test('copyToClipboard with json value stringifies it and returns true', async () => { + Object.defineProperty(navigator, 'clipboard', { value: { writeText: vi.fn() }, configurable: true }); + const writeTextSpy = vi.spyOn(navigator.clipboard, 'writeText'); + const copyValue = { test: 'value' }; + + const wrapper = mount(testComponent, { global }); + + const isCopied = await wrapper.vm.copyToClipboard(copyValue); + + expect(writeTextSpy).toHaveBeenCalledWith(JSON.stringify(copyValue)); + expect(isCopied).toBe(true); + }); + + test('copyToClipboard returns false when it fails', async () => { + Object.defineProperty(navigator, 'clipboard', { + value: { writeText: vi.fn().mockImplementation(() => Promise.reject()) }, + configurable: true, + }); + const copyValue = 'test'; + + const wrapper = mount(testComponent, { global }); + + const isCopied = await wrapper.vm.copyToClipboard(copyValue); + + expect(isCopied).toBe(false); + }); + + test('pasteFromClipboard returns string when it succeeds', async () => { + const testClipboardValue = 'test'; + + Object.defineProperty(navigator, 'clipboard', { + value: { readText: vi.fn().mockReturnValue(testClipboardValue) }, + configurable: true, + }); + + const wrapper = mount(testComponent, { global }); + + const clipboardValue = await wrapper.vm.pasteFromClipboard(); + + expect(clipboardValue).toBe(testClipboardValue); + }); + + test('pasteFromClipboard returns null when it fails', async () => { + Object.defineProperty(navigator, 'clipboard', { + value: { readText: vi.fn().mockImplementation(() => Promise.reject()) }, + configurable: true, + }); + + const wrapper = mount(testComponent, { global }); + + const clipboardValue = await wrapper.vm.pasteFromClipboard(); + + expect(clipboardValue).toBe(null); + }); +}); diff --git a/app/src/composables/use-clipboard.ts b/app/src/composables/use-clipboard.ts index 77c2e17aa5..74b649f272 100644 --- a/app/src/composables/use-clipboard.ts +++ b/app/src/composables/use-clipboard.ts @@ -20,9 +20,10 @@ export function useClipboard() { return { isCopySupported, isPasteSupported, copyToClipboard, pasteFromClipboard }; - async function copyToClipboard(value: string, message?: Message): Promise { + async function copyToClipboard(value: any, message?: Message): Promise { try { - await navigator?.clipboard?.writeText(value); + const valueString = typeof value === 'string' ? value : JSON.stringify(value); + await navigator?.clipboard?.writeText(valueString); notify({ title: message?.success ?? t('copy_raw_value_success'), }); diff --git a/app/src/modules/settings/routes/presets/item.vue b/app/src/modules/settings/routes/presets/item.vue index f152076197..119461e3d0 100644 --- a/app/src/modules/settings/routes/presets/item.vue +++ b/app/src/modules/settings/routes/presets/item.vue @@ -528,7 +528,7 @@ function useForm() { { field: 'filter', name: t('filter'), - type: 'string', + type: 'json', meta: { interface: 'system-filter', width: 'half',