From 1d94f6baaf0dc9810ead8f68c04a4331d810140f Mon Sep 17 00:00:00 2001 From: Christoph Date: Sat, 19 Apr 2025 17:43:51 +0200 Subject: [PATCH] change tagged-input-field component, allow to paste multiple values (#365) --- frontend/package-lock.json | 54 ++++--- frontend/package.json | 4 +- frontend/src/assets/base.css | 82 ++++++++++ .../src/components/InterfaceEditModal.vue | 143 +++++++++++------- frontend/src/components/PeerEditModal.vue | 97 ++++++++---- .../src/components/PeerMultiCreateModal.vue | 17 ++- frontend/src/helpers/validators.js | 24 ++- 7 files changed, 304 insertions(+), 117 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 14a88fe..7f1fb2f 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -12,6 +12,7 @@ "@fortawesome/fontawesome-free": "^6.7.2", "@kyvg/vue3-notification": "^3.4.1", "@popperjs/core": "^2.11.8", + "@vojtechlanka/vue-tags-input": "^3.1.1", "bootstrap": "^5.3.5", "bootswatch": "^5.3.5", "flag-icons": "^7.3.2", @@ -23,8 +24,7 @@ "vue": "^3.5.13", "vue-i18n": "^11.1.3", "vue-prism-component": "github:h44z/vue-prism-component", - "vue-router": "^4.5.0", - "vue3-tags-input": "^1.0.12" + "vue-router": "^4.5.0" }, "devDependencies": { "@vitejs/plugin-vue": "^5.2.3", @@ -884,6 +884,20 @@ "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": { "version": "3.5.13", "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.13.tgz", @@ -1070,15 +1084,6 @@ "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": { "version": "3.0.0", "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==", "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": { "version": "6.4.4", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", @@ -1893,6 +1904,12 @@ "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": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -2173,19 +2190,16 @@ "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", "license": "MIT" }, - "node_modules/vue3-tags-input": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/vue3-tags-input/-/vue3-tags-input-1.0.12.tgz", - "integrity": "sha512-s5rG+1W3M8+be0nd9H1nv/8WLjJOO6pShgVz8ALAqOiz3tDH5QhGrDH6fzD14ZjJNRWSa3bRBSXQwHEXffPQ6g==", + "node_modules/vuedraggable": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/vuedraggable/-/vuedraggable-4.1.0.tgz", + "integrity": "sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==", "license": "MIT", "dependencies": { - "click-outside-vue3": "^4.0.1" - }, - "engines": { - "node": ">=12" + "sortablejs": "1.14.0" }, "peerDependencies": { - "vue": "^3.0.5" + "vue": "^3.0.1" } } } diff --git a/frontend/package.json b/frontend/package.json index 63d5d93..8e226b0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,6 +12,7 @@ "@fortawesome/fontawesome-free": "^6.7.2", "@kyvg/vue3-notification": "^3.4.1", "@popperjs/core": "^2.11.8", + "@vojtechlanka/vue-tags-input": "^3.1.1", "bootstrap": "^5.3.5", "bootswatch": "^5.3.5", "flag-icons": "^7.3.2", @@ -23,8 +24,7 @@ "vue": "^3.5.13", "vue-i18n": "^11.1.3", "vue-prism-component": "github:h44z/vue-prism-component", - "vue-router": "^4.5.0", - "vue3-tags-input": "^1.0.12" + "vue-router": "^4.5.0" }, "devDependencies": { "@vitejs/plugin-vue": "^5.2.3", diff --git a/frontend/src/assets/base.css b/frontend/src/assets/base.css index 13a3f7e..39092f5 100644 --- a/frontend/src/assets/base.css +++ b/frontend/src/assets/base.css @@ -15,3 +15,85 @@ a.disabled { .desc::after { 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); +} \ No newline at end of file diff --git a/frontend/src/components/InterfaceEditModal.vue b/frontend/src/components/InterfaceEditModal.vue index 0496566..69b2f82 100644 --- a/frontend/src/components/InterfaceEditModal.vue +++ b/frontend/src/components/InterfaceEditModal.vue @@ -4,7 +4,7 @@ import {interfaceStore} from "@/stores/interfaces"; import {computed, ref, watch} from "vue"; import { useI18n } from 'vue-i18n'; 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 isCidr from "is-cidr"; import {isIP} from 'is-ip'; @@ -38,6 +38,15 @@ const title = computed(() => { return t("modals.interface-edit.headline-new") }) +const currentTags = ref({ + Addresses: "", + Dns: "", + DnsSearch: "", + PeerDefNetwork: "", + PeerDefAllowedIPs: "", + PeerDefDns: "", + PeerDefDnsSearch: "" +}) const formData = ref(freshInterface()) // functions @@ -137,94 +146,94 @@ function close() { function handleChangeAddresses(tags) { let validInput = true tags.forEach(tag => { - if(isCidr(tag) === 0) { + if(isCidr(tag.text) === 0) { validInput = false notify({ title: "Invalid CIDR", - text: tag + " is not a valid IP address", + text: tag.text + " is not a valid IP address", type: 'error', }) } }) if(validInput) { - formData.value.Addresses = tags + formData.value.Addresses = tags.map(tag => tag.text) } } function handleChangeDns(tags) { let validInput = true tags.forEach(tag => { - if(!isIP(tag)) { + if(!isIP(tag.text)) { validInput = false notify({ title: "Invalid IP", - text: tag + " is not a valid IP address", + text: tag.text + " is not a valid IP address", type: 'error', }) } }) if(validInput) { - formData.value.Dns = tags + formData.value.Dns = tags.map(tag => tag.text) } } function handleChangeDnsSearch(tags) { - formData.value.DnsSearch = tags + formData.value.DnsSearch = tags.map(tag => tag.text) } function handleChangePeerDefNetwork(tags) { let validInput = true tags.forEach(tag => { - if(isCidr(tag) === 0) { + if(isCidr(tag.text) === 0) { validInput = false notify({ title: "Invalid CIDR", - text: tag + " is not a valid IP address", + text: tag.text + " is not a valid IP address", type: 'error', }) } }) if(validInput) { - formData.value.PeerDefNetwork = tags + formData.value.PeerDefNetwork = tags.map(tag => tag.text) } } function handleChangePeerDefAllowedIPs(tags) { let validInput = true tags.forEach(tag => { - if(isCidr(tag) === 0) { + if(isCidr(tag.text) === 0) { validInput = false notify({ title: "Invalid CIDR", - text: tag + " is not a valid IP address", + text: tag.text + " is not a valid IP address", type: 'error', }) } }) if(validInput) { - formData.value.PeerDefAllowedIPs = tags + formData.value.PeerDefAllowedIPs = tags.map(tag => tag.text) } } function handleChangePeerDefDns(tags) { let validInput = true tags.forEach(tag => { - if(!isIP(tag)) { + if(!isIP(tag.text)) { validInput = false notify({ title: "Invalid IP", - text: tag + " is not a valid IP address", + text: tag.text + " is not a valid IP address", type: 'error', }) } }) if(validInput) { - formData.value.PeerDefDns = tags + formData.value.PeerDefDns = tags.map(tag => tag.text) } } function handleChangePeerDefDnsSearch(tags) { - formData.value.PeerDefDnsSearch = tags + formData.value.PeerDefDnsSearch = tags.map(tag => tag.text) } async function save() { @@ -333,11 +342,15 @@ async function del() { {{ $t('modals.interface-edit.header-network') }}
- +
@@ -345,19 +358,27 @@ async function del() {
- +
- +
@@ -420,36 +441,52 @@ async function del() {
- + {{ $t('modals.interface-edit.defaults.networks.description') }}
- +
- +
- +
diff --git a/frontend/src/components/PeerEditModal.vue b/frontend/src/components/PeerEditModal.vue index e6e0bd8..ff9ae68 100644 --- a/frontend/src/components/PeerEditModal.vue +++ b/frontend/src/components/PeerEditModal.vue @@ -5,7 +5,7 @@ import { interfaceStore } from "@/stores/interfaces"; import { computed, ref, watch } from "vue"; import { useI18n } from 'vue-i18n'; 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 isCidr from "is-cidr"; import { isIP } from 'is-ip'; @@ -65,6 +65,13 @@ const title = computed(() => { } }) +const currentTags = ref({ + Addresses: "", + AllowedIPs: "", + ExtraAllowedIPs: "", + Dns: "", + DnsSearch: "" +}) const formData = ref(freshPeer()) // functions @@ -193,73 +200,73 @@ function close() { function handleChangeAddresses(tags) { let validInput = true tags.forEach(tag => { - if (isCidr(tag) === 0) { + if (isCidr(tag.text) === 0) { validInput = false notify({ title: "Invalid CIDR", - text: tag + " is not a valid IP address", + text: tag.text + " is not a valid IP address", type: 'error', }) } }) if (validInput) { - formData.value.Addresses = tags + formData.value.Addresses = tags.map(tag => tag.text) } } function handleChangeAllowedIPs(tags) { let validInput = true tags.forEach(tag => { - if (isCidr(tag) === 0) { + if (isCidr(tag.text) === 0) { validInput = false notify({ title: "Invalid CIDR", - text: tag + " is not a valid IP address", + text: tag.text + " is not a valid IP address", type: 'error', }) } }) if (validInput) { - formData.value.AllowedIPs.Value = tags + formData.value.AllowedIPs.Value = tags.map(tag => tag.text) } } function handleChangeExtraAllowedIPs(tags) { let validInput = true tags.forEach(tag => { - if (isCidr(tag) === 0) { + if (isCidr(tag.text) === 0) { validInput = false notify({ title: "Invalid CIDR", - text: tag + " is not a valid IP address", + text: tag.text + " is not a valid IP address", type: 'error', }) } }) if (validInput) { - formData.value.ExtraAllowedIPs = tags + formData.value.ExtraAllowedIPs = tags.map(tag => tag.text) } } function handleChangeDns(tags) { let validInput = true tags.forEach(tag => { - if (!isIP(tag)) { + if (!isIP(tag.text)) { validInput = false notify({ title: "Invalid IP", - text: tag + " is not a valid IP address", + text: tag.text + " is not a valid IP address", type: 'error', }) } }) if (validInput) { - formData.value.Dns.Value = tags + formData.value.Dns.Value = tags.map(tag => tag.text) } } function handleChangeDnsSearch(tags) { - formData.value.DnsSearch.Value = tags + formData.value.DnsSearch.Value = tags.map(tag => tag.text) } async function save() { @@ -344,34 +351,64 @@ async function del() {
- +
- +
- + {{ $t('modals.peer-edit.extra-allowed-ip.description') }}
- +
-