Create input-autocomplete-api interface (#5524)

This commit is contained in:
Rijk van Zanten
2021-05-06 18:37:42 -04:00
committed by GitHub
parent c4ae4b66cc
commit f0042a7a3d
5 changed files with 318 additions and 4 deletions

View File

@@ -142,7 +142,10 @@ export default defineComponent({
...listeners,
input: emitValue,
keydown: processValue,
blur: trimIfEnabled,
blur: (e: Event) => {
trimIfEnabled();
listeners.blur?.(e);
},
}));
const hasClick = computed(() => {

View 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',
},
},
],
});

View File

@@ -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>

View File

@@ -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;

View File

@@ -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