mirror of
https://github.com/directus/directus.git
synced 2026-01-26 10:17:57 -05:00
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:
@@ -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>"
|
||||
`;
|
||||
76
app/src/components/v-form/form-field-raw-editor.test.ts
Normal file
76
app/src/components/v-form/form-field-raw-editor.test.ts
Normal 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);
|
||||
});
|
||||
99
app/src/components/v-form/form-field-raw-editor.vue
Normal file
99
app/src/components/v-form/form-field-raw-editor.vue
Normal 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>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user