change tagged-input-field component, allow to paste multiple values (#365)

This commit is contained in:
Christoph 2025-04-19 17:43:51 +02:00
parent 6681dfa96f
commit 1d94f6baaf
7 changed files with 304 additions and 117 deletions

View File

@ -12,6 +12,7 @@
"@fortawesome/fontawesome-free": "^6.7.2", "@fortawesome/fontawesome-free": "^6.7.2",
"@kyvg/vue3-notification": "^3.4.1", "@kyvg/vue3-notification": "^3.4.1",
"@popperjs/core": "^2.11.8", "@popperjs/core": "^2.11.8",
"@vojtechlanka/vue-tags-input": "^3.1.1",
"bootstrap": "^5.3.5", "bootstrap": "^5.3.5",
"bootswatch": "^5.3.5", "bootswatch": "^5.3.5",
"flag-icons": "^7.3.2", "flag-icons": "^7.3.2",
@ -23,8 +24,7 @@
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-i18n": "^11.1.3", "vue-i18n": "^11.1.3",
"vue-prism-component": "github:h44z/vue-prism-component", "vue-prism-component": "github:h44z/vue-prism-component",
"vue-router": "^4.5.0", "vue-router": "^4.5.0"
"vue3-tags-input": "^1.0.12"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^5.2.3", "@vitejs/plugin-vue": "^5.2.3",
@ -884,6 +884,20 @@
"vue": "^3.2.25" "vue": "^3.2.25"
} }
}, },
"node_modules/@vojtechlanka/vue-tags-input": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@vojtechlanka/vue-tags-input/-/vue-tags-input-3.1.1.tgz",
"integrity": "sha512-GdREECH+k2pQCKdbHHh4/IxRXje3QQ8rXzXd9/6L1kzGYXqHlG1tbRoi1qC7enph67/g2nvGaZfpqLuuW+CX3g==",
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"vue": "3.x",
"vuedraggable": "^4.1.0"
},
"peerDependencies": {
"vue": "3.x"
}
},
"node_modules/@vue/compiler-core": { "node_modules/@vue/compiler-core": {
"version": "3.5.13", "version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.13.tgz", "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.13.tgz",
@ -1070,15 +1084,6 @@
"node": ">=14" "node": ">=14"
} }
}, },
"node_modules/click-outside-vue3": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/click-outside-vue3/-/click-outside-vue3-4.0.1.tgz",
"integrity": "sha512-sbplNecrup5oGqA3o4bo8XmvHRT6q9fvw21Z67aDbTqB9M6LF7CuYLTlLvNtOgKU6W3zst5H5zJuEh4auqA34g==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/clone-regexp": { "node_modules/clone-regexp": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/clone-regexp/-/clone-regexp-3.0.0.tgz", "resolved": "https://registry.npmjs.org/clone-regexp/-/clone-regexp-3.0.0.tgz",
@ -1193,6 +1198,12 @@
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"license": "MIT"
},
"node_modules/fdir": { "node_modules/fdir": {
"version": "6.4.4", "version": "6.4.4",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz",
@ -1893,6 +1904,12 @@
"node": ">=14.0.0" "node": ">=14.0.0"
} }
}, },
"node_modules/sortablejs": {
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.14.0.tgz",
"integrity": "sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==",
"license": "MIT"
},
"node_modules/source-map-js": { "node_modules/source-map-js": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@ -2173,19 +2190,16 @@
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/vue3-tags-input": { "node_modules/vuedraggable": {
"version": "1.0.12", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/vue3-tags-input/-/vue3-tags-input-1.0.12.tgz", "resolved": "https://registry.npmjs.org/vuedraggable/-/vuedraggable-4.1.0.tgz",
"integrity": "sha512-s5rG+1W3M8+be0nd9H1nv/8WLjJOO6pShgVz8ALAqOiz3tDH5QhGrDH6fzD14ZjJNRWSa3bRBSXQwHEXffPQ6g==", "integrity": "sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"click-outside-vue3": "^4.0.1" "sortablejs": "1.14.0"
},
"engines": {
"node": ">=12"
}, },
"peerDependencies": { "peerDependencies": {
"vue": "^3.0.5" "vue": "^3.0.1"
} }
} }
} }

View File

@ -12,6 +12,7 @@
"@fortawesome/fontawesome-free": "^6.7.2", "@fortawesome/fontawesome-free": "^6.7.2",
"@kyvg/vue3-notification": "^3.4.1", "@kyvg/vue3-notification": "^3.4.1",
"@popperjs/core": "^2.11.8", "@popperjs/core": "^2.11.8",
"@vojtechlanka/vue-tags-input": "^3.1.1",
"bootstrap": "^5.3.5", "bootstrap": "^5.3.5",
"bootswatch": "^5.3.5", "bootswatch": "^5.3.5",
"flag-icons": "^7.3.2", "flag-icons": "^7.3.2",
@ -23,8 +24,7 @@
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-i18n": "^11.1.3", "vue-i18n": "^11.1.3",
"vue-prism-component": "github:h44z/vue-prism-component", "vue-prism-component": "github:h44z/vue-prism-component",
"vue-router": "^4.5.0", "vue-router": "^4.5.0"
"vue3-tags-input": "^1.0.12"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^5.2.3", "@vitejs/plugin-vue": "^5.2.3",

View File

@ -15,3 +15,85 @@ a.disabled {
.desc::after { .desc::after {
content: " ↓"; content: " ↓";
} }
/* style the background and the text color of the input ... */
.vue-tags-input {
max-width: 100% !important;
background-color: #f7f7f9 !important;
padding: 0 0;
}
.vue-tags-input .ti-input {
padding: 0 0;
border: none !important;
transition: border-bottom 200ms ease;
}
.vue-tags-input .ti-new-tag-input {
background: transparent;
color: var(--bs-body-color);
padding: 0.75rem 1.5rem !important;
}
/* style the placeholders color across all browser */
.vue-tags-input ::-webkit-input-placeholder {
color: var(--bs-secondary-color);
}
.vue-tags-input .ti-input::placeholder {
color: var(--bs-secondary-color);
}
.vue-tags-input ::-moz-placeholder {
color: var(--bs-secondary-color);
}
.vue-tags-input :-ms-input-placeholder {
color: var(--bs-secondary-color);
}
.vue-tags-input :-moz-placeholder {
color: var(--bs-secondary-color);
}
/* default styles for all the tags */
.vue-tags-input .ti-tag {
position: relative;
background: #ffffff;
border: 2px solid var(--bs-body-color);
margin: 6px;
color: var(--bs-body-color);
}
/* the styles if a tag is invalid */
.vue-tags-input .ti-tag.ti-invalid {
background-color: #e88a74;
}
/* if the user input is invalid, the input color should be red */
.vue-tags-input .ti-new-tag-input.ti-invalid {
color: #e88a74;
}
/* if a tag or the user input is a duplicate, it should be crossed out */
.vue-tags-input .ti-duplicate span,
.vue-tags-input .ti-new-tag-input.ti-duplicate {
text-decoration: line-through;
}
/* if the user presses backspace, the complete tag should be crossed out, to mark it for deletion */
.vue-tags-input .ti-tag:after {
transition: transform .2s;
position: absolute;
content: '';
height: 2px;
width: 108%;
left: -4%;
top: calc(50% - 1px);
background-color: #000;
transform: scaleX(0);
}
.vue-tags-input .ti-deletion-mark:after {
transform: scaleX(1);
}

View File

@ -4,7 +4,7 @@ import {interfaceStore} from "@/stores/interfaces";
import {computed, ref, watch} from "vue"; import {computed, ref, watch} from "vue";
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { notify } from "@kyvg/vue3-notification"; import { notify } from "@kyvg/vue3-notification";
import Vue3TagsInput from 'vue3-tags-input'; import { VueTagsInput } from '@vojtechlanka/vue-tags-input';
import { validateCIDR, validateIP, validateDomain } from '@/helpers/validators'; import { validateCIDR, validateIP, validateDomain } from '@/helpers/validators';
import isCidr from "is-cidr"; import isCidr from "is-cidr";
import {isIP} from 'is-ip'; import {isIP} from 'is-ip';
@ -38,6 +38,15 @@ const title = computed(() => {
return t("modals.interface-edit.headline-new") return t("modals.interface-edit.headline-new")
}) })
const currentTags = ref({
Addresses: "",
Dns: "",
DnsSearch: "",
PeerDefNetwork: "",
PeerDefAllowedIPs: "",
PeerDefDns: "",
PeerDefDnsSearch: ""
})
const formData = ref(freshInterface()) const formData = ref(freshInterface())
// functions // functions
@ -137,94 +146,94 @@ function close() {
function handleChangeAddresses(tags) { function handleChangeAddresses(tags) {
let validInput = true let validInput = true
tags.forEach(tag => { tags.forEach(tag => {
if(isCidr(tag) === 0) { if(isCidr(tag.text) === 0) {
validInput = false validInput = false
notify({ notify({
title: "Invalid CIDR", title: "Invalid CIDR",
text: tag + " is not a valid IP address", text: tag.text + " is not a valid IP address",
type: 'error', type: 'error',
}) })
} }
}) })
if(validInput) { if(validInput) {
formData.value.Addresses = tags formData.value.Addresses = tags.map(tag => tag.text)
} }
} }
function handleChangeDns(tags) { function handleChangeDns(tags) {
let validInput = true let validInput = true
tags.forEach(tag => { tags.forEach(tag => {
if(!isIP(tag)) { if(!isIP(tag.text)) {
validInput = false validInput = false
notify({ notify({
title: "Invalid IP", title: "Invalid IP",
text: tag + " is not a valid IP address", text: tag.text + " is not a valid IP address",
type: 'error', type: 'error',
}) })
} }
}) })
if(validInput) { if(validInput) {
formData.value.Dns = tags formData.value.Dns = tags.map(tag => tag.text)
} }
} }
function handleChangeDnsSearch(tags) { function handleChangeDnsSearch(tags) {
formData.value.DnsSearch = tags formData.value.DnsSearch = tags.map(tag => tag.text)
} }
function handleChangePeerDefNetwork(tags) { function handleChangePeerDefNetwork(tags) {
let validInput = true let validInput = true
tags.forEach(tag => { tags.forEach(tag => {
if(isCidr(tag) === 0) { if(isCidr(tag.text) === 0) {
validInput = false validInput = false
notify({ notify({
title: "Invalid CIDR", title: "Invalid CIDR",
text: tag + " is not a valid IP address", text: tag.text + " is not a valid IP address",
type: 'error', type: 'error',
}) })
} }
}) })
if(validInput) { if(validInput) {
formData.value.PeerDefNetwork = tags formData.value.PeerDefNetwork = tags.map(tag => tag.text)
} }
} }
function handleChangePeerDefAllowedIPs(tags) { function handleChangePeerDefAllowedIPs(tags) {
let validInput = true let validInput = true
tags.forEach(tag => { tags.forEach(tag => {
if(isCidr(tag) === 0) { if(isCidr(tag.text) === 0) {
validInput = false validInput = false
notify({ notify({
title: "Invalid CIDR", title: "Invalid CIDR",
text: tag + " is not a valid IP address", text: tag.text + " is not a valid IP address",
type: 'error', type: 'error',
}) })
} }
}) })
if(validInput) { if(validInput) {
formData.value.PeerDefAllowedIPs = tags formData.value.PeerDefAllowedIPs = tags.map(tag => tag.text)
} }
} }
function handleChangePeerDefDns(tags) { function handleChangePeerDefDns(tags) {
let validInput = true let validInput = true
tags.forEach(tag => { tags.forEach(tag => {
if(!isIP(tag)) { if(!isIP(tag.text)) {
validInput = false validInput = false
notify({ notify({
title: "Invalid IP", title: "Invalid IP",
text: tag + " is not a valid IP address", text: tag.text + " is not a valid IP address",
type: 'error', type: 'error',
}) })
} }
}) })
if(validInput) { if(validInput) {
formData.value.PeerDefDns = tags formData.value.PeerDefDns = tags.map(tag => tag.text)
} }
} }
function handleChangePeerDefDnsSearch(tags) { function handleChangePeerDefDnsSearch(tags) {
formData.value.PeerDefDnsSearch = tags formData.value.PeerDefDnsSearch = tags.map(tag => tag.text)
} }
async function save() { async function save() {
@ -333,11 +342,15 @@ async function del() {
<legend class="mt-4">{{ $t('modals.interface-edit.header-network') }}</legend> <legend class="mt-4">{{ $t('modals.interface-edit.header-network') }}</legend>
<div class="form-group"> <div class="form-group">
<label class="form-label mt-4">{{ $t('modals.interface-edit.ip.label') }}</label> <label class="form-label mt-4">{{ $t('modals.interface-edit.ip.label') }}</label>
<vue3-tags-input class="form-control" :tags="formData.Addresses" <vue-tags-input class="form-control" v-model="currentTags.Addresses"
:tags="formData.Addresses.map(str => ({ text: str }))"
:placeholder="$t('modals.interface-edit.ip.placeholder')" :placeholder="$t('modals.interface-edit.ip.placeholder')"
:add-tag-on-keys="[13, 188, 32, 9]" :validation="validateCIDR()"
:validate="validateCIDR" :add-on-key="[13, 188, 32, 9]"
@on-tags-changed="handleChangeAddresses"/> :save-on-key="[13, 188, 32, 9]"
:allow-edit-tags="true"
:separators="[',', ';', ' ']"
@tags-changed="handleChangeAddresses"/>
</div> </div>
<div v-if="formData.Mode==='server'" class="form-group"> <div v-if="formData.Mode==='server'" class="form-group">
<label class="form-label mt-4">{{ $t('modals.interface-edit.listen-port.label') }}</label> <label class="form-label mt-4">{{ $t('modals.interface-edit.listen-port.label') }}</label>
@ -345,19 +358,27 @@ async function del() {
</div> </div>
<div v-if="formData.Mode!=='server'" class="form-group"> <div v-if="formData.Mode!=='server'" class="form-group">
<label class="form-label mt-4">{{ $t('modals.interface-edit.dns.label') }}</label> <label class="form-label mt-4">{{ $t('modals.interface-edit.dns.label') }}</label>
<vue3-tags-input class="form-control" :tags="formData.Dns" <vue-tags-input class="form-control" v-model="currentTags.Dns"
:tags="formData.Dns.map(str => ({ text: str }))"
:placeholder="$t('modals.interface-edit.dns.placeholder')" :placeholder="$t('modals.interface-edit.dns.placeholder')"
:add-tag-on-keys="[13, 188, 32, 9]" :validation="validateIP()"
:validate="validateIP" :add-on-key="[13, 188, 32, 9]"
@on-tags-changed="handleChangeDns"/> :save-on-key="[13, 188, 32, 9]"
:allow-edit-tags="true"
:separators="[',', ';', ' ']"
@tags-changed="handleChangeDns"/>
</div> </div>
<div v-if="formData.Mode!=='server'" class="form-group"> <div v-if="formData.Mode!=='server'" class="form-group">
<label class="form-label mt-4">{{ $t('modals.interface-edit.dns-search.label') }}</label> <label class="form-label mt-4">{{ $t('modals.interface-edit.dns-search.label') }}</label>
<vue3-tags-input class="form-control" :tags="formData.DnsSearch" <vue-tags-input class="form-control" v-model="currentTags.DnsSearch"
:tags="formData.DnsSearch.map(str => ({ text: str }))"
:placeholder="$t('modals.interface-edit.dns-search.placeholder')" :placeholder="$t('modals.interface-edit.dns-search.placeholder')"
:add-tag-on-keys="[13, 188, 32, 9]" :validation="validateDomain()"
:validate="validateDomain" :add-on-key="[13, 188, 32, 9]"
@on-tags-changed="handleChangeDnsSearch"/> :save-on-key="[13, 188, 32, 9]"
:allow-edit-tags="true"
:separators="[',', ';', ' ']"
@tags-changed="handleChangeDnsSearch"/>
</div> </div>
<div class="row"> <div class="row">
<div class="form-group col-md-6"> <div class="form-group col-md-6">
@ -420,36 +441,52 @@ async function del() {
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label mt-4">{{ $t('modals.interface-edit.defaults.networks.label') }}</label> <label class="form-label mt-4">{{ $t('modals.interface-edit.defaults.networks.label') }}</label>
<vue3-tags-input class="form-control" :tags="formData.PeerDefNetwork" <vue-tags-input class="form-control" v-model="currentTags.PeerDefNetwork"
:tags="formData.PeerDefNetwork.map(str => ({ text: str }))"
:placeholder="$t('modals.interface-edit.defaults.networks.placeholder')" :placeholder="$t('modals.interface-edit.defaults.networks.placeholder')"
:add-tag-on-keys="[13, 188, 32, 9]" :validation="validateCIDR()"
:validate="validateCIDR" :add-on-key="[13, 188, 32, 9]"
@on-tags-changed="handleChangePeerDefNetwork"/> :save-on-key="[13, 188, 32, 9]"
:allow-edit-tags="true"
:separators="[',', ';', ' ']"
@tags-changed="handleChangePeerDefNetwork"/>
<small class="form-text text-muted">{{ $t('modals.interface-edit.defaults.networks.description') }}</small> <small class="form-text text-muted">{{ $t('modals.interface-edit.defaults.networks.description') }}</small>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label mt-4">{{ $t('modals.interface-edit.defaults.allowed-ip.label') }}</label> <label class="form-label mt-4">{{ $t('modals.interface-edit.defaults.allowed-ip.label') }}</label>
<vue3-tags-input class="form-control" :tags="formData.PeerDefAllowedIPs" <vue-tags-input class="form-control" v-model="currentTags.PeerDefAllowedIPs"
:tags="formData.PeerDefAllowedIPs.map(str => ({ text: str }))"
:placeholder="$t('modals.interface-edit.defaults.allowed-ip.placeholder')" :placeholder="$t('modals.interface-edit.defaults.allowed-ip.placeholder')"
:add-tag-on-keys="[13, 188, 32, 9]" :validation="validateCIDR()"
:validate="validateCIDR" :add-on-key="[13, 188, 32, 9]"
@on-tags-changed="handleChangePeerDefAllowedIPs"/> :save-on-key="[13, 188, 32, 9]"
:allow-edit-tags="true"
:separators="[',', ';', ' ']"
@tags-changed="handleChangePeerDefAllowedIPs"/>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label mt-4">{{ $t('modals.interface-edit.dns.label') }}</label> <label class="form-label mt-4">{{ $t('modals.interface-edit.dns.label') }}</label>
<vue3-tags-input class="form-control" :tags="formData.PeerDefDns" <vue-tags-input class="form-control" v-model="currentTags.PeerDefDns"
:tags="formData.PeerDefDns.map(str => ({ text: str }))"
:placeholder="$t('modals.interface-edit.dns.placeholder')" :placeholder="$t('modals.interface-edit.dns.placeholder')"
:add-tag-on-keys="[13, 188, 32, 9]" :validation="validateIP()"
:validate="validateIP" :add-on-key="[13, 188, 32, 9]"
@on-tags-changed="handleChangePeerDefDns"/> :save-on-key="[13, 188, 32, 9]"
:allow-edit-tags="true"
:separators="[',', ';', ' ']"
@tags-changed="handleChangePeerDefDns"/>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label mt-4">{{ $t('modals.interface-edit.dns-search.label') }}</label> <label class="form-label mt-4">{{ $t('modals.interface-edit.dns-search.label') }}</label>
<vue3-tags-input class="form-control" :tags="formData.PeerDefDnsSearch" <vue-tags-input class="form-control" v-model="currentTags.PeerDefDnsSearch"
:tags="formData.PeerDefDnsSearch.map(str => ({ text: str }))"
:placeholder="$t('modals.interface-edit.dns-search.placeholder')" :placeholder="$t('modals.interface-edit.dns-search.placeholder')"
:add-tag-on-keys="[13, 188, 32, 9]" :validation="validateDomain()"
:validate="validateDomain" :add-on-key="[13, 188, 32, 9]"
@on-tags-changed="handleChangePeerDefDnsSearch"/> :save-on-key="[13, 188, 32, 9]"
:allow-edit-tags="true"
:separators="[',', ';', ' ']"
@tags-changed="handleChangePeerDefDnsSearch"/>
</div> </div>
<div class="row"> <div class="row">
<div class="form-group col-md-6"> <div class="form-group col-md-6">

View File

@ -5,7 +5,7 @@ import { interfaceStore } from "@/stores/interfaces";
import { computed, ref, watch } from "vue"; import { computed, ref, watch } from "vue";
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { notify } from "@kyvg/vue3-notification"; import { notify } from "@kyvg/vue3-notification";
import Vue3TagsInput from "vue3-tags-input"; import { VueTagsInput } from '@vojtechlanka/vue-tags-input';
import { validateCIDR, validateIP, validateDomain } from '@/helpers/validators'; import { validateCIDR, validateIP, validateDomain } from '@/helpers/validators';
import isCidr from "is-cidr"; import isCidr from "is-cidr";
import { isIP } from 'is-ip'; import { isIP } from 'is-ip';
@ -65,6 +65,13 @@ const title = computed(() => {
} }
}) })
const currentTags = ref({
Addresses: "",
AllowedIPs: "",
ExtraAllowedIPs: "",
Dns: "",
DnsSearch: ""
})
const formData = ref(freshPeer()) const formData = ref(freshPeer())
// functions // functions
@ -193,73 +200,73 @@ function close() {
function handleChangeAddresses(tags) { function handleChangeAddresses(tags) {
let validInput = true let validInput = true
tags.forEach(tag => { tags.forEach(tag => {
if (isCidr(tag) === 0) { if (isCidr(tag.text) === 0) {
validInput = false validInput = false
notify({ notify({
title: "Invalid CIDR", title: "Invalid CIDR",
text: tag + " is not a valid IP address", text: tag.text + " is not a valid IP address",
type: 'error', type: 'error',
}) })
} }
}) })
if (validInput) { if (validInput) {
formData.value.Addresses = tags formData.value.Addresses = tags.map(tag => tag.text)
} }
} }
function handleChangeAllowedIPs(tags) { function handleChangeAllowedIPs(tags) {
let validInput = true let validInput = true
tags.forEach(tag => { tags.forEach(tag => {
if (isCidr(tag) === 0) { if (isCidr(tag.text) === 0) {
validInput = false validInput = false
notify({ notify({
title: "Invalid CIDR", title: "Invalid CIDR",
text: tag + " is not a valid IP address", text: tag.text + " is not a valid IP address",
type: 'error', type: 'error',
}) })
} }
}) })
if (validInput) { if (validInput) {
formData.value.AllowedIPs.Value = tags formData.value.AllowedIPs.Value = tags.map(tag => tag.text)
} }
} }
function handleChangeExtraAllowedIPs(tags) { function handleChangeExtraAllowedIPs(tags) {
let validInput = true let validInput = true
tags.forEach(tag => { tags.forEach(tag => {
if (isCidr(tag) === 0) { if (isCidr(tag.text) === 0) {
validInput = false validInput = false
notify({ notify({
title: "Invalid CIDR", title: "Invalid CIDR",
text: tag + " is not a valid IP address", text: tag.text + " is not a valid IP address",
type: 'error', type: 'error',
}) })
} }
}) })
if (validInput) { if (validInput) {
formData.value.ExtraAllowedIPs = tags formData.value.ExtraAllowedIPs = tags.map(tag => tag.text)
} }
} }
function handleChangeDns(tags) { function handleChangeDns(tags) {
let validInput = true let validInput = true
tags.forEach(tag => { tags.forEach(tag => {
if (!isIP(tag)) { if (!isIP(tag.text)) {
validInput = false validInput = false
notify({ notify({
title: "Invalid IP", title: "Invalid IP",
text: tag + " is not a valid IP address", text: tag.text + " is not a valid IP address",
type: 'error', type: 'error',
}) })
} }
}) })
if (validInput) { if (validInput) {
formData.value.Dns.Value = tags formData.value.Dns.Value = tags.map(tag => tag.text)
} }
} }
function handleChangeDnsSearch(tags) { function handleChangeDnsSearch(tags) {
formData.value.DnsSearch.Value = tags formData.value.DnsSearch.Value = tags.map(tag => tag.text)
} }
async function save() { async function save() {
@ -344,34 +351,64 @@ async function del() {
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label mt-4">{{ $t('modals.peer-edit.ip.label') }}</label> <label class="form-label mt-4">{{ $t('modals.peer-edit.ip.label') }}</label>
<vue3-tags-input class="form-control" :tags="formData.Addresses" <vue-tags-input class="form-control" v-model="currentTags.Addresses"
:placeholder="$t('modals.peer-edit.ip.placeholder')" :add-tag-on-keys="[13, 188, 32, 9]" :tags="formData.Addresses.map(str => ({ text: str }))"
:validate="validateCIDR" @on-tags-changed="handleChangeAddresses" /> :placeholder="$t('modals.peer-edit.ip.placeholder')"
:validation="validateCIDR()"
:add-on-key="[13, 188, 32, 9]"
:save-on-key="[13, 188, 32, 9]"
:allow-edit-tags="true"
:separators="[',', ';', ' ']"
@tags-changed="handleChangeAddresses" />
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label mt-4">{{ $t('modals.peer-edit.allowed-ip.label') }}</label> <label class="form-label mt-4">{{ $t('modals.peer-edit.allowed-ip.label') }}</label>
<vue3-tags-input class="form-control" :tags="formData.AllowedIPs.Value" <vue-tags-input class="form-control" v-model="currentTags.AllowedIPs"
:placeholder="$t('modals.peer-edit.allowed-ip.placeholder')" :add-tag-on-keys="[13, 188, 32, 9]" :tags="formData.AllowedIPs.Value.map(str => ({ text: str }))"
:validate="validateCIDR" @on-tags-changed="handleChangeAllowedIPs" /> :placeholder="$t('modals.peer-edit.allowed-ip.placeholder')"
:validation="validateCIDR()"
:add-on-key="[13, 188, 32, 9]"
:save-on-key="[13, 188, 32, 9]"
:allow-edit-tags="true"
:separators="[',', ';', ' ']"
@tags-changed="handleChangeAllowedIPs" />
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label mt-4">{{ $t('modals.peer-edit.extra-allowed-ip.label') }}</label> <label class="form-label mt-4">{{ $t('modals.peer-edit.extra-allowed-ip.label') }}</label>
<vue3-tags-input class="form-control" :tags="formData.ExtraAllowedIPs" <vue-tags-input class="form-control" v-model="currentTags.ExtraAllowedIPs"
:placeholder="$t('modals.peer-edit.extra-allowed-ip.placeholder')" :add-tag-on-keys="[13, 188, 32, 9]" :tags="formData.ExtraAllowedIPs.map(str => ({ text: str }))"
:validate="validateCIDR" @on-tags-changed="handleChangeExtraAllowedIPs" /> :placeholder="$t('modals.peer-edit.extra-allowed-ip.placeholder')"
:validation="validateCIDR()"
:add-on-key="[13, 188, 32, 9]"
:save-on-key="[13, 188, 32, 9]"
:allow-edit-tags="true"
:separators="[',', ';', ' ']"
@tags-changed="handleChangeExtraAllowedIPs" />
<small class="form-text text-muted">{{ $t('modals.peer-edit.extra-allowed-ip.description') }}</small> <small class="form-text text-muted">{{ $t('modals.peer-edit.extra-allowed-ip.description') }}</small>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label mt-4">{{ $t('modals.peer-edit.dns.label') }}</label> <label class="form-label mt-4">{{ $t('modals.peer-edit.dns.label') }}</label>
<vue3-tags-input class="form-control" :tags="formData.Dns.Value" <vue-tags-input class="form-control" v-model="currentTags.Dns"
:placeholder="$t('modals.peer-edit.dns.placeholder')" :add-tag-on-keys="[13, 188, 32, 9]" :tags="formData.Dns.Value.map(str => ({ text: str }))"
:validate="validateIP" @on-tags-changed="handleChangeDns" /> :placeholder="$t('modals.peer-edit.dns.placeholder')"
:validation="validateIP()"
:add-on-key="[13, 188, 32, 9]"
:save-on-key="[13, 188, 32, 9]"
:allow-edit-tags="true"
:separators="[',', ';', ' ']"
@tags-changed="handleChangeDns" />
</div> </div>
<div hidden class="form-group"> <div class="form-group">
<label class="form-label mt-4">{{ $t('modals.peer-edit.dns-search.label') }}</label> <label class="form-label mt-4">{{ $t('modals.peer-edit.dns-search.label') }}</label>
<vue3-tags-input class="form-control" :tags="formData.DnsSearch.Value" <vue-tags-input class="form-control" v-model="currentTags.DnsSearch"
:placeholder="$t('modals.peer-edit.dns-search.label')" :add-tag-on-keys="[13, 188, 32, 9]" :tags="formData.DnsSearch.Value.map(str => ({ text: str }))"
:validate="validateDomain" @on-tags-changed="handleChangeDnsSearch" /> :placeholder="$t('modals.peer-edit.dns-search.label')"
:validation="validateDomain()"
:add-on-key="[13, 188, 32, 9]"
:save-on-key="[13, 188, 32, 9]"
:allow-edit-tags="true"
:separators="[',', ';', ' ']"
@tags-changed="handleChangeDnsSearch" />
</div> </div>
<div class="row"> <div class="row">
<div class="form-group col-md-6"> <div class="form-group col-md-6">

View File

@ -5,7 +5,7 @@ import {interfaceStore} from "@/stores/interfaces";
import {computed, ref} from "vue"; import {computed, ref} from "vue";
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { notify } from "@kyvg/vue3-notification"; import { notify } from "@kyvg/vue3-notification";
import Vue3TagsInput from "vue3-tags-input"; import { VueTagsInput } from '@vojtechlanka/vue-tags-input';
import { freshInterface } from '@/helpers/models'; import { freshInterface } from '@/helpers/models';
const { t } = useI18n() const { t } = useI18n()
@ -36,6 +36,7 @@ function freshForm() {
} }
} }
const currentTag = ref("")
const formData = ref(freshForm()) const formData = ref(freshForm())
const title = computed(() => { const title = computed(() => {
@ -55,7 +56,7 @@ function close() {
} }
function handleChangeUserIdentifiers(tags) { function handleChangeUserIdentifiers(tags) {
formData.value.Identifiers = tags formData.value.Identifiers = tags.map(tag => tag.text)
} }
async function save() { async function save() {
@ -89,10 +90,14 @@ async function save() {
<fieldset> <fieldset>
<div class="form-group"> <div class="form-group">
<label class="form-label mt-4">{{ $t('modals.peer-multi-create.identifiers.label') }}</label> <label class="form-label mt-4">{{ $t('modals.peer-multi-create.identifiers.label') }}</label>
<vue3-tags-input class="form-control" :tags="formData.Identifiers" <vue-tags-input class="form-control" v-model="currentTag"
:tags="formData.Identifiers.map(str => ({ text: str }))"
:placeholder="$t('modals.peer-multi-create.identifiers.placeholder')" :placeholder="$t('modals.peer-multi-create.identifiers.placeholder')"
:add-tag-on-keys="[13, 188, 32, 9]" :add-on-key="[13, 188, 32, 9]"
@on-tags-changed="handleChangeUserIdentifiers"/> :save-on-key="[13, 188, 32, 9]"
:allow-edit-tags="true"
:separators="[',', ';', ' ']"
@tags-changed="handleChangeUserIdentifiers"/>
<small class="form-text text-muted">{{ $t('modals.peer-multi-create.identifiers.description') }}</small> <small class="form-text text-muted">{{ $t('modals.peer-multi-create.identifiers.description') }}</small>
</div> </div>
<div class="form-group"> <div class="form-group">

View File

@ -1,14 +1,26 @@
import isCidr from "is-cidr"; import isCidr from "is-cidr";
import {isIP} from 'is-ip'; import {isIP} from 'is-ip';
export function validateCIDR(value) { export function validateCIDR() {
return isCidr(value) !== 0 return [{
classes: 'invalid-cidr',
rule: ({ text }) => isCidr(text) === 0,
disableAdd: true,
}]
} }
export function validateIP(value) { export function validateIP() {
return isIP(value) return [{
classes: 'invalid-ip',
rule: ({ text }) => !isIP(text),
disableAdd: true,
}]
} }
export function validateDomain(value) { export function validateDomain() {
return true return [{
classes: 'invalid-domain',
rule: tag => tag.text.length < 3,
disableAdd: true,
}]
} }