mirror of
https://github.com/directus/directus.git
synced 2026-02-01 15:05:04 -05:00
Create input-autocomplete-api interface (#5524)
This commit is contained in:
@@ -142,7 +142,10 @@ export default defineComponent({
|
||||
...listeners,
|
||||
input: emitValue,
|
||||
keydown: processValue,
|
||||
blur: trimIfEnabled,
|
||||
blur: (e: Event) => {
|
||||
trimIfEnabled();
|
||||
listeners.blur?.(e);
|
||||
},
|
||||
}));
|
||||
|
||||
const hasClick = computed(() => {
|
||||
|
||||
138
app/src/interfaces/input-autocomplete-api/index.ts
Normal file
138
app/src/interfaces/input-autocomplete-api/index.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import { defineInterface } from '@/interfaces/define';
|
||||
import InterfaceInputAutocompleteAPI from './input-autocomplete-api.vue';
|
||||
|
||||
export default defineInterface({
|
||||
id: 'input-autocomplete-api',
|
||||
name: '$t:interfaces.input-autocomplete-api.input-autocomplete-api',
|
||||
description: '$t:interfaces.input-autocomplete-api.description',
|
||||
icon: 'find_in_page',
|
||||
component: InterfaceInputAutocompleteAPI,
|
||||
types: ['string', 'text'],
|
||||
groups: ['standard'],
|
||||
recommendedDisplays: ['formatted-value'],
|
||||
options: [
|
||||
{
|
||||
field: 'url',
|
||||
name: '$t:url',
|
||||
type: 'string',
|
||||
meta: {
|
||||
interface: 'input',
|
||||
options: {
|
||||
placeholder: 'https://example.com/search?q={{value}}',
|
||||
font: 'monospace',
|
||||
},
|
||||
width: 'full',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'resultsPath',
|
||||
name: '$t:interfaces.input-autocomplete-api.results_path',
|
||||
type: 'string',
|
||||
meta: {
|
||||
interface: 'input',
|
||||
options: {
|
||||
placeholder: 'result.predictions',
|
||||
font: 'monospace',
|
||||
},
|
||||
width: 'half',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'valuePath',
|
||||
name: '$t:interfaces.input-autocomplete-api.value_path',
|
||||
type: 'string',
|
||||
meta: {
|
||||
interface: 'input',
|
||||
options: {
|
||||
placeholder: 'structured_main_text',
|
||||
font: 'monospace',
|
||||
},
|
||||
width: 'half',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'trigger',
|
||||
name: '$t:interfaces.input-autocomplete-api.trigger',
|
||||
type: 'string',
|
||||
schema: {
|
||||
default_value: 'throttle',
|
||||
},
|
||||
meta: {
|
||||
width: 'half',
|
||||
interface: 'select-dropdown',
|
||||
options: {
|
||||
choices: [
|
||||
{
|
||||
text: 'Throttle',
|
||||
value: 'throttle',
|
||||
},
|
||||
{
|
||||
text: 'Debounce',
|
||||
value: 'debounce',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'rate',
|
||||
name: '$t:interfaces.input-autocomplete-api.rate',
|
||||
type: 'integer',
|
||||
schema: {
|
||||
default_value: 500,
|
||||
},
|
||||
meta: {
|
||||
width: 'half',
|
||||
interface: 'input',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'placeholder',
|
||||
name: '$t:placeholder',
|
||||
meta: {
|
||||
width: 'half',
|
||||
interface: 'input',
|
||||
options: {
|
||||
placeholder: '$t:enter_a_placeholder',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'font',
|
||||
name: '$t:font',
|
||||
type: 'string',
|
||||
meta: {
|
||||
width: 'half',
|
||||
interface: 'select-dropdown',
|
||||
options: {
|
||||
choices: [
|
||||
{ text: '$t:sans_serif', value: 'sans-serif' },
|
||||
{ text: '$t:monospace', value: 'monospace' },
|
||||
{ text: '$t:serif', value: 'serif' },
|
||||
],
|
||||
},
|
||||
},
|
||||
schema: {
|
||||
default_value: 'sans-serif',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'iconLeft',
|
||||
name: '$t:icon_left',
|
||||
type: 'string',
|
||||
meta: {
|
||||
width: 'half',
|
||||
interface: 'select-icon',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'iconRight',
|
||||
name: '$t:icon_right',
|
||||
type: 'string',
|
||||
meta: {
|
||||
width: 'half',
|
||||
interface: 'select-icon',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,145 @@
|
||||
<template>
|
||||
<v-notice type="warning" v-if="!url || !resultsPath || !valuePath">
|
||||
{{ $t('one_or_more_options_are_missing') }}
|
||||
</v-notice>
|
||||
<div v-else>
|
||||
<v-menu attached :disabled="disabled">
|
||||
<template #activator="{ activate, deactivate }">
|
||||
<v-input
|
||||
:placeholder="placeholder"
|
||||
:disabled="disabled"
|
||||
:class="font"
|
||||
:value="value"
|
||||
@input="onInput"
|
||||
@focus="activate"
|
||||
@blur="deactivate"
|
||||
>
|
||||
<template v-if="iconLeft" #prepend><v-icon :name="iconLeft" /></template>
|
||||
<template v-if="iconRight" #append><v-icon :name="iconRight" /></template>
|
||||
</v-input>
|
||||
</template>
|
||||
|
||||
<v-list v-if="results.length > 0">
|
||||
<v-list-item v-for="result of results" :key="result" @click="() => emitValue(result)">
|
||||
<v-list-item-content>{{ result }}</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, PropType } from '@vue/composition-api';
|
||||
import axios from 'axios';
|
||||
import { throttle, get, debounce } from 'lodash';
|
||||
import { render } from 'micromustache';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
value: {
|
||||
type: [String, Number],
|
||||
default: null,
|
||||
},
|
||||
url: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
resultsPath: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
valuePath: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
trigger: {
|
||||
type: String as PropType<'debounce' | 'throttle'>,
|
||||
default: 'throttle',
|
||||
},
|
||||
rate: {
|
||||
type: [Number, String],
|
||||
default: 500,
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
iconLeft: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
iconRight: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
font: {
|
||||
type: String as PropType<'sans-serif' | 'serif' | 'monospace'>,
|
||||
default: 'sans-serif',
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const results = ref<string[]>([]);
|
||||
|
||||
const fetchResultsRaw = async (value: string | null) => {
|
||||
if (!value) {
|
||||
results.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const url = render(props.url, { value });
|
||||
|
||||
try {
|
||||
const result = await axios.get(url);
|
||||
const resultsArray = get(result.data, props.resultsPath);
|
||||
|
||||
if (Array.isArray(resultsArray) === false) {
|
||||
console.warn(`Expected results type of array, "${typeof resultsArray}" recieved`);
|
||||
return;
|
||||
} else {
|
||||
results.value = resultsArray
|
||||
.map((result: Record<string, unknown>) => get(result, props.valuePath))
|
||||
.filter((val: unknown) => val);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(err);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchResults =
|
||||
props.trigger === 'debounce'
|
||||
? debounce(fetchResultsRaw, Number(props.rate))
|
||||
: throttle(fetchResultsRaw, Number(props.rate));
|
||||
|
||||
return { results, onInput, emitValue };
|
||||
|
||||
function onInput(value: string) {
|
||||
emitValue(value);
|
||||
fetchResults(value);
|
||||
}
|
||||
|
||||
function emitValue(value: string) {
|
||||
emit('input', value);
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.v-input {
|
||||
&.monospace {
|
||||
--v-input-font-family: var(--family-monospace);
|
||||
}
|
||||
|
||||
&.serif {
|
||||
--v-input-font-family: var(--family-serif);
|
||||
}
|
||||
|
||||
&.sans-serif {
|
||||
--v-input-font-family: var(--family-sans-serif);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -9,13 +9,16 @@
|
||||
:type="inputType"
|
||||
:class="font"
|
||||
:db-safe="dbSafe"
|
||||
@input="$listeners.input"
|
||||
:slug="slug"
|
||||
:min="min"
|
||||
:max="max"
|
||||
:step="step"
|
||||
@input="$listeners.input"
|
||||
>
|
||||
<template v-if="iconLeft" #prepend><v-icon :name="iconLeft" /></template>
|
||||
<template #append>
|
||||
<span
|
||||
v-if="percentageRemaining <= 20"
|
||||
v-if="percentageRemaining && percentageRemaining <= 20"
|
||||
class="remaining"
|
||||
:class="{
|
||||
warning: percentageRemaining < 10,
|
||||
@@ -39,7 +42,7 @@ import { defineComponent, PropType, computed } from '@vue/composition-api';
|
||||
export default defineComponent({
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
type: [String, Number],
|
||||
default: null,
|
||||
},
|
||||
type: {
|
||||
@@ -90,19 +93,36 @@ export default defineComponent({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
slug: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
min: {
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
max: {
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
step: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const charsRemaining = computed(() => {
|
||||
if (typeof props.value === 'number') return null;
|
||||
|
||||
if (!props.length) return null;
|
||||
if (!props.value) return null;
|
||||
return +props.length - props.value.length;
|
||||
});
|
||||
|
||||
const percentageRemaining = computed(() => {
|
||||
if (typeof props.value === 'number') return null;
|
||||
|
||||
if (!props.length) return false;
|
||||
if (!props.value) return false;
|
||||
return 100 - (props.value.length / +props.length) * 100;
|
||||
|
||||
@@ -617,6 +617,7 @@ settings_permissions: Roles & Permissions
|
||||
settings_project: Project Settings
|
||||
settings_webhooks: Webhooks
|
||||
settings_presets: Presets & Bookmarks
|
||||
one_or_more_options_are_missing: One or more options are missing
|
||||
scope: Scope
|
||||
select: Select...
|
||||
layout: Layout
|
||||
@@ -1042,6 +1043,13 @@ interfaces:
|
||||
toolbar: Toolbar
|
||||
custom_formats: Custom Formats
|
||||
options_override: Options Override
|
||||
input-autocomplete-api:
|
||||
input-autocomplete-api: Autocomplete Input (API)
|
||||
description: A search typeahead for external API values.
|
||||
results_path: Results Path
|
||||
value_path: Value Path
|
||||
trigger: Trigger
|
||||
rate: Rate
|
||||
displays:
|
||||
boolean:
|
||||
boolean: Boolean
|
||||
|
||||
Reference in New Issue
Block a user