update forms and select-like + fields validity

* avoid invalid popup when rendering input by setting valid by default and then check for validity
* remove useless props check for select and combobox
* add combobox for templates and plugins on Advanced forms
This commit is contained in:
Jordan Blasenhauer 2024-06-10 13:37:00 +02:00
parent c18c23e785
commit 351da6e99a
7 changed files with 120 additions and 63 deletions

View file

@ -4,8 +4,10 @@ import Container from "@components/Widget/Container.vue";
import Fields from "@components/Form/Fields.vue";
import Title from "@components/Widget/Title.vue";
import Subtitle from "@components/Widget/Subtitle.vue";
import Combobox from "@components/Forms/Field/Combobox.vue";
import { v4 as uuidv4 } from "uuid";
/**
/**
@name Form/Advanced.vue
@description This component is used to create a complete advanced form with plugin selection.
@example
@ -13,7 +15,6 @@ import Subtitle from "@components/Widget/Subtitle.vue";
{
name: "plugin name",
type: "pro",
is_activate: true,
description: "plugin description",
page: "/page",
settings: [
@ -43,7 +44,6 @@ import Subtitle from "@components/Widget/Subtitle.vue";
},
];
@param {object} forms - List of advanced forms that contains settings.
@param {boolean} [isActive=true] - Check if the form is active, it will display the form if true
*/
const props = defineProps({
@ -53,39 +53,108 @@ const props = defineProps({
required: true,
default: {},
},
isActive: {
type: Boolean,
required: false,
default: true,
},
});
const comboboxPlugin = {
id: uuidv4(),
name: uuidv4(),
disabled: false,
required: false,
label: "dashboard_plugins",
tabId: "1",
columns: { pc: 4, tablet: 6, mobile: 12 },
};
const comboboxTemplate = {
id: uuidv4(),
name: uuidv4(),
disabled: false,
required: false,
label: "dashboard_templates",
tabId: "1",
columns: { pc: 4, tablet: 6, mobile: 12 },
};
const data = reactive({
currTemplate: "",
currPlugin: "",
});
function getFirstTemplate() {
return Object.keys(props.forms)[0];
}
function getTemplateNames() {
return Object.keys(props.forms);
}
function getFirstPlugin(form) {
return form[0]["name"];
}
function getPluginNames(form) {
const pluginNames = [];
// Loop on each dict from form list
for (const plugin of form) {
// Return the first plugin
pluginNames.push(plugin.name);
}
return pluginNames;
}
onMounted(() => {
// Get first props.forms template name
data.currTemplate = getFirstTemplate();
// Get first plugin name
data.currPlugin = getFirstPlugin(props.forms[data.currTemplate]);
});
</script>
<template>
<Container
v-if="props.isActive"
:tag="'form'"
method="POST"
:containerClass="`col-span-12 w-full m-1 p-1`"
:columns="props.columns"
>
<Container v-for="form in props.forms">
<Container class="col-span-12 w-full" v-for="plugin in form">
<Title type="card" :title="plugin.name" />
<Subtitle type="card" :subtitle="plugin.description" />
<Container
style="max-height: 300px; overflow: auto"
class="grid grid-cols-12 w-full relative"
>
<template
v-for="(setting, name, index) in plugin.settings"
:key="index"
<template v-for="(template, template_name) in props.forms">
<Container
:containerClass="`col-span-12 grid grid-cols-12`"
v-if="template_name === data.currTemplate"
>
<Combobox
v-bind="comboboxTemplate"
:value="getFirstTemplate()"
:values="getTemplateNames()"
@inp="data.currPlugin = $event"
/>
<Combobox
v-bind="comboboxPlugin"
:value="getFirstPlugin(template)"
:values="getPluginNames(template)"
@inp="data.currPlugin = $event"
/>
<template v-for="plugin in template">
<Container
v-if="plugin.name === data.currPlugin"
class="col-span-12 w-full"
>
<Fields :setting="setting" />
</template>
</Container>
</Container>
</Container>
<Title type="card" :title="plugin.name" />
<Subtitle type="card" :subtitle="plugin.description" />
<Container
style="max-height: 300px; overflow: auto"
class="grid grid-cols-12 w-full relative"
>
<template
v-for="(setting, name, index) in plugin.settings"
:key="index"
>
<Fields :setting="setting" />
</template>
</Container>
</Container>
</template> </Container
></template>
</Container>
</template>

View file

@ -1,5 +1,5 @@
<script setup>
import { reactive, defineProps, onMounted, ref } from "vue";
import { reactive, defineProps, ref, onMounted } from "vue";
import { contentIndex } from "@utils/tabindex.js";
import Container from "@components/Widget/Container.vue";
import Header from "@components/Forms/Header/Field.vue";
@ -118,7 +118,7 @@ const checkboxEl = ref(null);
const checkbox = reactive({
value: props.value,
isValid: false,
isValid: true,
});
const emits = defineEmits(["inp"]);

View file

@ -29,7 +29,7 @@ import { v4 as uuidv4 } from "uuid";
iconColor: "info",
},]
}
@param {string} [id=uuidv4()] - Unique id
@param {string} [id=uuidv4()] - Unique id
@param {string} label - The label of the field. Can be a translation key or by default raw text.
@param {string} name - The name of the field. Case no label, this is the fallback. Can be a translation key or by default raw text.
@param {string} value
@ -124,19 +124,9 @@ const props = defineProps({
},
});
// When mounted or when props changed, we want select to display new props values
// When component value change itself, we want to switch to select.value
// To avoid component to send and stick to props values (bad behavior)
// Trick is to use select.value || props.value on template
watch(props, (newProp, oldProp) => {
if (newProp.value !== select.value) {
select.value = "";
}
});
const inp = reactive({
value: "",
isValid: false,
isValid: true,
});
const inputEl = ref();
@ -253,6 +243,7 @@ watch(select, () => {
});
onMounted(() => {
inp.isValid = inputEl.value.checkValidity();
selectWidth.value = `${selectBtn.value.clientWidth}px`;
window.addEventListener("resize", () => {
try {
@ -353,12 +344,7 @@ const emits = defineEmits(["inp"]);
ref="inputEl"
v-model="inp.value"
:placeholder="$t('inp_combobox_placeholder')"
@input="
() => {
inp.isValid = inputEl.checkValidity();
$emit('inp', inp.value);
}
"
@input="inp.isValid = inputEl.checkValidity()"
:aria-controls="`${props.id}-list`"
:id="`${props.id}-combobox`"
:class="[
@ -373,9 +359,15 @@ const emits = defineEmits(["inp"]);
/>
<div
class="select-dropdown-btn"
v-if="!props.values.some((str) => str.includes(inp.value))"
v-if="
!props.values.some((str) =>
str.toLowerCase().includes(inp.value.toLowerCase())
)
"
:aria-hidden="
!props.values.some((str) => str.includes(inp.value))
!props.values.some((str) =>
str.toLowerCase().includes(inp.value.toLowerCase())
)
? 'true'
: 'false'
"
@ -395,11 +387,15 @@ const emits = defineEmits(["inp"]);
>
<template v-for="(value, id) in props.values">
<button
v-if="value.includes(inp.value)"
:aria-hidden="value.includes(inp.value) ? 'false' : 'true'"
v-if="value.toLowerCase().includes(inp.value.toLowerCase())"
:aria-hidden="
value.toLowerCase().includes(inp.value.toLowerCase())
? 'false'
: 'true'
"
:tabindex="
select.isOpen
? value.includes(inp.value)
? value.toLowerCase().includes(inp.value.toLowerCase())
? props.tabId
: '-1'
: '-1'

View file

@ -137,7 +137,7 @@ const props = defineProps({
});
const date = reactive({
isValid: false,
isValid: true,
format: "m/d/Y H:i:S",
});

View file

@ -147,7 +147,7 @@ const inp = reactive({
value: props.value,
showInp: false,
isClipAllow: false,
isValid: false,
isValid: true,
});
const emits = defineEmits(["inp"]);
@ -170,6 +170,7 @@ function copyClipboard() {
onMounted(() => {
inp.isValid = inputEl.value.checkValidity();
// Clipboard not allowed on http
if (!window.location.href.startsWith("https://")) return;

View file

@ -125,16 +125,6 @@ const props = defineProps({
},
});
// When mounted or when props changed, we want select to display new props values
// When component value change itself, we want to switch to select.value
// To avoid component to send and stick to props values (bad behavior)
// Trick is to use select.value || props.value on template
watch(props, (newProp, oldProp) => {
if (newProp.value !== select.value) {
select.value = "";
}
});
const select = reactive({
isOpen: false,
// On mounted value is null to display props value

View file

@ -24,6 +24,7 @@
"dashboard_services": "services",
"dashboard_configs": "configs",
"dashboard_plugins": "plugins",
"dashboard_templates": "templates",
"dashboard_jobs": "jobs",
"dashboard_bans": "bans",
"dashboard_actions": "actions",