mirror of
https://github.com/UNITRONIX/BetterDesk.git
synced 2026-10-26 06:19:53 +00:00
feat(devices): manage rename and delete device tags
Add scoped tag rename/delete APIs and Devices UI (Manage tags modal, context menu, edit/detail hints) so operators can retire or rename tags without editing every peer. Refs #470 Thanks: INSOLVE (Honorary); Marco Jakobs (@jacotec); MyNameisStitch (@MyNameisStitch); Redspin (@playerumpknow); muopc (@muopc)
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- **Device tags management (#470):** Operators can rename or delete tags across all visible devices from Devices → Manage tags, unassign tags per device in Edit / Details / context menu, and matching dynamic group filters update on rename. Ships via panel update.
|
||||
|
||||
### Changed
|
||||
- _(none yet)_
|
||||
|
||||
|
||||
+22
-2
@@ -442,7 +442,26 @@
|
||||
"group_commercialization_assign_existing": "Assign existing package",
|
||||
"group_commercialization_create_new": "Create new package and assign",
|
||||
"group_commercialization_saved": "Package assigned to group",
|
||||
"group_one_only": "A device can belong to only one group"
|
||||
"group_one_only": "A device can belong to only one group",
|
||||
"manage_tags": "إدارة الوسوم",
|
||||
"manage_device_tags": "إدارة الوسوم",
|
||||
"rename_tag": "إعادة تسمية الوسم",
|
||||
"delete_tag": "حذف الوسم",
|
||||
"rename_tag_prompt": "اسم الوسم الجديد",
|
||||
"rename_tag_hint": "يعيد تسمية هذا الوسم على كل الأجهزة المرئية ويحدّث المجموعات الديناميكية المطابقة.",
|
||||
"delete_tag_confirm": "إزالة الوسم \"{tag}\" من {count} جهاز/أجهزة؟ ستصبح المجموعات الديناميكية التي تستخدمه فارغة حتى التحديث.",
|
||||
"tag_renamed": "تمت إعادة تسمية الوسم",
|
||||
"tag_deleted": "تم حذف الوسم",
|
||||
"tag_name_required": "اسم الوسم مطلوب",
|
||||
"tag_rename_identical": "يجب أن يكون الاسم الجديد مختلفاً",
|
||||
"tags_backend_unsupported": "وسوم الأجهزة غير مدعومة مع الخلفية الحالية",
|
||||
"tags_manage_hint": "أعد تسمية الوسوم أو احذفها على كل الأجهزة المرئية. الإزالة من جهاز واحد تتم في التعديل أو التفاصيل.",
|
||||
"tags_device_hint": "أزل وسماً هنا لإلغاء تعيينه من هذا الجهاز فقط. استخدم إدارة الوسوم لإعادة التسمية أو الحذف في كل مكان.",
|
||||
"unassign_tag": "إزالة من هذا الجهاز",
|
||||
"tag_device_one": "جهاز",
|
||||
"tag_device_many": "أجهزة",
|
||||
"tag_group_one": "مجموعة ديناميكية",
|
||||
"tag_group_many": "مجموعات ديناميكية"
|
||||
},
|
||||
"keys": {
|
||||
"title": "مفاتيح الخادم",
|
||||
@@ -1776,7 +1795,8 @@
|
||||
"sessions_col_duration": "المدة",
|
||||
"sessions_active": "نشطة",
|
||||
"sessions_transport_punch": "مباشر / punch",
|
||||
"sessions_transport_relay": "Relay"
|
||||
"sessions_transport_relay": "Relay",
|
||||
"tags_unassign_hint": "إزالة وسم هنا تلغي تعيينه من هذا الجهاز فقط. استخدم إدارة الوسوم في صفحة الأجهزة لإعادة التسمية أو الحذف في كل مكان."
|
||||
},
|
||||
"inventory": {
|
||||
"title": "Inventory",
|
||||
|
||||
+22
-2
@@ -435,7 +435,26 @@
|
||||
"group_commercialization_assign_existing": "Assign existing package",
|
||||
"group_commercialization_create_new": "Create new package and assign",
|
||||
"group_commercialization_saved": "Package assigned to group",
|
||||
"group_one_only": "A device can belong to only one group"
|
||||
"group_one_only": "A device can belong to only one group",
|
||||
"manage_tags": "Spravovat štítky",
|
||||
"manage_device_tags": "Spravovat štítky",
|
||||
"rename_tag": "Přejmenovat štítek",
|
||||
"delete_tag": "Smazat štítek",
|
||||
"rename_tag_prompt": "Nový název štítku",
|
||||
"rename_tag_hint": "Přejmenuje tento štítek na všech viditelných zařízeních a aktualizuje odpovídající dynamické skupiny.",
|
||||
"delete_tag_confirm": "Odebrat štítek \"{tag}\" z {count} zařízení? Dynamické skupiny používající tento štítek budou prázdné do aktualizace.",
|
||||
"tag_renamed": "Štítek přejmenován",
|
||||
"tag_deleted": "Štítek smazán",
|
||||
"tag_name_required": "Název štítku je povinný",
|
||||
"tag_rename_identical": "Nový název musí být jiný",
|
||||
"tags_backend_unsupported": "Štítky zařízení nejsou s aktuálním backendem podporovány",
|
||||
"tags_manage_hint": "Přejmenujte nebo smažte štítky na všech viditelných zařízeních. Odebrání z jednoho zařízení je v Upravit nebo Podrobnostech.",
|
||||
"tags_device_hint": "Odeberte štítek zde, abyste jej odpojili jen od tohoto zařízení. Pro globální změnu použijte Spravovat štítky.",
|
||||
"unassign_tag": "Odebrat z tohoto zařízení",
|
||||
"tag_device_one": "zařízení",
|
||||
"tag_device_many": "zařízení",
|
||||
"tag_group_one": "dynamická skupina",
|
||||
"tag_group_many": "dynamických skupin"
|
||||
},
|
||||
"keys": {
|
||||
"title": "Klíče serveru",
|
||||
@@ -1769,7 +1788,8 @@
|
||||
"sessions_col_duration": "Trvání",
|
||||
"sessions_active": "Aktivní",
|
||||
"sessions_transport_punch": "Přímé / punch",
|
||||
"sessions_transport_relay": "Relay"
|
||||
"sessions_transport_relay": "Relay",
|
||||
"tags_unassign_hint": "Odebrání štítku zde jej odpojí jen od tohoto zařízení. Pro globální změnu použijte Spravovat štítky na stránce Zařízení."
|
||||
},
|
||||
"inventory": {
|
||||
"title": "Inventář",
|
||||
|
||||
+22
-2
@@ -441,7 +441,26 @@
|
||||
"group_commercialization_assign_existing": "Assign existing package",
|
||||
"group_commercialization_create_new": "Create new package and assign",
|
||||
"group_commercialization_saved": "Package assigned to group",
|
||||
"group_one_only": "A device can belong to only one group"
|
||||
"group_one_only": "A device can belong to only one group",
|
||||
"manage_tags": "Administrer tags",
|
||||
"manage_device_tags": "Administrer tags",
|
||||
"rename_tag": "Omdøb tag",
|
||||
"delete_tag": "Slet tag",
|
||||
"rename_tag_prompt": "Nyt tagnavn",
|
||||
"rename_tag_hint": "Omdøber dette tag på alle synlige enheder og opdaterer matchende dynamiske grupper.",
|
||||
"delete_tag_confirm": "Fjern tagget \"{tag}\" fra {count} enhed(er)? Dynamiske grupper der bruger det bliver tomme indtil opdatering.",
|
||||
"tag_renamed": "Tag omdøbt",
|
||||
"tag_deleted": "Tag slettet",
|
||||
"tag_name_required": "Tagnavn er påkrævet",
|
||||
"tag_rename_identical": "Det nye tagnavn skal være anderledes",
|
||||
"tags_backend_unsupported": "Enhedstags understøttes ikke med den aktuelle backend",
|
||||
"tags_manage_hint": "Omdøb eller slet tags på alle synlige enheder. Fjernelse fra én enhed sker under Rediger eller Detaljer.",
|
||||
"tags_device_hint": "Fjern et tag her for kun at fjerne det fra denne enhed. Brug Administrer tags til at omdøbe eller slette overalt.",
|
||||
"unassign_tag": "Fjern fra denne enhed",
|
||||
"tag_device_one": "enhed",
|
||||
"tag_device_many": "enheder",
|
||||
"tag_group_one": "dynamisk gruppe",
|
||||
"tag_group_many": "dynamiske grupper"
|
||||
},
|
||||
"keys": {
|
||||
"title": "Servernøgler",
|
||||
@@ -1775,7 +1794,8 @@
|
||||
"sessions_col_duration": "Varighed",
|
||||
"sessions_active": "Aktiv",
|
||||
"sessions_transport_punch": "Direkte / punch",
|
||||
"sessions_transport_relay": "Relay"
|
||||
"sessions_transport_relay": "Relay",
|
||||
"tags_unassign_hint": "Fjernelse af et tag her fjerner det kun fra denne enhed. Brug Administrer tags på Enheder til at omdøbe eller slette overalt."
|
||||
},
|
||||
"inventory": {
|
||||
"title": "Inventory",
|
||||
|
||||
+22
-2
@@ -435,7 +435,26 @@
|
||||
"group_commercialization_assign_existing": "Assign existing package",
|
||||
"group_commercialization_create_new": "Create new package and assign",
|
||||
"group_commercialization_saved": "Package assigned to group",
|
||||
"group_one_only": "A device can belong to only one group"
|
||||
"group_one_only": "A device can belong to only one group",
|
||||
"manage_tags": "Tags verwalten",
|
||||
"manage_device_tags": "Tags verwalten",
|
||||
"rename_tag": "Tag umbenennen",
|
||||
"delete_tag": "Tag löschen",
|
||||
"rename_tag_prompt": "Neuer Tagname",
|
||||
"rename_tag_hint": "Benennt diesen Tag auf allen sichtbaren Geräten um und aktualisiert passende dynamische Gruppen.",
|
||||
"delete_tag_confirm": "Tag \"{tag}\" von {count} Gerät(en) entfernen? Dynamische Gruppen mit diesem Tag bleiben bis zur Aktualisierung leer.",
|
||||
"tag_renamed": "Tag umbenannt",
|
||||
"tag_deleted": "Tag gelöscht",
|
||||
"tag_name_required": "Tagname ist erforderlich",
|
||||
"tag_rename_identical": "Der neue Tagname muss anders sein",
|
||||
"tags_backend_unsupported": "Geräte-Tags werden mit dem aktuellen Backend nicht unterstützt",
|
||||
"tags_manage_hint": "Tags auf allen sichtbaren Geräten umbenennen oder löschen. Entfernen von einem Gerät erfolgt in Bearbeiten oder Gerätedetails.",
|
||||
"tags_device_hint": "Entfernen Sie einen Tag hier, um ihn nur von diesem Gerät zu lösen. Nutzen Sie Tags verwalten zum globalen Umbenennen oder Löschen.",
|
||||
"unassign_tag": "Von diesem Gerät entfernen",
|
||||
"tag_device_one": "Gerät",
|
||||
"tag_device_many": "Geräte",
|
||||
"tag_group_one": "dynamische Gruppe",
|
||||
"tag_group_many": "dynamische Gruppen"
|
||||
},
|
||||
"keys": {
|
||||
"title": "Dashboard",
|
||||
@@ -1769,7 +1788,8 @@
|
||||
"sessions_col_duration": "Dauer",
|
||||
"sessions_active": "Aktiv",
|
||||
"sessions_transport_punch": "Direkt / Punch",
|
||||
"sessions_transport_relay": "Relay"
|
||||
"sessions_transport_relay": "Relay",
|
||||
"tags_unassign_hint": "Das Entfernen eines Tags hier hebt die Zuweisung nur für dieses Gerät auf. Nutzen Sie Tags verwalten auf der Geräte-Seite zum globalen Umbenennen oder Löschen."
|
||||
},
|
||||
"inventory": {
|
||||
"title": "Dashboard",
|
||||
|
||||
@@ -320,6 +320,25 @@
|
||||
"access_policy": "Access Policy",
|
||||
"filter_by_tags": "Filter by tags",
|
||||
"no_tags": "No tags",
|
||||
"manage_tags": "Manage tags",
|
||||
"manage_device_tags": "Manage tags",
|
||||
"rename_tag": "Rename tag",
|
||||
"delete_tag": "Delete tag",
|
||||
"rename_tag_prompt": "New tag name",
|
||||
"rename_tag_hint": "Renames this tag on all devices you can see and updates matching dynamic groups.",
|
||||
"delete_tag_confirm": "Remove tag \"{tag}\" from {count} device(s)? Dynamic groups that use this tag will become empty until updated.",
|
||||
"tag_renamed": "Tag renamed",
|
||||
"tag_deleted": "Tag deleted",
|
||||
"tag_name_required": "Tag name is required",
|
||||
"tag_rename_identical": "New tag name must be different",
|
||||
"tags_backend_unsupported": "Device tags are not supported with the current backend",
|
||||
"tags_manage_hint": "Rename or delete tags across all devices you can see. Removing a tag from one device is done in Edit or Device details.",
|
||||
"tags_device_hint": "Remove a tag here to unassign it from this device only. Use Manage Tags to rename or delete a tag everywhere.",
|
||||
"unassign_tag": "Remove from this device",
|
||||
"tag_device_one": "device",
|
||||
"tag_device_many": "devices",
|
||||
"tag_group_one": "dynamic group",
|
||||
"tag_group_many": "dynamic groups",
|
||||
"all_groups": "All groups",
|
||||
"create_group": "Create group",
|
||||
"manage_groups": "Manage Groups",
|
||||
@@ -1736,6 +1755,7 @@
|
||||
"tag_placeholder": "Enter a tag name...",
|
||||
"add_tag": "Add",
|
||||
"tag_exists": "This tag already exists",
|
||||
"tags_unassign_hint": "Removing a tag here unassigns it from this device only. Use Manage Tags on the Devices page to rename or delete a tag everywhere.",
|
||||
"notes": "Notes",
|
||||
"notes_placeholder": "Add notes about this device...",
|
||||
"danger_zone": "Danger Zone",
|
||||
|
||||
+22
-2
@@ -435,7 +435,26 @@
|
||||
"group_commercialization_assign_existing": "Assign existing package",
|
||||
"group_commercialization_create_new": "Create new package and assign",
|
||||
"group_commercialization_saved": "Package assigned to group",
|
||||
"group_one_only": "A device can belong to only one group"
|
||||
"group_one_only": "A device can belong to only one group",
|
||||
"manage_tags": "Administrar etiquetas",
|
||||
"manage_device_tags": "Administrar etiquetas",
|
||||
"rename_tag": "Renombrar etiqueta",
|
||||
"delete_tag": "Eliminar etiqueta",
|
||||
"rename_tag_prompt": "Nuevo nombre de etiqueta",
|
||||
"rename_tag_hint": "Renombra esta etiqueta en todos los dispositivos visibles y actualiza los grupos dinámicos coincidentes.",
|
||||
"delete_tag_confirm": "¿Quitar la etiqueta \"{tag}\" de {count} dispositivo(s)? Los grupos dinámicos que la usan quedarán vacíos hasta actualizarlos.",
|
||||
"tag_renamed": "Etiqueta renombrada",
|
||||
"tag_deleted": "Etiqueta eliminada",
|
||||
"tag_name_required": "El nombre de etiqueta es obligatorio",
|
||||
"tag_rename_identical": "El nuevo nombre debe ser distinto",
|
||||
"tags_backend_unsupported": "Las etiquetas de dispositivos no son compatibles con el backend actual",
|
||||
"tags_manage_hint": "Renombre o elimine etiquetas en todos los dispositivos visibles. Quitar una etiqueta de un dispositivo se hace en Editar o Detalles.",
|
||||
"tags_device_hint": "Quite una etiqueta aquí para desasignarla solo de este dispositivo. Use Administrar etiquetas para renombrar o eliminar en todas partes.",
|
||||
"unassign_tag": "Quitar de este dispositivo",
|
||||
"tag_device_one": "dispositivo",
|
||||
"tag_device_many": "dispositivos",
|
||||
"tag_group_one": "grupo dinámico",
|
||||
"tag_group_many": "grupos dinámicos"
|
||||
},
|
||||
"keys": {
|
||||
"title": "Panel",
|
||||
@@ -1769,7 +1788,8 @@
|
||||
"sessions_col_duration": "Duración",
|
||||
"sessions_active": "Activa",
|
||||
"sessions_transport_punch": "Directo / punch",
|
||||
"sessions_transport_relay": "Relay"
|
||||
"sessions_transport_relay": "Relay",
|
||||
"tags_unassign_hint": "Quitar una etiqueta aquí la desasigna solo de este dispositivo. Use Administrar etiquetas en Dispositivos para renombrar o eliminar en todas partes."
|
||||
},
|
||||
"inventory": {
|
||||
"title": "Panel",
|
||||
|
||||
+22
-2
@@ -441,7 +441,26 @@
|
||||
"group_commercialization_assign_existing": "Assign existing package",
|
||||
"group_commercialization_create_new": "Create new package and assign",
|
||||
"group_commercialization_saved": "Package assigned to group",
|
||||
"group_one_only": "A device can belong to only one group"
|
||||
"group_one_only": "A device can belong to only one group",
|
||||
"manage_tags": "Hallitse tageja",
|
||||
"manage_device_tags": "Hallitse tageja",
|
||||
"rename_tag": "Nimeä tagi uudelleen",
|
||||
"delete_tag": "Poista tagi",
|
||||
"rename_tag_prompt": "Uusi tagin nimi",
|
||||
"rename_tag_hint": "Nimeää tämän tagin uudelleen kaikilla näkyvillä laitteilla ja päivittää vastaavat dynaamiset ryhmät.",
|
||||
"delete_tag_confirm": "Poistetaanko tagi \"{tag}\" {count} laitteelta? Dynaamiset ryhmät, jotka käyttävät sitä, ovat tyhjiä päivitykseen asti.",
|
||||
"tag_renamed": "Tagi nimetty uudelleen",
|
||||
"tag_deleted": "Tagi poistettu",
|
||||
"tag_name_required": "Tagin nimi on pakollinen",
|
||||
"tag_rename_identical": "Uuden nimen on oltava eri",
|
||||
"tags_backend_unsupported": "Laitetageja ei tueta nykyisellä backendillä",
|
||||
"tags_manage_hint": "Nimeä uudelleen tai poista tageja kaikilta näkyviltä laitteilta. Poisto yhdeltä laitteelta tehdään Muokkaa- tai Tiedot-näkymässä.",
|
||||
"tags_device_hint": "Poista tagi täällä irrottaaksesi sen vain tältä laitteelta. Käytä Hallitse tageja globaaliin uudelleennimeämiseen tai poistoon.",
|
||||
"unassign_tag": "Poista tältä laitteelta",
|
||||
"tag_device_one": "laite",
|
||||
"tag_device_many": "laitetta",
|
||||
"tag_group_one": "dynaaminen ryhmä",
|
||||
"tag_group_many": "dynaamista ryhmää"
|
||||
},
|
||||
"keys": {
|
||||
"title": "Palvelimen avaimet",
|
||||
@@ -1775,7 +1794,8 @@
|
||||
"sessions_col_duration": "Kesto",
|
||||
"sessions_active": "Aktiivinen",
|
||||
"sessions_transport_punch": "Suora / punch",
|
||||
"sessions_transport_relay": "Relay"
|
||||
"sessions_transport_relay": "Relay",
|
||||
"tags_unassign_hint": "Tagin poisto täällä irrottaa sen vain tältä laitteelta. Käytä Hallitse tageja Laitteet-sivulla globaaliin muutokseen."
|
||||
},
|
||||
"inventory": {
|
||||
"title": "Inventory",
|
||||
|
||||
+22
-2
@@ -439,7 +439,26 @@
|
||||
"group_commercialization_assign_existing": "Assign existing package",
|
||||
"group_commercialization_create_new": "Create new package and assign",
|
||||
"group_commercialization_saved": "Package assigned to group",
|
||||
"group_one_only": "A device can belong to only one group"
|
||||
"group_one_only": "A device can belong to only one group",
|
||||
"manage_tags": "Gérer les balises",
|
||||
"manage_device_tags": "Gérer les balises",
|
||||
"rename_tag": "Renommer la balise",
|
||||
"delete_tag": "Supprimer la balise",
|
||||
"rename_tag_prompt": "Nouveau nom de balise",
|
||||
"rename_tag_hint": "Renomme cette balise sur tous les appareils visibles et met à jour les groupes dynamiques correspondants.",
|
||||
"delete_tag_confirm": "Retirer la balise \"{tag}\" de {count} appareil(s) ? Les groupes dynamiques qui l’utilisent seront vides jusqu’à mise à jour.",
|
||||
"tag_renamed": "Balise renommée",
|
||||
"tag_deleted": "Balise supprimée",
|
||||
"tag_name_required": "Le nom de balise est requis",
|
||||
"tag_rename_identical": "Le nouveau nom doit être différent",
|
||||
"tags_backend_unsupported": "Les balises d’appareils ne sont pas prises en charge avec le backend actuel",
|
||||
"tags_manage_hint": "Renommez ou supprimez des balises sur tous les appareils visibles. Retirer une balise d’un appareil se fait dans Modifier ou Détails.",
|
||||
"tags_device_hint": "Retirez une balise ici pour la désassigner de cet appareil uniquement. Utilisez Gérer les balises pour renommer ou supprimer partout.",
|
||||
"unassign_tag": "Retirer de cet appareil",
|
||||
"tag_device_one": "appareil",
|
||||
"tag_device_many": "appareils",
|
||||
"tag_group_one": "groupe dynamique",
|
||||
"tag_group_many": "groupes dynamiques"
|
||||
},
|
||||
"keys": {
|
||||
"title": "Tableau de bord",
|
||||
@@ -1786,7 +1805,8 @@
|
||||
"sessions_col_duration": "Durée",
|
||||
"sessions_active": "Active",
|
||||
"sessions_transport_punch": "Direct / punch",
|
||||
"sessions_transport_relay": "Relay"
|
||||
"sessions_transport_relay": "Relay",
|
||||
"tags_unassign_hint": "Retirer une balise ici la désassigne uniquement de cet appareil. Utilisez Gérer les balises sur la page Appareils pour renommer ou supprimer partout."
|
||||
},
|
||||
"inventory": {
|
||||
"title": "Tableau de bord",
|
||||
|
||||
+22
-2
@@ -441,7 +441,26 @@
|
||||
"group_commercialization_assign_existing": "Assign existing package",
|
||||
"group_commercialization_create_new": "Create new package and assign",
|
||||
"group_commercialization_saved": "Package assigned to group",
|
||||
"group_one_only": "A device can belong to only one group"
|
||||
"group_one_only": "A device can belong to only one group",
|
||||
"manage_tags": "टैग प्रबंधित करें",
|
||||
"manage_device_tags": "टैग प्रबंधित करें",
|
||||
"rename_tag": "टैग का नाम बदलें",
|
||||
"delete_tag": "टैग हटाएँ",
|
||||
"rename_tag_prompt": "नया टैग नाम",
|
||||
"rename_tag_hint": "इस टैग का नाम सभी दिखाई देने वाले उपकरणों पर बदलता है और मेल खाते गतिशील समूह अपडेट करता है।",
|
||||
"delete_tag_confirm": "टैग \"{tag}\" को {count} उपकरण(ों) से हटाएँ? इसे उपयोग करने वाले गतिशील समूह अपडेट तक खाली रहेंगे।",
|
||||
"tag_renamed": "टैग का नाम बदला गया",
|
||||
"tag_deleted": "टैग हटाया गया",
|
||||
"tag_name_required": "टैग नाम आवश्यक है",
|
||||
"tag_rename_identical": "नया नाम अलग होना चाहिए",
|
||||
"tags_backend_unsupported": "वर्तमान बैकएंड के साथ उपकरण टैग समर्थित नहीं हैं",
|
||||
"tags_manage_hint": "सभी दिखाई देने वाले उपकरणों पर टैग का नाम बदलें या हटाएँ। एक उपकरण से हटाना संपादन या विवरण में होता है।",
|
||||
"tags_device_hint": "यहाँ टैग हटाकर केवल इस उपकरण से अनअसाइन करें। सर्वत्र बदलने के लिए टैग प्रबंधित करें उपयोग करें।",
|
||||
"unassign_tag": "इस उपकरण से हटाएँ",
|
||||
"tag_device_one": "उपकरण",
|
||||
"tag_device_many": "उपकरण",
|
||||
"tag_group_one": "गतिशील समूह",
|
||||
"tag_group_many": "गतिशील समूह"
|
||||
},
|
||||
"keys": {
|
||||
"title": "सर्वर कुंजी",
|
||||
@@ -1775,7 +1794,8 @@
|
||||
"sessions_col_duration": "अवधि",
|
||||
"sessions_active": "सक्रिय",
|
||||
"sessions_transport_punch": "प्रत्यक्ष / punch",
|
||||
"sessions_transport_relay": "Relay"
|
||||
"sessions_transport_relay": "Relay",
|
||||
"tags_unassign_hint": "यहाँ टैग हटाने से केवल इस उपकरण से अनअसाइन होता है। सर्वत्र बदलने के लिए उपकरण पृष्ठ पर टैग प्रबंधित करें उपयोग करें।"
|
||||
},
|
||||
"inventory": {
|
||||
"title": "Inventory",
|
||||
|
||||
+22
-2
@@ -441,7 +441,26 @@
|
||||
"group_commercialization_assign_existing": "Assign existing package",
|
||||
"group_commercialization_create_new": "Create new package and assign",
|
||||
"group_commercialization_saved": "Package assigned to group",
|
||||
"group_one_only": "A device can belong to only one group"
|
||||
"group_one_only": "A device can belong to only one group",
|
||||
"manage_tags": "Címkék kezelése",
|
||||
"manage_device_tags": "Címkék kezelése",
|
||||
"rename_tag": "Címke átnevezése",
|
||||
"delete_tag": "Címke törlése",
|
||||
"rename_tag_prompt": "Új címkenév",
|
||||
"rename_tag_hint": "Átnevezi ezt a címkét minden látható eszközön, és frissíti a megfelelő dinamikus csoportokat.",
|
||||
"delete_tag_confirm": "Eltávolítja a(z) \"{tag}\" címkét {count} eszközről? A címkét használó dinamikus csoportok üresek maradnak a frissítésig.",
|
||||
"tag_renamed": "Címke átnevezve",
|
||||
"tag_deleted": "Címke törölve",
|
||||
"tag_name_required": "A címkenév kötelező",
|
||||
"tag_rename_identical": "Az új névnek különböznie kell",
|
||||
"tags_backend_unsupported": "Az eszközcímkék nem támogatottak a jelenlegi backenddel",
|
||||
"tags_manage_hint": "Nevezzen át vagy töröljön címkéket minden látható eszközön. Egy eszközről az Edit / Részletek alatt vehető le.",
|
||||
"tags_device_hint": "Itt távolítsa el a címkét, hogy csak erről az eszközről legyen leválasztva. Globális műveletekhez használja a Címkék kezelése gombot.",
|
||||
"unassign_tag": "Eltávolítás erről az eszközről",
|
||||
"tag_device_one": "eszköz",
|
||||
"tag_device_many": "eszköz",
|
||||
"tag_group_one": "dinamikus csoport",
|
||||
"tag_group_many": "dinamikus csoport"
|
||||
},
|
||||
"keys": {
|
||||
"title": "Szerverkulcsok",
|
||||
@@ -1775,7 +1794,8 @@
|
||||
"sessions_col_duration": "Időtartam",
|
||||
"sessions_active": "Aktív",
|
||||
"sessions_transport_punch": "Közvetlen / punch",
|
||||
"sessions_transport_relay": "Relay"
|
||||
"sessions_transport_relay": "Relay",
|
||||
"tags_unassign_hint": "A címke eltávolítása itt csak erről az eszközről veszi le. Globális műveletekhez használja a Címkék kezelése gombot az Eszközök oldalon."
|
||||
},
|
||||
"inventory": {
|
||||
"title": "Inventory",
|
||||
|
||||
+22
-2
@@ -441,7 +441,26 @@
|
||||
"group_commercialization_assign_existing": "Assign existing package",
|
||||
"group_commercialization_create_new": "Create new package and assign",
|
||||
"group_commercialization_saved": "Package assigned to group",
|
||||
"group_one_only": "A device can belong to only one group"
|
||||
"group_one_only": "A device can belong to only one group",
|
||||
"manage_tags": "Kelola tag",
|
||||
"manage_device_tags": "Kelola tag",
|
||||
"rename_tag": "Ganti nama tag",
|
||||
"delete_tag": "Hapus tag",
|
||||
"rename_tag_prompt": "Nama tag baru",
|
||||
"rename_tag_hint": "Mengganti nama tag ini pada semua perangkat yang terlihat dan memperbarui grup dinamis yang cocok.",
|
||||
"delete_tag_confirm": "Hapus tag \"{tag}\" dari {count} perangkat? Grup dinamis yang memakai tag ini akan kosong hingga diperbarui.",
|
||||
"tag_renamed": "Tag diganti nama",
|
||||
"tag_deleted": "Tag dihapus",
|
||||
"tag_name_required": "Nama tag wajib diisi",
|
||||
"tag_rename_identical": "Nama baru harus berbeda",
|
||||
"tags_backend_unsupported": "Tag perangkat tidak didukung dengan backend saat ini",
|
||||
"tags_manage_hint": "Ganti nama atau hapus tag di semua perangkat yang terlihat. Lepas dari satu perangkat lewat Edit atau Detail.",
|
||||
"tags_device_hint": "Hapus tag di sini untuk melepasnya hanya dari perangkat ini. Gunakan Kelola tag untuk perubahan global.",
|
||||
"unassign_tag": "Hapus dari perangkat ini",
|
||||
"tag_device_one": "perangkat",
|
||||
"tag_device_many": "perangkat",
|
||||
"tag_group_one": "grup dinamis",
|
||||
"tag_group_many": "grup dinamis"
|
||||
},
|
||||
"keys": {
|
||||
"title": "Kunci Server",
|
||||
@@ -1775,7 +1794,8 @@
|
||||
"sessions_col_duration": "Durasi",
|
||||
"sessions_active": "Aktif",
|
||||
"sessions_transport_punch": "Langsung / punch",
|
||||
"sessions_transport_relay": "Relay"
|
||||
"sessions_transport_relay": "Relay",
|
||||
"tags_unassign_hint": "Menghapus tag di sini hanya melepasnya dari perangkat ini. Gunakan Kelola tag di halaman Perangkat untuk perubahan global."
|
||||
},
|
||||
"inventory": {
|
||||
"title": "Inventory",
|
||||
|
||||
+22
-2
@@ -435,7 +435,26 @@
|
||||
"group_commercialization_assign_existing": "Assign existing package",
|
||||
"group_commercialization_create_new": "Create new package and assign",
|
||||
"group_commercialization_saved": "Package assigned to group",
|
||||
"group_one_only": "A device can belong to only one group"
|
||||
"group_one_only": "A device can belong to only one group",
|
||||
"manage_tags": "Gestisci tag",
|
||||
"manage_device_tags": "Gestisci tag",
|
||||
"rename_tag": "Rinomina tag",
|
||||
"delete_tag": "Elimina tag",
|
||||
"rename_tag_prompt": "Nuovo nome tag",
|
||||
"rename_tag_hint": "Rinomina questo tag su tutti i dispositivi visibili e aggiorna i gruppi dinamici corrispondenti.",
|
||||
"delete_tag_confirm": "Rimuovere il tag \"{tag}\" da {count} dispositivo/i? I gruppi dinamici che lo usano resteranno vuoti fino all’aggiornamento.",
|
||||
"tag_renamed": "Tag rinominato",
|
||||
"tag_deleted": "Tag eliminato",
|
||||
"tag_name_required": "Il nome del tag è obbligatorio",
|
||||
"tag_rename_identical": "Il nuovo nome deve essere diverso",
|
||||
"tags_backend_unsupported": "I tag dei dispositivi non sono supportati con il backend attuale",
|
||||
"tags_manage_hint": "Rinomina o elimina i tag su tutti i dispositivi visibili. Rimuovere un tag da un dispositivo si fa in Modifica o Dettagli.",
|
||||
"tags_device_hint": "Rimuovi un tag qui per disassegnarlo solo da questo dispositivo. Usa Gestisci tag per rinominare o eliminare ovunque.",
|
||||
"unassign_tag": "Rimuovi da questo dispositivo",
|
||||
"tag_device_one": "dispositivo",
|
||||
"tag_device_many": "dispositivi",
|
||||
"tag_group_one": "gruppo dinamico",
|
||||
"tag_group_many": "gruppi dinamici"
|
||||
},
|
||||
"keys": {
|
||||
"title": "Chiavi del server",
|
||||
@@ -1769,7 +1788,8 @@
|
||||
"sessions_col_duration": "Durata",
|
||||
"sessions_active": "Attiva",
|
||||
"sessions_transport_punch": "Diretto / punch",
|
||||
"sessions_transport_relay": "Relay"
|
||||
"sessions_transport_relay": "Relay",
|
||||
"tags_unassign_hint": "Rimuovere un tag qui lo disassegna solo da questo dispositivo. Usa Gestisci tag nella pagina Dispositivi per rinominare o eliminare ovunque."
|
||||
},
|
||||
"inventory": {
|
||||
"title": "Inventario",
|
||||
|
||||
+22
-2
@@ -435,7 +435,26 @@
|
||||
"group_commercialization_assign_existing": "Assign existing package",
|
||||
"group_commercialization_create_new": "Create new package and assign",
|
||||
"group_commercialization_saved": "Package assigned to group",
|
||||
"group_one_only": "A device can belong to only one group"
|
||||
"group_one_only": "A device can belong to only one group",
|
||||
"manage_tags": "タグを管理",
|
||||
"manage_device_tags": "タグを管理",
|
||||
"rename_tag": "タグ名を変更",
|
||||
"delete_tag": "タグを削除",
|
||||
"rename_tag_prompt": "新しいタグ名",
|
||||
"rename_tag_hint": "表示中のすべてのデバイスでこのタグ名を変更し、一致する動的グループを更新します。",
|
||||
"delete_tag_confirm": "タグ \"{tag}\" を {count} 台のデバイスから削除しますか?このタグを使う動的グループは更新まで空になります。",
|
||||
"tag_renamed": "タグ名を変更しました",
|
||||
"tag_deleted": "タグを削除しました",
|
||||
"tag_name_required": "タグ名は必須です",
|
||||
"tag_rename_identical": "新しい名前は別である必要があります",
|
||||
"tags_backend_unsupported": "現在のバックエンドではデバイスタグはサポートされていません",
|
||||
"tags_manage_hint": "表示中のすべてのデバイスでタグを変更または削除します。1台からの解除は編集または詳細で行います。",
|
||||
"tags_device_hint": "ここでタグを削除すると、このデバイスからのみ割り当てが解除されます。全体の変更にはタグを管理を使います。",
|
||||
"unassign_tag": "このデバイスから削除",
|
||||
"tag_device_one": "デバイス",
|
||||
"tag_device_many": "デバイス",
|
||||
"tag_group_one": "動的グループ",
|
||||
"tag_group_many": "動的グループ"
|
||||
},
|
||||
"keys": {
|
||||
"title": "サーバーキー",
|
||||
@@ -1769,7 +1788,8 @@
|
||||
"sessions_col_duration": "所要時間",
|
||||
"sessions_active": "進行中",
|
||||
"sessions_transport_punch": "直接 / パンチ",
|
||||
"sessions_transport_relay": "リレー"
|
||||
"sessions_transport_relay": "リレー",
|
||||
"tags_unassign_hint": "ここでタグを削除すると、このデバイスからのみ割り当てが解除されます。全体の変更にはデバイスページのタグを管理を使います。"
|
||||
},
|
||||
"inventory": {
|
||||
"title": "Inventory",
|
||||
|
||||
+22
-2
@@ -435,7 +435,26 @@
|
||||
"group_commercialization_assign_existing": "Assign existing package",
|
||||
"group_commercialization_create_new": "Create new package and assign",
|
||||
"group_commercialization_saved": "Package assigned to group",
|
||||
"group_one_only": "A device can belong to only one group"
|
||||
"group_one_only": "A device can belong to only one group",
|
||||
"manage_tags": "태그 관리",
|
||||
"manage_device_tags": "태그 관리",
|
||||
"rename_tag": "태그 이름 바꾸기",
|
||||
"delete_tag": "태그 삭제",
|
||||
"rename_tag_prompt": "새 태그 이름",
|
||||
"rename_tag_hint": "볼 수 있는 모든 장치에서 이 태그 이름을 바꾸고 일치하는 동적 그룹을 업데이트합니다.",
|
||||
"delete_tag_confirm": "태그 \"{tag}\"을(를) {count}개 장치에서 제거할까요? 이 태그를 쓰는 동적 그룹은 업데이트될 때까지 비어 있습니다.",
|
||||
"tag_renamed": "태그 이름이 바뀌었습니다",
|
||||
"tag_deleted": "태그가 삭제되었습니다",
|
||||
"tag_name_required": "태그 이름이 필요합니다",
|
||||
"tag_rename_identical": "새 이름은 달라야 합니다",
|
||||
"tags_backend_unsupported": "현재 백엔드에서는 장치 태그가 지원되지 않습니다",
|
||||
"tags_manage_hint": "볼 수 있는 모든 장치에서 태그를 바꾸거나 삭제합니다. 한 장치에서 제거는 편집 또는 세부정보에서 합니다.",
|
||||
"tags_device_hint": "여기서 태그를 제거하면 이 장치에서만 할당이 해제됩니다. 전역 변경은 태그 관리를 사용하세요.",
|
||||
"unassign_tag": "이 장치에서 제거",
|
||||
"tag_device_one": "장치",
|
||||
"tag_device_many": "장치",
|
||||
"tag_group_one": "동적 그룹",
|
||||
"tag_group_many": "동적 그룹"
|
||||
},
|
||||
"keys": {
|
||||
"title": "서버 키",
|
||||
@@ -1769,7 +1788,8 @@
|
||||
"sessions_col_duration": "지속 시간",
|
||||
"sessions_active": "진행 중",
|
||||
"sessions_transport_punch": "직접 / 펀치",
|
||||
"sessions_transport_relay": "릴레이"
|
||||
"sessions_transport_relay": "릴레이",
|
||||
"tags_unassign_hint": "여기서 태그를 제거하면 이 장치에서만 할당이 해제됩니다. 전역 변경은 장치 페이지의 태그 관리를 사용하세요."
|
||||
},
|
||||
"inventory": {
|
||||
"title": "Inventory",
|
||||
|
||||
+22
-2
@@ -441,7 +441,26 @@
|
||||
"group_commercialization_assign_existing": "Assign existing package",
|
||||
"group_commercialization_create_new": "Create new package and assign",
|
||||
"group_commercialization_saved": "Package assigned to group",
|
||||
"group_one_only": "A device can belong to only one group"
|
||||
"group_one_only": "A device can belong to only one group",
|
||||
"manage_tags": "Administrer tagger",
|
||||
"manage_device_tags": "Administrer tagger",
|
||||
"rename_tag": "Gi tag nytt navn",
|
||||
"delete_tag": "Slett tag",
|
||||
"rename_tag_prompt": "Nytt tagnavn",
|
||||
"rename_tag_hint": "Gir denne tagen nytt navn på alle synlige enheter og oppdaterer matchende dynamiske grupper.",
|
||||
"delete_tag_confirm": "Fjern tagen \"{tag}\" fra {count} enhet(er)? Dynamiske grupper som bruker den blir tomme til de oppdateres.",
|
||||
"tag_renamed": "Tag omdøpt",
|
||||
"tag_deleted": "Tag slettet",
|
||||
"tag_name_required": "Tagnavn er påkrevd",
|
||||
"tag_rename_identical": "Det nye tagnavnet må være annerledes",
|
||||
"tags_backend_unsupported": "Enhetstagger støttes ikke med gjeldende backend",
|
||||
"tags_manage_hint": "Gi nytt navn eller slett tagger på alle synlige enheter. Fjerning fra én enhet gjøres i Rediger eller Detaljer.",
|
||||
"tags_device_hint": "Fjern en tag her for å frakoble den kun fra denne enheten. Bruk Administrer tagger for global endring.",
|
||||
"unassign_tag": "Fjern fra denne enheten",
|
||||
"tag_device_one": "enhet",
|
||||
"tag_device_many": "enheter",
|
||||
"tag_group_one": "dynamisk gruppe",
|
||||
"tag_group_many": "dynamiske grupper"
|
||||
},
|
||||
"keys": {
|
||||
"title": "Servernøkler",
|
||||
@@ -1775,7 +1794,8 @@
|
||||
"sessions_col_duration": "Varighet",
|
||||
"sessions_active": "Aktiv",
|
||||
"sessions_transport_punch": "Direkte / punch",
|
||||
"sessions_transport_relay": "Relay"
|
||||
"sessions_transport_relay": "Relay",
|
||||
"tags_unassign_hint": "Fjerning av en tag her frakobler den kun fra denne enheten. Bruk Administrer tagger på Enheter for global endring."
|
||||
},
|
||||
"inventory": {
|
||||
"title": "Inventory",
|
||||
|
||||
+22
-2
@@ -435,7 +435,26 @@
|
||||
"group_commercialization_assign_existing": "Assign existing package",
|
||||
"group_commercialization_create_new": "Create new package and assign",
|
||||
"group_commercialization_saved": "Package assigned to group",
|
||||
"group_one_only": "A device can belong to only one group"
|
||||
"group_one_only": "A device can belong to only one group",
|
||||
"manage_tags": "Tags beheren",
|
||||
"manage_device_tags": "Tags beheren",
|
||||
"rename_tag": "Tag hernoemen",
|
||||
"delete_tag": "Tag verwijderen",
|
||||
"rename_tag_prompt": "Nieuwe tagnaam",
|
||||
"rename_tag_hint": "Hernoemt deze tag op alle zichtbare apparaten en werkt bijpassende dynamische groepen bij.",
|
||||
"delete_tag_confirm": "Tag \"{tag}\" van {count} apparaat/apparaten verwijderen? Dynamische groepen die deze tag gebruiken blijven leeg tot bijgewerkt.",
|
||||
"tag_renamed": "Tag hernoemd",
|
||||
"tag_deleted": "Tag verwijderd",
|
||||
"tag_name_required": "Tagnaam is verplicht",
|
||||
"tag_rename_identical": "Nieuwe tagnaam moet anders zijn",
|
||||
"tags_backend_unsupported": "Apparaattags worden niet ondersteund met de huidige backend",
|
||||
"tags_manage_hint": "Hernoem of verwijder tags op alle zichtbare apparaten. Verwijderen van één apparaat gebeurt in Bewerken of Details.",
|
||||
"tags_device_hint": "Verwijder hier een tag om deze alleen van dit apparaat te ontkoppelen. Gebruik Tags beheren om overal te hernoemen of te verwijderen.",
|
||||
"unassign_tag": "Van dit apparaat verwijderen",
|
||||
"tag_device_one": "apparaat",
|
||||
"tag_device_many": "apparaten",
|
||||
"tag_group_one": "dynamische groep",
|
||||
"tag_group_many": "dynamische groepen"
|
||||
},
|
||||
"keys": {
|
||||
"title": "Serversleutels",
|
||||
@@ -1769,7 +1788,8 @@
|
||||
"sessions_col_duration": "Duur",
|
||||
"sessions_active": "Actief",
|
||||
"sessions_transport_punch": "Direct / punch",
|
||||
"sessions_transport_relay": "Relay"
|
||||
"sessions_transport_relay": "Relay",
|
||||
"tags_unassign_hint": "Een tag hier verwijderen ontkoppelt deze alleen van dit apparaat. Gebruik Tags beheren op Apparaten om overal te hernoemen of te verwijderen."
|
||||
},
|
||||
"inventory": {
|
||||
"title": "Inventaris",
|
||||
|
||||
+22
-2
@@ -435,7 +435,26 @@
|
||||
"group_commercialization_assign_existing": "Przypisz istniejący pakiet",
|
||||
"group_commercialization_create_new": "Utwórz nowy pakiet i przypisz",
|
||||
"group_commercialization_saved": "Pakiet przypisany do grupy",
|
||||
"group_one_only": "Urządzenie może należeć tylko do jednej grupy"
|
||||
"group_one_only": "Urządzenie może należeć tylko do jednej grupy",
|
||||
"manage_tags": "Zarządzaj tagami",
|
||||
"manage_device_tags": "Zarządzaj tagami",
|
||||
"rename_tag": "Zmień nazwę tagu",
|
||||
"delete_tag": "Usuń tag",
|
||||
"rename_tag_prompt": "Nowa nazwa tagu",
|
||||
"rename_tag_hint": "Zmienia nazwę tego tagu na wszystkich widocznych urządzeniach i aktualizuje pasujące grupy dynamiczne.",
|
||||
"delete_tag_confirm": "Usunąć tag \"{tag}\" z {count} urządzeń? Grupy dynamiczne używające tego tagu będą puste do czasu aktualizacji.",
|
||||
"tag_renamed": "Zmieniono nazwę tagu",
|
||||
"tag_deleted": "Usunięto tag",
|
||||
"tag_name_required": "Nazwa tagu jest wymagana",
|
||||
"tag_rename_identical": "Nowa nazwa tagu musi być inna",
|
||||
"tags_backend_unsupported": "Tagi urządzeń nie są obsługiwane w bieżącym backendzie",
|
||||
"tags_manage_hint": "Zmieniaj nazwy lub usuwaj tagi na wszystkich widocznych urządzeniach. Usunięcie tagu z jednego urządzenia odbywa się w Edycji lub szczegółach urządzenia.",
|
||||
"tags_device_hint": "Usuń tag tutaj, aby odpiąć go tylko od tego urządzenia. Użyj Zarządzaj tagami, aby zmienić nazwę lub usunąć tag wszędzie.",
|
||||
"unassign_tag": "Usuń z tego urządzenia",
|
||||
"tag_device_one": "urządzenie",
|
||||
"tag_device_many": "urządzeń",
|
||||
"tag_group_one": "grupa dynamiczna",
|
||||
"tag_group_many": "grup dynamicznych"
|
||||
},
|
||||
"keys": {
|
||||
"title": "Klucze serwera",
|
||||
@@ -1782,7 +1801,8 @@
|
||||
"sessions_col_duration": "Czas",
|
||||
"sessions_active": "Aktywna",
|
||||
"sessions_transport_punch": "Bezpośrednie / punch",
|
||||
"sessions_transport_relay": "Relay"
|
||||
"sessions_transport_relay": "Relay",
|
||||
"tags_unassign_hint": "Usunięcie tagu tutaj odłącza go tylko od tego urządzenia. Użyj Zarządzaj tagami na stronie Urządzenia, aby zmienić nazwę lub usunąć tag wszędzie."
|
||||
},
|
||||
"inventory": {
|
||||
"title": "Inwentaryzacja",
|
||||
|
||||
+22
-2
@@ -435,7 +435,26 @@
|
||||
"group_commercialization_assign_existing": "Assign existing package",
|
||||
"group_commercialization_create_new": "Create new package and assign",
|
||||
"group_commercialization_saved": "Package assigned to group",
|
||||
"group_one_only": "A device can belong to only one group"
|
||||
"group_one_only": "A device can belong to only one group",
|
||||
"manage_tags": "Gerir etiquetas",
|
||||
"manage_device_tags": "Gerir etiquetas",
|
||||
"rename_tag": "Mudar nome da etiqueta",
|
||||
"delete_tag": "Eliminar etiqueta",
|
||||
"rename_tag_prompt": "Novo nome da etiqueta",
|
||||
"rename_tag_hint": "Altera o nome desta etiqueta em todos os dispositivos visíveis e atualiza grupos dinâmicos correspondentes.",
|
||||
"delete_tag_confirm": "Remover a etiqueta \"{tag}\" de {count} dispositivo(s)? Grupos dinâmicos que a usam ficarão vazios até serem atualizados.",
|
||||
"tag_renamed": "Etiqueta renomeada",
|
||||
"tag_deleted": "Etiqueta eliminada",
|
||||
"tag_name_required": "O nome da etiqueta é obrigatório",
|
||||
"tag_rename_identical": "O novo nome deve ser diferente",
|
||||
"tags_backend_unsupported": "Etiquetas de dispositivos não são suportadas com o backend atual",
|
||||
"tags_manage_hint": "Renomeie ou elimine etiquetas em todos os dispositivos visíveis. Remover de um dispositivo faz-se em Editar ou Detalhes.",
|
||||
"tags_device_hint": "Remova uma etiqueta aqui para a desassociar apenas deste dispositivo. Use Gerir etiquetas para renomear ou eliminar em todo o lado.",
|
||||
"unassign_tag": "Remover deste dispositivo",
|
||||
"tag_device_one": "dispositivo",
|
||||
"tag_device_many": "dispositivos",
|
||||
"tag_group_one": "grupo dinâmico",
|
||||
"tag_group_many": "grupos dinâmicos"
|
||||
},
|
||||
"keys": {
|
||||
"title": "Chaves do servidor",
|
||||
@@ -1769,7 +1788,8 @@
|
||||
"sessions_col_duration": "Duração",
|
||||
"sessions_active": "Ativa",
|
||||
"sessions_transport_punch": "Direto / punch",
|
||||
"sessions_transport_relay": "Relay"
|
||||
"sessions_transport_relay": "Relay",
|
||||
"tags_unassign_hint": "Remover uma etiqueta aqui desassocia-a apenas deste dispositivo. Use Gerir etiquetas na página Dispositivos para renomear ou eliminar em todo o lado."
|
||||
},
|
||||
"inventory": {
|
||||
"title": "Inventário",
|
||||
|
||||
+22
-2
@@ -441,7 +441,26 @@
|
||||
"group_commercialization_assign_existing": "Assign existing package",
|
||||
"group_commercialization_create_new": "Create new package and assign",
|
||||
"group_commercialization_saved": "Package assigned to group",
|
||||
"group_one_only": "A device can belong to only one group"
|
||||
"group_one_only": "A device can belong to only one group",
|
||||
"manage_tags": "Gestionează etichetele",
|
||||
"manage_device_tags": "Gestionează etichetele",
|
||||
"rename_tag": "Redenumește eticheta",
|
||||
"delete_tag": "Șterge eticheta",
|
||||
"rename_tag_prompt": "Nume nou pentru etichetă",
|
||||
"rename_tag_hint": "Redenumește această etichetă pe toate dispozitivele vizibile și actualizează grupurile dinamice potrivite.",
|
||||
"delete_tag_confirm": "Eliminați eticheta \"{tag}\" de pe {count} dispozitiv(e)? Grupurile dinamice care o folosesc vor fi goale până la actualizare.",
|
||||
"tag_renamed": "Etichetă redenumită",
|
||||
"tag_deleted": "Etichetă ștearsă",
|
||||
"tag_name_required": "Numele etichetei este obligatoriu",
|
||||
"tag_rename_identical": "Noul nume trebuie să fie diferit",
|
||||
"tags_backend_unsupported": "Etichetele dispozitivelor nu sunt acceptate cu backend-ul curent",
|
||||
"tags_manage_hint": "Redenumiți sau ștergeți etichete pe toate dispozitivele vizibile. Eliminarea de pe un dispozitiv se face în Editare sau Detalii.",
|
||||
"tags_device_hint": "Eliminați o etichetă aici pentru a o dezasocia doar de acest dispozitiv. Folosiți Gestionează etichetele pentru redenumire/ștergere globală.",
|
||||
"unassign_tag": "Elimină de pe acest dispozitiv",
|
||||
"tag_device_one": "dispozitiv",
|
||||
"tag_device_many": "dispozitive",
|
||||
"tag_group_one": "grup dinamic",
|
||||
"tag_group_many": "grupuri dinamice"
|
||||
},
|
||||
"keys": {
|
||||
"title": "Cheile serverului",
|
||||
@@ -1775,7 +1794,8 @@
|
||||
"sessions_col_duration": "Durată",
|
||||
"sessions_active": "Activă",
|
||||
"sessions_transport_punch": "Direct / punch",
|
||||
"sessions_transport_relay": "Relay"
|
||||
"sessions_transport_relay": "Relay",
|
||||
"tags_unassign_hint": "Eliminarea unei etichete aici o dezasociază doar de acest dispozitiv. Folosiți Gestionează etichetele pe pagina Dispozitive pentru redenumire/ștergere globală."
|
||||
},
|
||||
"inventory": {
|
||||
"title": "Inventory",
|
||||
|
||||
+22
-2
@@ -441,7 +441,26 @@
|
||||
"group_commercialization_assign_existing": "Assign existing package",
|
||||
"group_commercialization_create_new": "Create new package and assign",
|
||||
"group_commercialization_saved": "Package assigned to group",
|
||||
"group_one_only": "A device can belong to only one group"
|
||||
"group_one_only": "A device can belong to only one group",
|
||||
"manage_tags": "Hantera taggar",
|
||||
"manage_device_tags": "Hantera taggar",
|
||||
"rename_tag": "Byt namn på tagg",
|
||||
"delete_tag": "Ta bort tagg",
|
||||
"rename_tag_prompt": "Nytt taggnamn",
|
||||
"rename_tag_hint": "Byter namn på denna tagg på alla synliga enheter och uppdaterar matchande dynamiska grupper.",
|
||||
"delete_tag_confirm": "Ta bort taggen \"{tag}\" från {count} enhet(er)? Dynamiska grupper som använder den blir tomma tills de uppdateras.",
|
||||
"tag_renamed": "Tagg omdöpt",
|
||||
"tag_deleted": "Tagg borttagen",
|
||||
"tag_name_required": "Taggnamn krävs",
|
||||
"tag_rename_identical": "Det nya namnet måste vara annorlunda",
|
||||
"tags_backend_unsupported": "Enhetstaggar stöds inte med aktuell backend",
|
||||
"tags_manage_hint": "Byt namn på eller ta bort taggar på alla synliga enheter. Borttagning från en enhet görs i Redigera eller Detaljer.",
|
||||
"tags_device_hint": "Ta bort en tagg här för att koppla bort den endast från denna enhet. Använd Hantera taggar för global ändring.",
|
||||
"unassign_tag": "Ta bort från denna enhet",
|
||||
"tag_device_one": "enhet",
|
||||
"tag_device_many": "enheter",
|
||||
"tag_group_one": "dynamisk grupp",
|
||||
"tag_group_many": "dynamiska grupper"
|
||||
},
|
||||
"keys": {
|
||||
"title": "Servernycklar",
|
||||
@@ -1775,7 +1794,8 @@
|
||||
"sessions_col_duration": "Varaktighet",
|
||||
"sessions_active": "Aktiv",
|
||||
"sessions_transport_punch": "Direkt / punch",
|
||||
"sessions_transport_relay": "Relay"
|
||||
"sessions_transport_relay": "Relay",
|
||||
"tags_unassign_hint": "Att ta bort en tagg här kopplar bort den endast från denna enhet. Använd Hantera taggar på Enheter för global ändring."
|
||||
},
|
||||
"inventory": {
|
||||
"title": "Inventory",
|
||||
|
||||
+22
-2
@@ -441,7 +441,26 @@
|
||||
"group_commercialization_assign_existing": "Assign existing package",
|
||||
"group_commercialization_create_new": "Create new package and assign",
|
||||
"group_commercialization_saved": "Package assigned to group",
|
||||
"group_one_only": "A device can belong to only one group"
|
||||
"group_one_only": "A device can belong to only one group",
|
||||
"manage_tags": "จัดการแท็ก",
|
||||
"manage_device_tags": "จัดการแท็ก",
|
||||
"rename_tag": "เปลี่ยนชื่อแท็ก",
|
||||
"delete_tag": "ลบแท็ก",
|
||||
"rename_tag_prompt": "ชื่อแท็กใหม่",
|
||||
"rename_tag_hint": "เปลี่ยนชื่อแท็กนี้บนอุปกรณ์ที่มองเห็นทั้งหมด และอัปเดตกลุ่มไดนามิกที่ตรงกัน",
|
||||
"delete_tag_confirm": "ลบแท็ก \"{tag}\" ออกจาก {count} อุปกรณ์หรือไม่ กลุ่มไดนามิกที่ใช้แท็กนี้จะว่างจนกว่าจะอัปเดต",
|
||||
"tag_renamed": "เปลี่ยนชื่อแท็กแล้ว",
|
||||
"tag_deleted": "ลบแท็กแล้ว",
|
||||
"tag_name_required": "ต้องระบุชื่อแท็ก",
|
||||
"tag_rename_identical": "ชื่อใหม่ต้องต่างจากเดิม",
|
||||
"tags_backend_unsupported": "แท็กอุปกรณ์ไม่รองรับกับแบ็กเอนด์ปัจจุบัน",
|
||||
"tags_manage_hint": "เปลี่ยนชื่อหรือลบแท็กบนอุปกรณ์ที่มองเห็นทั้งหมด การลบจากอุปกรณ์เดียวทำในแก้ไขหรือรายละเอียด",
|
||||
"tags_device_hint": "ลบแท็กที่นี่เพื่อยกเลิกการกำหนดเฉพาะอุปกรณ์นี้ ใช้จัดการแท็กเพื่อเปลี่ยนชื่อหรือลบทั้งหมด",
|
||||
"unassign_tag": "ลบออกจากอุปกรณ์นี้",
|
||||
"tag_device_one": "อุปกรณ์",
|
||||
"tag_device_many": "อุปกรณ์",
|
||||
"tag_group_one": "กลุ่มไดนามิก",
|
||||
"tag_group_many": "กลุ่มไดนามิก"
|
||||
},
|
||||
"keys": {
|
||||
"title": "คีย์เซิร์ฟเวอร์",
|
||||
@@ -1775,7 +1794,8 @@
|
||||
"sessions_col_duration": "ระยะเวลา",
|
||||
"sessions_active": "กำลังทำงาน",
|
||||
"sessions_transport_punch": "ตรง / punch",
|
||||
"sessions_transport_relay": "Relay"
|
||||
"sessions_transport_relay": "Relay",
|
||||
"tags_unassign_hint": "ลบแท็กที่นี่จะยกเลิกการกำหนดเฉพาะอุปกรณ์นี้ ใช้จัดการแท็กในหน้าอุปกรณ์เพื่อเปลี่ยนชื่อหรือลบทั้งหมด"
|
||||
},
|
||||
"inventory": {
|
||||
"title": "Inventory",
|
||||
|
||||
+22
-2
@@ -441,7 +441,26 @@
|
||||
"group_commercialization_assign_existing": "Assign existing package",
|
||||
"group_commercialization_create_new": "Create new package and assign",
|
||||
"group_commercialization_saved": "Package assigned to group",
|
||||
"group_one_only": "A device can belong to only one group"
|
||||
"group_one_only": "A device can belong to only one group",
|
||||
"manage_tags": "Etiketleri yönet",
|
||||
"manage_device_tags": "Etiketleri yönet",
|
||||
"rename_tag": "Etiketi yeniden adlandır",
|
||||
"delete_tag": "Etiketi sil",
|
||||
"rename_tag_prompt": "Yeni etiket adı",
|
||||
"rename_tag_hint": "Bu etiketi görebildiğiniz tüm cihazlarda yeniden adlandırır ve eşleşen dinamik grupları günceller.",
|
||||
"delete_tag_confirm": "\"{tag}\" etiketi {count} cihazdan kaldırılsın mı? Bu etiketi kullanan dinamik gruplar güncellenene kadar boş kalır.",
|
||||
"tag_renamed": "Etiket yeniden adlandırıldı",
|
||||
"tag_deleted": "Etiket silindi",
|
||||
"tag_name_required": "Etiket adı gerekli",
|
||||
"tag_rename_identical": "Yeni ad farklı olmalı",
|
||||
"tags_backend_unsupported": "Cihaz etiketleri mevcut backend ile desteklenmiyor",
|
||||
"tags_manage_hint": "Görünür tüm cihazlarda etiketleri yeniden adlandırın veya silin. Bir cihazdan kaldırma Düzenle veya Ayrıntılar’da yapılır.",
|
||||
"tags_device_hint": "Burada bir etiketi kaldırarak yalnızca bu cihazdan ayırırsınız. Her yerde yeniden adlandırmak/silmek için Etiketleri yönet’i kullanın.",
|
||||
"unassign_tag": "Bu cihazdan kaldır",
|
||||
"tag_device_one": "cihaz",
|
||||
"tag_device_many": "cihaz",
|
||||
"tag_group_one": "dinamik grup",
|
||||
"tag_group_many": "dinamik grup"
|
||||
},
|
||||
"keys": {
|
||||
"title": "Sunucu Anahtarları",
|
||||
@@ -1775,7 +1794,8 @@
|
||||
"sessions_col_duration": "Süre",
|
||||
"sessions_active": "Aktif",
|
||||
"sessions_transport_punch": "Doğrudan / punch",
|
||||
"sessions_transport_relay": "Relay"
|
||||
"sessions_transport_relay": "Relay",
|
||||
"tags_unassign_hint": "Burada bir etiketi kaldırmak yalnızca bu cihazdan ayırır. Her yerde yeniden adlandırmak/silmek için Cihazlar sayfasındaki Etiketleri yönet’i kullanın."
|
||||
},
|
||||
"inventory": {
|
||||
"title": "Inventory",
|
||||
|
||||
+22
-2
@@ -441,7 +441,26 @@
|
||||
"group_commercialization_assign_existing": "Assign existing package",
|
||||
"group_commercialization_create_new": "Create new package and assign",
|
||||
"group_commercialization_saved": "Package assigned to group",
|
||||
"group_one_only": "A device can belong to only one group"
|
||||
"group_one_only": "A device can belong to only one group",
|
||||
"manage_tags": "Керувати тегами",
|
||||
"manage_device_tags": "Керувати тегами",
|
||||
"rename_tag": "Перейменувати тег",
|
||||
"delete_tag": "Видалити тег",
|
||||
"rename_tag_prompt": "Нова назва тегу",
|
||||
"rename_tag_hint": "Перейменовує цей тег на всіх видимих пристроях і оновлює відповідні динамічні групи.",
|
||||
"delete_tag_confirm": "Видалити тег \"{tag}\" з {count} пристрою(їв)? Динамічні групи з цим тегом будуть порожніми до оновлення.",
|
||||
"tag_renamed": "Тег перейменовано",
|
||||
"tag_deleted": "Тег видалено",
|
||||
"tag_name_required": "Назва тегу обов’язкова",
|
||||
"tag_rename_identical": "Нова назва має відрізнятися",
|
||||
"tags_backend_unsupported": "Теги пристроїв не підтримуються з поточним бекендом",
|
||||
"tags_manage_hint": "Перейменовуйте або видаляйте теги на всіх видимих пристроях. Зняття з одного пристрою — в Редагуванні або Деталях.",
|
||||
"tags_device_hint": "Видаліть тег тут, щоб відв’язати його лише від цього пристрою. Для глобальних змін використовуйте Керувати тегами.",
|
||||
"unassign_tag": "Видалити з цього пристрою",
|
||||
"tag_device_one": "пристрій",
|
||||
"tag_device_many": "пристроїв",
|
||||
"tag_group_one": "динамічна група",
|
||||
"tag_group_many": "динамічних груп"
|
||||
},
|
||||
"keys": {
|
||||
"title": "Ключі сервера",
|
||||
@@ -1775,7 +1794,8 @@
|
||||
"sessions_col_duration": "Тривалість",
|
||||
"sessions_active": "Активна",
|
||||
"sessions_transport_punch": "Пряме / punch",
|
||||
"sessions_transport_relay": "Relay"
|
||||
"sessions_transport_relay": "Relay",
|
||||
"tags_unassign_hint": "Видалення тегу тут відв’язує його лише від цього пристрою. Для глобальних змін використайте Керувати тегами на сторінці Пристрої."
|
||||
},
|
||||
"inventory": {
|
||||
"title": "Inventory",
|
||||
|
||||
+22
-2
@@ -441,7 +441,26 @@
|
||||
"group_commercialization_assign_existing": "Assign existing package",
|
||||
"group_commercialization_create_new": "Create new package and assign",
|
||||
"group_commercialization_saved": "Package assigned to group",
|
||||
"group_one_only": "A device can belong to only one group"
|
||||
"group_one_only": "A device can belong to only one group",
|
||||
"manage_tags": "Quản lý thẻ",
|
||||
"manage_device_tags": "Quản lý thẻ",
|
||||
"rename_tag": "Đổi tên thẻ",
|
||||
"delete_tag": "Xóa thẻ",
|
||||
"rename_tag_prompt": "Tên thẻ mới",
|
||||
"rename_tag_hint": "Đổi tên thẻ này trên tất cả thiết bị bạn thấy và cập nhật nhóm động khớp.",
|
||||
"delete_tag_confirm": "Gỡ thẻ \"{tag}\" khỏi {count} thiết bị? Nhóm động dùng thẻ này sẽ trống cho đến khi cập nhật.",
|
||||
"tag_renamed": "Đã đổi tên thẻ",
|
||||
"tag_deleted": "Đã xóa thẻ",
|
||||
"tag_name_required": "Tên thẻ là bắt buộc",
|
||||
"tag_rename_identical": "Tên mới phải khác",
|
||||
"tags_backend_unsupported": "Thẻ thiết bị không được hỗ trợ với backend hiện tại",
|
||||
"tags_manage_hint": "Đổi tên hoặc xóa thẻ trên tất cả thiết bị hiển thị. Gỡ khỏi một thiết bị trong Sửa hoặc Chi tiết.",
|
||||
"tags_device_hint": "Gỡ thẻ tại đây chỉ bỏ gán khỏi thiết bị này. Dùng Quản lý thẻ để đổi tên/xóa toàn cục.",
|
||||
"unassign_tag": "Gỡ khỏi thiết bị này",
|
||||
"tag_device_one": "thiết bị",
|
||||
"tag_device_many": "thiết bị",
|
||||
"tag_group_one": "nhóm động",
|
||||
"tag_group_many": "nhóm động"
|
||||
},
|
||||
"keys": {
|
||||
"title": "Khóa máy chủ",
|
||||
@@ -1775,7 +1794,8 @@
|
||||
"sessions_col_duration": "Thời lượng",
|
||||
"sessions_active": "Đang hoạt động",
|
||||
"sessions_transport_punch": "Trực tiếp / punch",
|
||||
"sessions_transport_relay": "Relay"
|
||||
"sessions_transport_relay": "Relay",
|
||||
"tags_unassign_hint": "Gỡ thẻ tại đây chỉ bỏ gán khỏi thiết bị này. Dùng Quản lý thẻ trên trang Thiết bị để đổi tên/xóa toàn cục."
|
||||
},
|
||||
"inventory": {
|
||||
"title": "Inventory",
|
||||
|
||||
@@ -439,7 +439,26 @@
|
||||
"group_commercialization_assign_existing": "Assign existing package",
|
||||
"group_commercialization_create_new": "Create new package and assign",
|
||||
"group_commercialization_saved": "Package assigned to group",
|
||||
"group_one_only": "A device can belong to only one group"
|
||||
"group_one_only": "A device can belong to only one group",
|
||||
"manage_tags": "管理標籤",
|
||||
"manage_device_tags": "管理標籤",
|
||||
"rename_tag": "重新命名標籤",
|
||||
"delete_tag": "刪除標籤",
|
||||
"rename_tag_prompt": "新標籤名稱",
|
||||
"rename_tag_hint": "在您可見的所有裝置上重新命名此標籤,並更新相符的動態群組。",
|
||||
"delete_tag_confirm": "從 {count} 台裝置移除標籤 \"{tag}\"?使用此標籤的動態群組在更新前將為空。",
|
||||
"tag_renamed": "標籤已重新命名",
|
||||
"tag_deleted": "標籤已刪除",
|
||||
"tag_name_required": "標籤名稱為必填",
|
||||
"tag_rename_identical": "新名稱必須不同",
|
||||
"tags_backend_unsupported": "目前後端不支援裝置標籤",
|
||||
"tags_manage_hint": "在您可見的所有裝置上重新命名或刪除標籤。從單一裝置移除請在編輯或詳細資料中操作。",
|
||||
"tags_device_hint": "在此移除標籤僅取消本裝置的指派。全域重新命名或刪除請使用管理標籤。",
|
||||
"unassign_tag": "從此裝置移除",
|
||||
"tag_device_one": "台裝置",
|
||||
"tag_device_many": "台裝置",
|
||||
"tag_group_one": "個動態群組",
|
||||
"tag_group_many": "個動態群組"
|
||||
},
|
||||
"keys": {
|
||||
"title": "服務器密鑰",
|
||||
@@ -1786,7 +1805,8 @@
|
||||
"sessions_col_duration": "時長",
|
||||
"sessions_active": "進行中",
|
||||
"sessions_transport_punch": "直連 / 打洞",
|
||||
"sessions_transport_relay": "中繼"
|
||||
"sessions_transport_relay": "中繼",
|
||||
"tags_unassign_hint": "在此移除標籤僅取消本裝置的指派。全域重新命名或刪除請在裝置頁使用管理標籤。"
|
||||
},
|
||||
"inventory": {
|
||||
"title": "資產清單",
|
||||
|
||||
+22
-2
@@ -435,7 +435,26 @@
|
||||
"group_commercialization_assign_existing": "Assign existing package",
|
||||
"group_commercialization_create_new": "Create new package and assign",
|
||||
"group_commercialization_saved": "Package assigned to group",
|
||||
"group_one_only": "A device can belong to only one group"
|
||||
"group_one_only": "A device can belong to only one group",
|
||||
"manage_tags": "管理标签",
|
||||
"manage_device_tags": "管理标签",
|
||||
"rename_tag": "重命名标签",
|
||||
"delete_tag": "删除标签",
|
||||
"rename_tag_prompt": "新标签名称",
|
||||
"rename_tag_hint": "在您可见的所有设备上重命名此标签,并更新匹配的动态组。",
|
||||
"delete_tag_confirm": "从 {count} 台设备移除标签 \"{tag}\"?使用此标签的动态组在更新前将为空。",
|
||||
"tag_renamed": "标签已重命名",
|
||||
"tag_deleted": "标签已删除",
|
||||
"tag_name_required": "标签名称为必填项",
|
||||
"tag_rename_identical": "新名称必须不同",
|
||||
"tags_backend_unsupported": "当前后端不支持设备标签",
|
||||
"tags_manage_hint": "在您可见的所有设备上重命名或删除标签。从单台设备移除请在编辑或详情中操作。",
|
||||
"tags_device_hint": "在此移除标签仅取消本设备的分配。全局重命名或删除请使用管理标签。",
|
||||
"unassign_tag": "从此设备移除",
|
||||
"tag_device_one": "台设备",
|
||||
"tag_device_many": "台设备",
|
||||
"tag_group_one": "个动态组",
|
||||
"tag_group_many": "个动态组"
|
||||
},
|
||||
"keys": {
|
||||
"title": "服务器密钥",
|
||||
@@ -1769,7 +1788,8 @@
|
||||
"sessions_col_duration": "时长",
|
||||
"sessions_active": "进行中",
|
||||
"sessions_transport_punch": "直连 / 打洞",
|
||||
"sessions_transport_relay": "中继"
|
||||
"sessions_transport_relay": "中继",
|
||||
"tags_unassign_hint": "在此移除标签仅取消本设备的分配。全局重命名或删除请在设备页使用管理标签。"
|
||||
},
|
||||
"inventory": {
|
||||
"title": "资产清单",
|
||||
|
||||
@@ -481,6 +481,12 @@ body.device-detail-open {
|
||||
TAGS TAB
|
||||
═══════════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
.device-panel-tags-hint {
|
||||
margin: 0 0 var(--space-sm, 0.5rem);
|
||||
color: var(--text-tertiary, #8b949e);
|
||||
font-size: var(--font-size-sm, 0.875rem);
|
||||
}
|
||||
|
||||
.device-panel-tags-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@@ -434,6 +434,74 @@
|
||||
border-color: var(--accent-blue) !important;
|
||||
}
|
||||
|
||||
.manage-tags-hint {
|
||||
margin: 0 0 var(--space-md);
|
||||
}
|
||||
|
||||
.manage-tags-empty {
|
||||
padding: var(--space-md);
|
||||
color: var(--text-tertiary);
|
||||
font-size: var(--font-size-sm);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.manage-tags-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
max-height: min(50vh, 420px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.manage-tags-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--border-primary);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--bg-secondary, transparent);
|
||||
}
|
||||
|
||||
.manage-tags-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.manage-tags-name {
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.manage-tags-meta {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.manage-tags-actions {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.manage-tags-actions .btn .material-icons {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.device-edit-tags-focus {
|
||||
outline: 1px solid var(--accent-blue);
|
||||
outline-offset: 4px;
|
||||
border-radius: var(--radius-md);
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
/* ==================== Toolbar ==================== */
|
||||
|
||||
.devices-toolbar {
|
||||
|
||||
@@ -840,12 +840,13 @@ const DeviceDetail = (function () {
|
||||
html += `
|
||||
<div class="device-panel-section">
|
||||
<div class="device-panel-section-title"><span class="material-icons">sell</span> ${_('device_detail.tags')}</div>
|
||||
<p class="form-hint device-panel-tags-hint">${_('device_detail.tags_unassign_hint') || _('devices.tags_device_hint') || 'Removing a tag here unassigns it from this device only. Use Manage Tags on the Devices page to rename or delete a tag everywhere.'}</p>
|
||||
<div class="device-panel-tags-container" id="dp-tags-list">
|
||||
${hasTags
|
||||
? tags.map(t => `
|
||||
<span class="device-panel-tag">
|
||||
${Utils.escapeHtml(t)}
|
||||
<button class="device-panel-tag-remove" data-tag="${Utils.escapeHtml(t)}" title="${_('actions.delete')}">
|
||||
<button class="device-panel-tag-remove" data-tag="${Utils.escapeHtml(t)}" title="${_('devices.unassign_tag') || _('actions.delete')}">
|
||||
<span class="material-icons">close</span>
|
||||
</button>
|
||||
</span>`).join('')
|
||||
|
||||
@@ -141,6 +141,7 @@
|
||||
initFilters();
|
||||
initShowDeletedToggle();
|
||||
initTagFilter();
|
||||
initManageTags();
|
||||
initSorting();
|
||||
initSync();
|
||||
document.getElementById('add-remote-target-btn')?.addEventListener('click', () => showRemoteTargetModal());
|
||||
@@ -401,6 +402,10 @@
|
||||
<span class="material-icons">edit</span>
|
||||
<span>${_('actions.edit')}</span>
|
||||
</button>
|
||||
<button type="button" class="kebab-menu-item" data-action="manage-tags" data-id="${eid}">
|
||||
<span class="material-icons">sell</span>
|
||||
<span>${_('devices.manage_device_tags') || 'Manage tags'}</span>
|
||||
</button>
|
||||
<button type="button" class="kebab-menu-item" data-action="groups" data-id="${eid}">
|
||||
<span class="material-icons">hub</span>
|
||||
<span>${_('devices.manage_groups') || 'Manage Groups'}</span>
|
||||
@@ -509,6 +514,10 @@
|
||||
<span class="material-icons">edit</span>
|
||||
<span>${_('actions.edit')}</span>
|
||||
</button>
|
||||
<button type="button" class="kebab-menu-item" data-action="manage-tags" data-id="${eid}">
|
||||
<span class="material-icons">sell</span>
|
||||
<span>${_('devices.manage_device_tags') || 'Manage tags'}</span>
|
||||
</button>
|
||||
<button type="button" class="kebab-menu-item" data-action="groups" data-id="${eid}">
|
||||
<span class="material-icons">hub</span>
|
||||
<span>${_('devices.manage_groups') || 'Manage Groups'}</span>
|
||||
@@ -1286,6 +1295,14 @@
|
||||
}
|
||||
break;
|
||||
|
||||
case 'manage-tags':
|
||||
if (devices.find(d => d.id === deviceId)?.remote_target) {
|
||||
showEditModal(deviceId);
|
||||
} else {
|
||||
showEditModal(deviceId, { focusTags: true });
|
||||
}
|
||||
break;
|
||||
|
||||
case 'groups':
|
||||
showDeviceMembershipModal(deviceId);
|
||||
break;
|
||||
@@ -2284,9 +2301,10 @@
|
||||
/**
|
||||
* Show edit modal
|
||||
*/
|
||||
function showEditModal(deviceId) {
|
||||
function showEditModal(deviceId, options = {}) {
|
||||
const device = devices.find(d => d.id === deviceId);
|
||||
if (!device) return;
|
||||
const focusTags = !!(options && options.focusTags);
|
||||
|
||||
const editTags = new Set(normalizeTags(device.tags));
|
||||
const editTagInputId = 'edit-tag-input';
|
||||
@@ -2304,7 +2322,7 @@
|
||||
${Utils.escapeHtml(tag)}
|
||||
<button type="button" class="device-edit-tag-remove"
|
||||
data-edit-tag-remove="${Utils.escapeHtml(tag)}"
|
||||
title="${Utils.escapeHtml(_('actions.delete'))}">
|
||||
title="${Utils.escapeHtml(_('devices.unassign_tag') || _('actions.delete'))}">
|
||||
<span class="material-icons">close</span>
|
||||
</button>
|
||||
</span>
|
||||
@@ -2347,7 +2365,9 @@
|
||||
};
|
||||
|
||||
Modal.show({
|
||||
title: _('devices.edit_title'),
|
||||
title: focusTags
|
||||
? (_('devices.manage_device_tags') || _('devices.edit_title'))
|
||||
: _('devices.edit_title'),
|
||||
content: `
|
||||
<div class="device-info-grid">
|
||||
<div class="device-info-item">
|
||||
@@ -2369,8 +2389,9 @@
|
||||
<textarea id="edit-note" class="form-input" rows="2"
|
||||
placeholder="${_('devices.note_placeholder')}" maxlength="512">${Utils.escapeHtml(device.note || '')}</textarea>
|
||||
</div>
|
||||
<div class="device-info-item full-width device-edit-tags-field">
|
||||
<div class="device-info-item full-width device-edit-tags-field${focusTags ? ' device-edit-tags-focus' : ''}">
|
||||
<label for="${editTagInputId}">${_('device_detail.tags')}</label>
|
||||
<p class="form-hint">${Utils.escapeHtml(_('devices.tags_device_hint') || 'Remove a tag here to unassign it from this device only. Use Manage Tags to rename or delete a tag everywhere.')}</p>
|
||||
<div id="${editTagsSelectedId}" class="device-edit-tags-selected"></div>
|
||||
<div id="${editTagOptionsId}" class="device-edit-tag-options"></div>
|
||||
<div class="device-edit-tag-input-row">
|
||||
@@ -2480,12 +2501,173 @@
|
||||
addEditTag();
|
||||
}
|
||||
});
|
||||
if (focusTags) {
|
||||
const field = document.querySelector('.device-edit-tags-field');
|
||||
field?.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
||||
setTimeout(() => input?.focus(), 100);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
window.BetterDeskDevices = window.BetterDeskDevices || {};
|
||||
window.BetterDeskDevices.showEditModal = showEditModal;
|
||||
|
||||
function countDevicesWithTag(tag) {
|
||||
const expected = String(tag || '').trim().toLowerCase();
|
||||
if (!expected) return 0;
|
||||
return devices.filter(device =>
|
||||
normalizeTags(device.tags).some(t => t.toLowerCase() === expected)
|
||||
).length;
|
||||
}
|
||||
|
||||
function countGroupsWithTag(tag) {
|
||||
const expected = String(tag || '').trim().toLowerCase();
|
||||
if (!expected) return 0;
|
||||
return deviceGroups.filter(group =>
|
||||
(group.source_type || 'manual') === 'tag'
|
||||
&& String(group.tag_filter || '').trim().toLowerCase() === expected
|
||||
).length;
|
||||
}
|
||||
|
||||
function showManageTagsModal() {
|
||||
const renderList = () => {
|
||||
if (!availableTags.length) {
|
||||
return `<div class="manage-tags-empty">${Utils.escapeHtml(_('devices.no_tags') || 'No tags')}</div>`;
|
||||
}
|
||||
return `<ul class="manage-tags-list">${availableTags.map(tag => {
|
||||
const deviceCount = countDevicesWithTag(tag);
|
||||
const groupCount = countGroupsWithTag(tag);
|
||||
const metaParts = [
|
||||
`${deviceCount} ${deviceCount === 1 ? (_('devices.tag_device_one') || 'device') : (_('devices.tag_device_many') || 'devices')}`
|
||||
];
|
||||
if (groupCount > 0) {
|
||||
metaParts.push(`${groupCount} ${groupCount === 1 ? (_('devices.tag_group_one') || 'dynamic group') : (_('devices.tag_group_many') || 'dynamic groups')}`);
|
||||
}
|
||||
return `<li class="manage-tags-row" data-tag="${Utils.escapeHtml(tag)}">
|
||||
<div class="manage-tags-info">
|
||||
<span class="manage-tags-name">${Utils.escapeHtml(tag)}</span>
|
||||
<span class="manage-tags-meta">${Utils.escapeHtml(metaParts.join(' · '))}</span>
|
||||
</div>
|
||||
<div class="manage-tags-actions">
|
||||
<button type="button" class="btn btn-secondary btn-sm manage-tag-rename" data-tag="${Utils.escapeHtml(tag)}" title="${Utils.escapeHtml(_('devices.rename_tag') || 'Rename')}">
|
||||
<span class="material-icons">edit</span>
|
||||
<span>${Utils.escapeHtml(_('devices.rename_tag') || 'Rename')}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-danger btn-sm manage-tag-delete" data-tag="${Utils.escapeHtml(tag)}" title="${Utils.escapeHtml(_('devices.delete_tag') || 'Delete')}">
|
||||
<span class="material-icons">delete</span>
|
||||
<span>${Utils.escapeHtml(_('devices.delete_tag') || 'Delete')}</span>
|
||||
</button>
|
||||
</div>
|
||||
</li>`;
|
||||
}).join('')}</ul>`;
|
||||
};
|
||||
|
||||
const bindActions = () => {
|
||||
document.querySelectorAll('.manage-tag-rename').forEach(button => {
|
||||
button.addEventListener('click', async () => {
|
||||
const from = button.dataset.tag;
|
||||
if (!from) return;
|
||||
const to = await Modal.prompt({
|
||||
title: _('devices.rename_tag') || 'Rename tag',
|
||||
label: _('devices.rename_tag_prompt') || 'New tag name',
|
||||
value: from,
|
||||
placeholder: from,
|
||||
hint: _('devices.rename_tag_hint') || 'Renames this tag on all devices you can see and updates matching dynamic groups.',
|
||||
confirmLabel: _('actions.save')
|
||||
});
|
||||
if (to === null) return;
|
||||
const cleaned = String(to || '').trim();
|
||||
if (!cleaned) {
|
||||
Notifications.error(_('devices.tag_name_required') || 'Tag name is required');
|
||||
return;
|
||||
}
|
||||
if (cleaned === from) return;
|
||||
try {
|
||||
const result = await Utils.api('/api/tags/rename', {
|
||||
method: 'POST',
|
||||
body: { from, to: cleaned }
|
||||
});
|
||||
if (selectedTags.has(from)) {
|
||||
selectedTags.delete(from);
|
||||
selectedTags.add(result?.to || cleaned);
|
||||
}
|
||||
Notifications.success(
|
||||
(_('devices.tag_renamed') || 'Tag renamed')
|
||||
+ ` (${result?.updatedDevices || 0})`
|
||||
);
|
||||
Modal.close();
|
||||
await loadTags();
|
||||
await loadDeviceGroups();
|
||||
await loadDevices();
|
||||
showManageTagsModal();
|
||||
} catch (err) {
|
||||
Notifications.error(err.message || _('errors.server_error'));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.manage-tag-delete').forEach(button => {
|
||||
button.addEventListener('click', async () => {
|
||||
const tag = button.dataset.tag;
|
||||
if (!tag) return;
|
||||
const deviceCount = countDevicesWithTag(tag);
|
||||
const groupCount = countGroupsWithTag(tag);
|
||||
const confirmMsg = (_('devices.delete_tag_confirm') || 'Remove tag "{tag}" from {count} device(s)? Dynamic groups that use this tag will become empty until updated.')
|
||||
.replace('{tag}', tag)
|
||||
.replace('{count}', String(deviceCount))
|
||||
.replace('{groups}', String(groupCount));
|
||||
const confirmed = await Modal.confirm({
|
||||
title: _('devices.delete_tag') || 'Delete tag',
|
||||
message: confirmMsg,
|
||||
danger: true,
|
||||
confirmLabel: _('actions.delete')
|
||||
});
|
||||
if (!confirmed) return;
|
||||
try {
|
||||
const result = await Utils.api('/api/tags/delete', {
|
||||
method: 'POST',
|
||||
body: { tag }
|
||||
});
|
||||
selectedTags.delete(tag);
|
||||
Notifications.success(
|
||||
(_('devices.tag_deleted') || 'Tag deleted')
|
||||
+ ` (${result?.updatedDevices || 0})`
|
||||
);
|
||||
Modal.close();
|
||||
await loadTags();
|
||||
await loadDeviceGroups();
|
||||
await loadDevices();
|
||||
showManageTagsModal();
|
||||
} catch (err) {
|
||||
Notifications.error(err.message || _('errors.server_error'));
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
Modal.show({
|
||||
title: _('devices.manage_tags') || 'Manage tags',
|
||||
content: `
|
||||
<p class="form-hint manage-tags-hint">${Utils.escapeHtml(_('devices.tags_manage_hint') || 'Rename or delete tags across all devices you can see. Removing a tag from one device is done in Edit / Device details.')}</p>
|
||||
${renderList()}
|
||||
`,
|
||||
size: 'medium',
|
||||
buttons: [
|
||||
{ label: _('actions.close'), class: 'btn-secondary', onClick: () => Modal.close() }
|
||||
],
|
||||
onOpen: bindActions
|
||||
});
|
||||
}
|
||||
|
||||
function initManageTags() {
|
||||
const btn = document.getElementById('manage-tags-btn');
|
||||
if (!btn) return;
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
showManageTagsModal();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Render pagination
|
||||
|
||||
@@ -9,6 +9,7 @@ const serverBackend = require('../services/serverBackend');
|
||||
const betterdeskApi = require('../services/betterdeskApi');
|
||||
const addressBookSync = require('../services/rustdeskAddressBookSync');
|
||||
const deviceGroupService = require('../services/deviceGroupService');
|
||||
const tagManagementService = require('../services/tagManagementService');
|
||||
const { requireAuth, requirePermission, requestHasPermission } = require('../middleware/auth');
|
||||
const { bodyInt, bodyString, bodyBool, plainBodyObject } = require('../lib/bodyScalars');
|
||||
|
||||
@@ -250,6 +251,134 @@ router.get('/api/tags', requireAuth, requirePermission('device.view'), async (re
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/tags/rename - Rename a tag on all visible devices (and matching dynamic groups).
|
||||
*/
|
||||
router.post('/api/tags/rename', requireAuth, requirePermission('device.edit'), async (req, res) => {
|
||||
try {
|
||||
if (!(await serverBackend.isBetterDesk())) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: req.t('devices.tags_backend_unsupported')
|
||||
});
|
||||
}
|
||||
|
||||
const from = tagManagementService.sanitizeTagName(bodyString(req.body && req.body.from));
|
||||
const to = tagManagementService.sanitizeTagName(bodyString(req.body && req.body.to));
|
||||
|
||||
if (!from || !to) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: req.t('devices.tag_name_required')
|
||||
});
|
||||
}
|
||||
if (from === to) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: req.t('devices.tag_rename_identical')
|
||||
});
|
||||
}
|
||||
|
||||
const devices = await getVisibleDevicesForRequest(req);
|
||||
const groups = await getVisibleDeviceGroupsForRequest(req);
|
||||
const result = await tagManagementService.renameTagAcrossScope({
|
||||
devices,
|
||||
groups,
|
||||
fromTag: from,
|
||||
toTag: to,
|
||||
setPeerTags: (id, tags) => serverBackend.setPeerTags(id, tags),
|
||||
updateDeviceGroup: (guid, data) => db.updateDeviceGroup(guid, data)
|
||||
});
|
||||
|
||||
if (result.errors && result.errors.includes('identical')) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: req.t('devices.tag_rename_identical')
|
||||
});
|
||||
}
|
||||
|
||||
await db.logAction(
|
||||
req.session.userId,
|
||||
'device_tag_renamed',
|
||||
`Tag "${from}" renamed to "${to}" on ${result.updatedDevices} device(s), ${result.updatedGroups} group(s)`,
|
||||
req.ip
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
from: result.from,
|
||||
to: result.to,
|
||||
updatedDevices: result.updatedDevices,
|
||||
updatedGroups: result.updatedGroups,
|
||||
skipped: result.skipped,
|
||||
failedDevices: result.errors || []
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Rename tag error:', err);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: req.t('errors.server_error')
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/tags/delete - Remove a tag from all visible devices.
|
||||
*/
|
||||
router.post('/api/tags/delete', requireAuth, requirePermission('device.edit'), async (req, res) => {
|
||||
try {
|
||||
if (!(await serverBackend.isBetterDesk())) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: req.t('devices.tags_backend_unsupported')
|
||||
});
|
||||
}
|
||||
|
||||
const tag = tagManagementService.sanitizeTagName(bodyString(req.body && req.body.tag));
|
||||
if (!tag) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: req.t('devices.tag_name_required')
|
||||
});
|
||||
}
|
||||
|
||||
const devices = await getVisibleDevicesForRequest(req);
|
||||
const groups = await getVisibleDeviceGroupsForRequest(req);
|
||||
const result = await tagManagementService.deleteTagAcrossScope({
|
||||
devices,
|
||||
groups,
|
||||
tag,
|
||||
setPeerTags: (id, tags) => serverBackend.setPeerTags(id, tags)
|
||||
});
|
||||
|
||||
await db.logAction(
|
||||
req.session.userId,
|
||||
'device_tag_deleted',
|
||||
`Tag "${tag}" removed from ${result.updatedDevices} device(s) (${result.affectedGroups} dynamic group(s) reference it)`,
|
||||
req.ip
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
tag: result.tag,
|
||||
updatedDevices: result.updatedDevices,
|
||||
skipped: result.skipped,
|
||||
affectedGroups: result.affectedGroups,
|
||||
failedDevices: result.errors || []
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Delete tag error:', err);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: req.t('errors.server_error')
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function isValidGroupGuid(guid) {
|
||||
return typeof guid === 'string' && guid.length > 0 && guid.length <= 80 && /^[A-Za-z0-9_.:-]+$/.test(guid);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
/**
|
||||
* Device tag rename / delete helpers.
|
||||
* Tags remain free-form strings on peers; these helpers rewrite visible peers only.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const addressBookSync = require('./rustdeskAddressBookSync');
|
||||
|
||||
const MAX_TAG_LENGTH = 50;
|
||||
const MAX_TAGS_PER_DEVICE = 20;
|
||||
const SET_PEER_TAGS_CONCURRENCY = 4;
|
||||
|
||||
function sanitizeTagName(value) {
|
||||
if (typeof value !== 'string') return '';
|
||||
return value
|
||||
.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, '')
|
||||
.trim()
|
||||
.slice(0, MAX_TAG_LENGTH);
|
||||
}
|
||||
|
||||
function tagsEqual(a, b) {
|
||||
return String(a || '').trim().toLowerCase() === String(b || '').trim().toLowerCase();
|
||||
}
|
||||
|
||||
function deviceHasTag(device, tag) {
|
||||
const expected = sanitizeTagName(tag).toLowerCase();
|
||||
if (!expected) return false;
|
||||
return addressBookSync.normalizeTags(device && device.tags).some(t => t.toLowerCase() === expected);
|
||||
}
|
||||
|
||||
function rewriteTagsRename(tags, fromTag, toTag) {
|
||||
const from = sanitizeTagName(fromTag);
|
||||
const to = sanitizeTagName(toTag);
|
||||
if (!from || !to) return addressBookSync.normalizeTags(tags);
|
||||
|
||||
const next = [];
|
||||
const seen = new Set();
|
||||
let replaced = false;
|
||||
|
||||
for (const tag of addressBookSync.normalizeTags(tags)) {
|
||||
let value = tag;
|
||||
if (tagsEqual(tag, from)) {
|
||||
value = to;
|
||||
replaced = true;
|
||||
}
|
||||
const key = value.toLowerCase();
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
next.push(value);
|
||||
if (next.length >= MAX_TAGS_PER_DEVICE) break;
|
||||
}
|
||||
|
||||
return { tags: next, changed: replaced };
|
||||
}
|
||||
|
||||
function rewriteTagsDelete(tags, tagToRemove) {
|
||||
const remove = sanitizeTagName(tagToRemove);
|
||||
if (!remove) return { tags: addressBookSync.normalizeTags(tags), changed: false };
|
||||
|
||||
const before = addressBookSync.normalizeTags(tags);
|
||||
const next = before.filter(t => !tagsEqual(t, remove));
|
||||
return { tags: next, changed: next.length !== before.length };
|
||||
}
|
||||
|
||||
async function mapWithConcurrency(items, concurrency, mapper) {
|
||||
const list = Array.isArray(items) ? items : [];
|
||||
const limit = Math.max(1, Number(concurrency) || 1);
|
||||
const results = new Array(list.length);
|
||||
let index = 0;
|
||||
|
||||
async function worker() {
|
||||
while (index < list.length) {
|
||||
const current = index++;
|
||||
results[current] = await mapper(list[current], current);
|
||||
}
|
||||
}
|
||||
|
||||
const workers = [];
|
||||
for (let i = 0; i < Math.min(limit, list.length); i++) {
|
||||
workers.push(worker());
|
||||
}
|
||||
await Promise.all(workers);
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename a tag across visible devices and matching dynamic groups.
|
||||
* @returns {{ updatedDevices: number, updatedGroups: number, skipped: number, errors: string[] }}
|
||||
*/
|
||||
async function renameTagAcrossScope({
|
||||
devices,
|
||||
groups,
|
||||
fromTag,
|
||||
toTag,
|
||||
setPeerTags,
|
||||
updateDeviceGroup
|
||||
}) {
|
||||
const from = sanitizeTagName(fromTag);
|
||||
const to = sanitizeTagName(toTag);
|
||||
const errors = [];
|
||||
|
||||
if (!from || !to) {
|
||||
return { updatedDevices: 0, updatedGroups: 0, skipped: 0, errors: ['invalid_tag'], from, to };
|
||||
}
|
||||
// Exact same string after sanitize — nothing to do (case-only renames still proceed)
|
||||
if (from === to) {
|
||||
return { updatedDevices: 0, updatedGroups: 0, skipped: 0, errors: ['identical'], from, to };
|
||||
}
|
||||
|
||||
const targets = (devices || []).filter(d => deviceHasTag(d, from));
|
||||
let updatedDevices = 0;
|
||||
let skipped = 0;
|
||||
|
||||
await mapWithConcurrency(targets, SET_PEER_TAGS_CONCURRENCY, async (device) => {
|
||||
const { tags, changed } = rewriteTagsRename(device.tags, from, to);
|
||||
if (!changed) {
|
||||
skipped += 1;
|
||||
return;
|
||||
}
|
||||
const result = await setPeerTags(device.id, tags);
|
||||
if (!result || result.success === false) {
|
||||
errors.push(String(device.id));
|
||||
skipped += 1;
|
||||
return;
|
||||
}
|
||||
device.tags = tags;
|
||||
updatedDevices += 1;
|
||||
});
|
||||
|
||||
let updatedGroups = 0;
|
||||
for (const group of groups || []) {
|
||||
if ((group.source_type || 'manual') !== 'tag') continue;
|
||||
const filter = String(group.tag_filter || '').trim();
|
||||
if (!filter || !tagsEqual(filter, from)) continue;
|
||||
if (typeof updateDeviceGroup === 'function') {
|
||||
await updateDeviceGroup(group.guid, { tag_filter: to });
|
||||
group.tag_filter = to;
|
||||
updatedGroups += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return { updatedDevices, updatedGroups, skipped, errors, from, to };
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a tag from all visible devices (dynamic group filters left unchanged).
|
||||
* @returns {{ updatedDevices: number, skipped: number, errors: string[], affectedGroups: number }}
|
||||
*/
|
||||
async function deleteTagAcrossScope({
|
||||
devices,
|
||||
groups,
|
||||
tag,
|
||||
setPeerTags
|
||||
}) {
|
||||
const target = sanitizeTagName(tag);
|
||||
const errors = [];
|
||||
|
||||
if (!target) {
|
||||
return { updatedDevices: 0, skipped: 0, errors: ['invalid_tag'], affectedGroups: 0, tag: target };
|
||||
}
|
||||
|
||||
const peers = (devices || []).filter(d => deviceHasTag(d, target));
|
||||
let updatedDevices = 0;
|
||||
let skipped = 0;
|
||||
|
||||
await mapWithConcurrency(peers, SET_PEER_TAGS_CONCURRENCY, async (device) => {
|
||||
const { tags, changed } = rewriteTagsDelete(device.tags, target);
|
||||
if (!changed) {
|
||||
skipped += 1;
|
||||
return;
|
||||
}
|
||||
const result = await setPeerTags(device.id, tags);
|
||||
if (!result || result.success === false) {
|
||||
errors.push(String(device.id));
|
||||
skipped += 1;
|
||||
return;
|
||||
}
|
||||
device.tags = tags;
|
||||
updatedDevices += 1;
|
||||
});
|
||||
|
||||
const affectedGroups = (groups || []).filter(group =>
|
||||
(group.source_type || 'manual') === 'tag'
|
||||
&& tagsEqual(group.tag_filter, target)
|
||||
).length;
|
||||
|
||||
return { updatedDevices, skipped, errors, affectedGroups, tag: target };
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
MAX_TAG_LENGTH,
|
||||
sanitizeTagName,
|
||||
tagsEqual,
|
||||
deviceHasTag,
|
||||
rewriteTagsRename,
|
||||
rewriteTagsDelete,
|
||||
renameTagAcrossScope,
|
||||
deleteTagAcrossScope
|
||||
};
|
||||
@@ -43,7 +43,9 @@ jest.mock('../services/serverBackend', () => ({
|
||||
updateDevice: jest.fn().mockResolvedValue(undefined),
|
||||
deleteDevice: jest.fn().mockResolvedValue(undefined),
|
||||
restoreDevice: jest.fn().mockResolvedValue({ success: true }),
|
||||
changePeerId: jest.fn().mockResolvedValue({ success: true })
|
||||
changePeerId: jest.fn().mockResolvedValue({ success: true }),
|
||||
isBetterDesk: jest.fn().mockResolvedValue(true),
|
||||
setPeerTags: jest.fn().mockResolvedValue({ success: true })
|
||||
}));
|
||||
|
||||
const serverBackend = require('../services/serverBackend');
|
||||
@@ -238,6 +240,140 @@ describe('Devices Routes', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /api/tags/rename', () => {
|
||||
it('should rename a tag on visible devices and update dynamic group filters', async () => {
|
||||
serverBackend.getAllDevices.mockResolvedValue([
|
||||
{ id: 'A1', tags: ['Linux', 'Kiosk'] },
|
||||
{ id: 'A2', tags: ['Windows'] },
|
||||
{ id: 'A3', tags: 'linux,Lab' }
|
||||
]);
|
||||
db.getAllDeviceGroups.mockResolvedValue([
|
||||
{ guid: 'linux', name: 'Linux Devices', source_type: 'tag', tag_filter: 'Linux', allowed_users: [] }
|
||||
]);
|
||||
serverBackend.setPeerTags.mockResolvedValue({ success: true });
|
||||
|
||||
const res = await request(app)
|
||||
.post('/api/tags/rename')
|
||||
.send({ from: 'Linux', to: 'GNU/Linux' });
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.success).toBe(true);
|
||||
expect(res.body.data.updatedDevices).toBe(2);
|
||||
expect(res.body.data.updatedGroups).toBe(1);
|
||||
expect(serverBackend.setPeerTags).toHaveBeenCalledWith('A1', ['GNU/Linux', 'Kiosk']);
|
||||
expect(serverBackend.setPeerTags).toHaveBeenCalledWith('A3', ['GNU/Linux', 'Lab']);
|
||||
expect(serverBackend.setPeerTags).not.toHaveBeenCalledWith('A2', expect.anything());
|
||||
expect(db.updateDeviceGroup).toHaveBeenCalledWith('linux', { tag_filter: 'GNU/Linux' });
|
||||
expect(db.logAction).toHaveBeenCalledWith(
|
||||
1,
|
||||
'device_tag_renamed',
|
||||
expect.stringContaining('Linux'),
|
||||
expect.anything()
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject empty or identical rename', async () => {
|
||||
const empty = await request(app).post('/api/tags/rename').send({ from: '', to: 'X' });
|
||||
expect(empty.status).toBe(400);
|
||||
|
||||
const identical = await request(app).post('/api/tags/rename').send({ from: 'Same', to: 'Same' });
|
||||
expect(identical.status).toBe(400);
|
||||
expect(serverBackend.setPeerTags).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should reject rename when backend is not BetterDesk', async () => {
|
||||
serverBackend.isBetterDesk.mockResolvedValueOnce(false);
|
||||
const res = await request(app).post('/api/tags/rename').send({ from: 'A', to: 'B' });
|
||||
expect(res.status).toBe(400);
|
||||
expect(serverBackend.setPeerTags).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should only rewrite devices in operator scope', async () => {
|
||||
app = createTestApp();
|
||||
app.use((req, _res, next) => {
|
||||
req.session.userId = 2;
|
||||
req.session.user = { id: 2, username: 'operator1', role: 'operator' };
|
||||
next();
|
||||
});
|
||||
app.use('/', devicesRoutes);
|
||||
|
||||
serverBackend.getAllDevices.mockResolvedValue([
|
||||
{ id: 'VISIBLE', tags: ['Fleet'] },
|
||||
{ id: 'HIDDEN', tags: ['Fleet'] }
|
||||
]);
|
||||
db.getAllDeviceGroups.mockResolvedValue([
|
||||
{
|
||||
guid: 'ops',
|
||||
name: 'Ops',
|
||||
source_type: 'manual',
|
||||
tag_filter: '',
|
||||
allowed_users: ['operator1']
|
||||
},
|
||||
{
|
||||
guid: 'other',
|
||||
name: 'Other',
|
||||
source_type: 'manual',
|
||||
tag_filter: '',
|
||||
allowed_users: ['someone_else']
|
||||
}
|
||||
]);
|
||||
db.getDeviceGroupMembers.mockImplementation(async (guid) => {
|
||||
if (guid === 'ops') return ['VISIBLE'];
|
||||
if (guid === 'other') return ['HIDDEN'];
|
||||
return [];
|
||||
});
|
||||
serverBackend.setPeerTags.mockResolvedValue({ success: true });
|
||||
|
||||
const res = await request(app)
|
||||
.post('/api/tags/rename')
|
||||
.send({ from: 'Fleet', to: 'Prod' });
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.data.updatedDevices).toBe(1);
|
||||
expect(serverBackend.setPeerTags).toHaveBeenCalledTimes(1);
|
||||
expect(serverBackend.setPeerTags).toHaveBeenCalledWith('VISIBLE', ['Prod']);
|
||||
expect(serverBackend.setPeerTags).not.toHaveBeenCalledWith('HIDDEN', expect.anything());
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /api/tags/delete', () => {
|
||||
it('should remove a tag from all visible devices', async () => {
|
||||
serverBackend.getAllDevices.mockResolvedValue([
|
||||
{ id: 'A1', tags: ['Temp', 'Keep'] },
|
||||
{ id: 'A2', tags: ['Keep'] },
|
||||
{ id: 'A3', tags: 'temp' }
|
||||
]);
|
||||
db.getAllDeviceGroups.mockResolvedValue([
|
||||
{ guid: 'temp-g', name: 'Temp', source_type: 'tag', tag_filter: 'Temp', allowed_users: [] }
|
||||
]);
|
||||
serverBackend.setPeerTags.mockResolvedValue({ success: true });
|
||||
|
||||
const res = await request(app)
|
||||
.post('/api/tags/delete')
|
||||
.send({ tag: 'Temp' });
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.success).toBe(true);
|
||||
expect(res.body.data.updatedDevices).toBe(2);
|
||||
expect(res.body.data.affectedGroups).toBe(1);
|
||||
expect(serverBackend.setPeerTags).toHaveBeenCalledWith('A1', ['Keep']);
|
||||
expect(serverBackend.setPeerTags).toHaveBeenCalledWith('A3', []);
|
||||
expect(db.updateDeviceGroup).not.toHaveBeenCalled();
|
||||
expect(db.logAction).toHaveBeenCalledWith(
|
||||
1,
|
||||
'device_tag_deleted',
|
||||
expect.stringContaining('Temp'),
|
||||
expect.anything()
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject empty tag delete', async () => {
|
||||
const res = await request(app).post('/api/tags/delete').send({ tag: ' ' });
|
||||
expect(res.status).toBe(400);
|
||||
expect(serverBackend.setPeerTags).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Device groups', () => {
|
||||
it('should count dynamic tag groups from visible devices', async () => {
|
||||
serverBackend.getAllDevices.mockResolvedValue([
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* Tag management helper unit tests
|
||||
*/
|
||||
|
||||
const {
|
||||
sanitizeTagName,
|
||||
rewriteTagsRename,
|
||||
rewriteTagsDelete,
|
||||
renameTagAcrossScope,
|
||||
deleteTagAcrossScope
|
||||
} = require('../services/tagManagementService');
|
||||
|
||||
describe('tagManagementService', () => {
|
||||
it('sanitizes tag names', () => {
|
||||
expect(sanitizeTagName(' Hello\x00 ')).toBe('Hello');
|
||||
expect(sanitizeTagName('x'.repeat(80))).toHaveLength(50);
|
||||
expect(sanitizeTagName(null)).toBe('');
|
||||
});
|
||||
|
||||
it('renames tags case-insensitively and dedupes', () => {
|
||||
expect(rewriteTagsRename(['Linux', 'Kiosk'], 'linux', 'GNU')).toEqual({
|
||||
tags: ['GNU', 'Kiosk'],
|
||||
changed: true
|
||||
});
|
||||
expect(rewriteTagsRename(['GNU', 'Linux'], 'Linux', 'GNU')).toEqual({
|
||||
tags: ['GNU'],
|
||||
changed: true
|
||||
});
|
||||
expect(rewriteTagsRename(['Windows'], 'Linux', 'GNU').changed).toBe(false);
|
||||
});
|
||||
|
||||
it('deletes tags case-insensitively', () => {
|
||||
expect(rewriteTagsDelete(['Temp', 'Keep'], 'temp')).toEqual({
|
||||
tags: ['Keep'],
|
||||
changed: true
|
||||
});
|
||||
expect(rewriteTagsDelete(['Keep'], 'Temp').changed).toBe(false);
|
||||
});
|
||||
|
||||
it('renameTagAcrossScope updates devices and groups', async () => {
|
||||
const setPeerTags = jest.fn().mockResolvedValue({ success: true });
|
||||
const updateDeviceGroup = jest.fn().mockResolvedValue(undefined);
|
||||
const devices = [
|
||||
{ id: '1', tags: ['A', 'B'] },
|
||||
{ id: '2', tags: ['C'] }
|
||||
];
|
||||
const groups = [
|
||||
{ guid: 'g1', source_type: 'tag', tag_filter: 'A' },
|
||||
{ guid: 'g2', source_type: 'manual', tag_filter: 'A' }
|
||||
];
|
||||
|
||||
const result = await renameTagAcrossScope({
|
||||
devices,
|
||||
groups,
|
||||
fromTag: 'A',
|
||||
toTag: 'Z',
|
||||
setPeerTags,
|
||||
updateDeviceGroup
|
||||
});
|
||||
|
||||
expect(result.updatedDevices).toBe(1);
|
||||
expect(result.updatedGroups).toBe(1);
|
||||
expect(setPeerTags).toHaveBeenCalledWith('1', ['Z', 'B']);
|
||||
expect(updateDeviceGroup).toHaveBeenCalledWith('g1', { tag_filter: 'Z' });
|
||||
expect(groups[0].tag_filter).toBe('Z');
|
||||
});
|
||||
|
||||
it('deleteTagAcrossScope strips tags and reports affected groups', async () => {
|
||||
const setPeerTags = jest.fn().mockResolvedValue({ success: true });
|
||||
const result = await deleteTagAcrossScope({
|
||||
devices: [
|
||||
{ id: '1', tags: ['Temp'] },
|
||||
{ id: '2', tags: ['Keep'] }
|
||||
],
|
||||
groups: [{ guid: 'g', source_type: 'tag', tag_filter: 'Temp' }],
|
||||
tag: 'Temp',
|
||||
setPeerTags
|
||||
});
|
||||
|
||||
expect(result.updatedDevices).toBe(1);
|
||||
expect(result.affectedGroups).toBe(1);
|
||||
expect(setPeerTags).toHaveBeenCalledWith('1', []);
|
||||
});
|
||||
});
|
||||
@@ -108,6 +108,9 @@
|
||||
<div class="tag-filter-empty">${_('devices.no_tags')}</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn-icon toolbar-icon" id="manage-tags-btn" title="${_('devices.manage_tags')}">
|
||||
<span class="material-icons">label</span>
|
||||
</button>
|
||||
<div class="column-visibility-dropdown">
|
||||
<button class="btn-icon toolbar-icon" id="columns-btn" title="${_('devices.toggle_columns')}">
|
||||
<span class="material-icons">view_column</span>
|
||||
|
||||
Reference in New Issue
Block a user