Fix and improve raw value editor (#15868)

* this works in the form-field setting up for refactor

* refactored the useRaw and made form-field-raw-editor

* add defaults

* add tests for render submitting and cancelling

* add isNil

* delete the comment

* add a cancel button

* change let to const

* add the if statement when it's not a object

* delete the .raw-value and place it in the raw-editor form field

* rename submit to setRawValue

* change submit to set-raw-value

* add a possibility to add a placeholder to the system-raw-editor

* implement the system-raw-editor to the form-field-raw-editor

* update the snapshot and fix the emitted tests

* found out we can disable the gutter and line-numbers

* add a language prop to the system when it's not defined it should default to mustache

* delete style; add language and add type

* update the html in tests

* add input-code for the extended validation

* add default value

* Update form-field-raw-editor.vue language to plaintext

Co-authored-by: Rijk van Zanten <rijkvanzanten@me.com>

* update test

Co-authored-by: Vincent Kempers <vincentkempers@vincents-mbp.lan>
Co-authored-by: Rijk van Zanten <rijkvanzanten@me.com>
This commit is contained in:
Vincent Kempers
2022-10-14 20:17:24 +02:00
committed by GitHub
parent 74b60c9154
commit 03d65e8363
5 changed files with 214 additions and 58 deletions

View File

@@ -0,0 +1,16 @@
// Vitest Snapshot v1
exports[`should render 1`] = `
"<v-dialog model-value=\\"true\\" persistent=\\"\\">
<v-card>
<v-card-title>edit_raw_value</v-card-title>
<v-card-text>
<interface-system-raw-editor type=\\"undefined\\" disabled=\\"false\\" language=\\"plaintext\\" placeholder=\\"enter_raw_value\\"></interface-system-raw-editor>
</v-card-text>
<v-card-actions>
<v-button secondary=\\"\\">cancel</v-button>
<v-button>done</v-button>
</v-card-actions>
</v-card>
</v-dialog>"
`;

View File

@@ -0,0 +1,76 @@
import { it, test, expect } from 'vitest';
import { mount } from '@vue/test-utils';
import { createI18n } from 'vue-i18n';
import formFieldRawEditor from './form-field-raw-editor.vue';
import { GlobalMountOptions } from '@vue/test-utils/dist/types';
const i18n = createI18n();
const global: GlobalMountOptions = {
plugins: [i18n],
};
test('should render', () => {
expect(formFieldRawEditor).toBeTruthy();
const wrapper = mount(formFieldRawEditor, {
props: {
showModal: true,
field: 'object',
disabled: false,
currentValue: '["id","new_content"]',
},
global,
});
expect(wrapper.html()).toMatchSnapshot();
});
// test if there is a value
test('submitting', async () => {
expect(formFieldRawEditor).toBeTruthy();
const wrapper = mount(formFieldRawEditor, {
props: {
showModal: true,
field: 'string',
disabled: false,
currentValue: 'things',
},
global,
});
const button = wrapper.findAll('v-button').at(1);
await button!.trigger('click');
await wrapper.vm.$nextTick();
expect(wrapper.emitted().setRawValue.length).toBe(1);
});
it('should cancel with keydown', async () => {
const wrapper = mount(formFieldRawEditor, {
props: {
showModal: true,
field: 'object',
disabled: false,
currentValue: '["id","new_content"]',
},
global,
});
await wrapper.trigger('esc');
await wrapper.vm.$nextTick();
expect(wrapper.emitted().cancel.length).toBe(1);
});
it('should cancel with the cancel button', async () => {
const wrapper = mount(formFieldRawEditor, {
props: {
showModal: true,
field: 'object',
disabled: false,
currentValue: '["id","new_content"]',
},
global,
});
const button = wrapper.findAll('v-button').at(0);
await button!.trigger('click');
await wrapper.vm.$nextTick();
expect(wrapper.emitted().cancel.length).toBe(1);
});

View File

@@ -0,0 +1,99 @@
<template>
<v-dialog :model-value="showModal" persistent @esc="$emit('cancel')">
<v-card>
<v-card-title>{{ disabled ? t('view_raw_value') : t('edit_raw_value') }}</v-card-title>
<v-card-text>
<interface-input-code
v-if="type === 'object'"
:value="internalValue"
:disabled="disabled"
:line-number="false"
:alt-options="{ gutters: false }"
:placeholder="t('enter_raw_value')"
language="json"
@input="internalValue = $event"
/>
<interface-system-raw-editor
v-else
:value="internalValue"
:type="type === 'string' ? 'text' : type"
:disabled="disabled"
language="plaintext"
:placeholder="t('enter_raw_value')"
@input="internalValue = $event"
/>
</v-card-text>
<v-card-actions>
<v-button secondary @click="$emit('cancel')">{{ t('cancel') }}</v-button>
<v-button @click.prevent="setRawValue">{{ t('done') }}</v-button>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script setup lang="ts">
import { getJSType } from '@/utils/get-js-type';
import { Field } from '@directus/shared/types';
import { isNil } from 'lodash';
import { computed, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
interface Props {
field: Field;
showModal: boolean;
disabled: boolean;
currentValue: unknown;
}
const props = withDefaults(defineProps<Props>(), {
showModal: false,
disabled: false,
currentValue: undefined,
});
const emit = defineEmits(['cancel', 'setRawValue']);
const { t } = useI18n();
const internalValue = ref();
const type = computed(() => {
return getJSType(props.field);
});
watch(
() => props.showModal,
(isActive) => {
if (isActive) {
if (isNil(props.currentValue)) {
return;
}
if (type.value === 'object') {
internalValue.value = JSON.stringify(props.currentValue, null, '\t');
} else {
internalValue.value = String(props.currentValue);
}
}
}
);
const setRawValue = () => {
switch (type.value) {
case 'string':
emit('setRawValue', internalValue.value);
break;
case 'number':
emit('setRawValue', Number(internalValue.value));
break;
case 'boolean':
emit('setRawValue', internalValue.value === 'true');
break;
case 'object':
emit('setRawValue', JSON.parse(internalValue.value));
break;
default:
emit('setRawValue', internalValue.value);
break;
}
};
</script>

View File

@@ -49,17 +49,14 @@
@set-field-value="$emit('setFieldValue', $event)"
/>
<v-dialog v-model="showRaw" @esc="showRaw = false">
<v-card>
<v-card-title>{{ isDisabled ? t('view_raw_value') : t('edit_raw_value') }}</v-card-title>
<v-card-text>
<v-textarea v-model="rawValue" :disabled="isDisabled" class="raw-value" :placeholder="t('enter_raw_value')" />
</v-card-text>
<v-card-actions>
<v-button @click="showRaw = false">{{ t('done') }}</v-button>
</v-card-actions>
</v-card>
</v-dialog>
<form-field-raw-editor
:show-modal="showRaw"
:field="field"
:current-value="internalValue"
:disabled="isDisabled"
@cancel="showRaw = false"
@set-raw-value="onRawValueSubmit"
/>
<small v-if="field.meta && field.meta.note" v-md="field.meta.note" class="type-note" />
@@ -74,7 +71,6 @@
</template>
<script setup lang="ts">
import { getJSType } from '@/utils/get-js-type';
import { Field, ValidationError } from '@directus/shared/types';
import { isEqual } from 'lodash';
import { computed, ref, watch } from 'vue';
@@ -84,6 +80,7 @@ import FormFieldLabel from './form-field-label.vue';
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';
interface Props {
field: Field;
@@ -131,7 +128,7 @@ const isDisabled = computed(() => {
const { internalValue, isEdited, defaultValue } = useComputedValues();
const { showRaw, rawValue, copyRaw, pasteRaw } = useRaw();
const { showRaw, copyRaw, pasteRaw, onRawValueSubmit } = useRaw();
const validationMessage = computed(() => {
if (!props.validationError) return null;
@@ -169,54 +166,22 @@ function useRaw() {
const { copyToClipboard, pasteFromClipboard } = useClipboard();
const type = computed(() => {
return getJSType(props.field);
});
const rawValue = computed({
get() {
switch (type.value) {
case 'object':
return JSON.stringify(internalValue.value, null, '\t');
case 'string':
case 'number':
case 'boolean':
default:
return internalValue.value;
}
},
set(newRawValue: string) {
switch (type.value) {
case 'string':
emit('update:modelValue', newRawValue);
break;
case 'number':
emit('update:modelValue', Number(newRawValue));
break;
case 'boolean':
emit('update:modelValue', newRawValue === 'true');
break;
case 'object':
emit('update:modelValue', JSON.parse(newRawValue));
break;
default:
emit('update:modelValue', newRawValue);
break;
}
},
});
function onRawValueSubmit(value: any) {
showRaw.value = false;
emitValue(value);
}
async function copyRaw() {
await copyToClipboard(rawValue.value);
await copyToClipboard(internalValue.value);
}
async function pasteRaw() {
const pastedValue = await pasteFromClipboard();
if (!pastedValue) return;
rawValue.value = pastedValue;
internalValue.value = pastedValue;
}
return { showRaw, rawValue, copyRaw, pasteRaw };
return { showRaw, copyRaw, pasteRaw, onRawValueSubmit };
}
function useComputedValues() {
@@ -282,10 +247,6 @@ function useComputedValues() {
font-style: italic;
}
.raw-value {
--v-textarea-font-family: var(--family-monospace);
}
.label-spacer {
height: 28px;
}

View File

@@ -18,12 +18,16 @@ const props = withDefaults(
autofocus?: boolean;
disabled?: boolean;
type?: string;
language?: string;
placeholder?: string;
}>(),
{
value: undefined,
autofocus: false,
disabled: false,
type: undefined,
placeholder: undefined,
language: 'mustache',
}
);
@@ -43,7 +47,7 @@ onMounted(async () => {
CodeMirror.defineSimpleMode('mustache', mustacheMode);
codemirror = CodeMirror(codemirrorEl.value, {
mode: 'mustache',
mode: props.language,
value: typeof props.value === 'object' ? JSON.stringify(props.value, null, 4) : String(props.value ?? ''),
tabSize: 0,
autoRefresh: true,
@@ -57,7 +61,7 @@ onMounted(async () => {
scrollbarStyle: isMultiLine.value ? 'native' : 'null',
extraKeys: { Ctrl: 'autocomplete' },
cursorBlinkRate: props.disabled ? -1 : 530,
placeholder: t('raw_editor_placeholder'),
placeholder: props.placeholder !== undefined ? props.placeholder : t('raw_editor_placeholder'),
});
// prevent new lines for single lines