diff --git a/static/app.js b/static/app.js index b4eb10e..acdecb3 100644 --- a/static/app.js +++ b/static/app.js @@ -1,5 +1,43 @@ const SESSION_KEY_NAME = "ignis_session_key"; const DEFAULT_STATS_DAYS = 7; +const GROUP_ID_SANITIZE_PATTERN = /[^a-z0-9_-]+/g; +const GROUP_ID_DASH_COLLAPSE_PATTERN = /[-_]{2,}/g; +const GROUP_ID_EDGE_TRIM_PATTERN = /^[-_]+|[-_]+$/g; +const GROUP_ID_TRANSLITERATION_MAP = { + а: "a", + б: "b", + в: "v", + г: "g", + д: "d", + е: "e", + ё: "e", + ж: "zh", + з: "z", + и: "i", + й: "y", + к: "k", + л: "l", + м: "m", + н: "n", + о: "o", + п: "p", + р: "r", + с: "s", + т: "t", + у: "u", + ф: "f", + х: "h", + ц: "ts", + ч: "ch", + ш: "sh", + щ: "sch", + ъ: "", + ы: "y", + ь: "", + э: "e", + ю: "yu", + я: "ya", +}; function summaryInt(summary, key) { const value = summary?.[key]; @@ -51,6 +89,8 @@ createApp({ devices: [], sliders: {}, newGroup: { id: "", name: "", macs: [] }, + isSyncingNewGroupId: false, + isNewGroupIdEditedManually: false, isLoading: false, isLoadingStatus: false, isFetching: false, @@ -297,6 +337,44 @@ createApp({ } return room; }, + slugifyGroupId(input) { + const lower = String(input || "").trim().toLowerCase(); + let value = ""; + for (const char of lower) { + value += GROUP_ID_TRANSLITERATION_MAP[char] ?? char; + } + + value = value + .replace(/\s+/g, "-") + .replace(GROUP_ID_SANITIZE_PATTERN, "-") + .replace(GROUP_ID_DASH_COLLAPSE_PATTERN, "-") + .replace(GROUP_ID_EDGE_TRIM_PATTERN, ""); + + if (value.length > 32) { + value = value.slice(0, 32).replace(GROUP_ID_EDGE_TRIM_PATTERN, ""); + } + return value; + }, + syncNewGroupIdFromName() { + if (this.isNewGroupIdEditedManually) { + return; + } + const slug = this.slugifyGroupId(this.newGroup.name); + this.isSyncingNewGroupId = true; + this.newGroup.id = slug; + this.isSyncingNewGroupId = false; + }, + handleNewGroupNameInput() { + this.syncNewGroupIdFromName(); + }, + handleNewGroupIdInput() { + if (this.isSyncingNewGroupId) { + return; + } + const suggested = this.slugifyGroupId(this.newGroup.name); + const current = String(this.newGroup.id || "").trim(); + this.isNewGroupIdEditedManually = current.length > 0 && current !== suggested; + }, deviceLocationLabel(device) { const room = this.formatRoomName(device?.room); if (room !== "Без комнаты") { @@ -689,6 +767,8 @@ createApp({ this.toast(`Группа "${this.newGroup.name}" создана`, "success"); this.newGroup = { id: "", name: "", macs: [] }; + this.isSyncingNewGroupId = false; + this.isNewGroupIdEditedManually = false; await this.fetchData(); }, async deleteGroup(id) { diff --git a/static/index.html b/static/index.html index dffe252..f07bfe1 100644 --- a/static/index.html +++ b/static/index.html @@ -233,8 +233,8 @@
Новая группа

Собрать комнату из найденных ламп

- - + +
diff --git a/tests/test_p1_ui_security.py b/tests/test_p1_ui_security.py index f39677a..f5edf83 100644 --- a/tests/test_p1_ui_security.py +++ b/tests/test_p1_ui_security.py @@ -55,9 +55,15 @@ class UiSecurityTests(unittest.IsolatedAsyncioTestCase): self.assertIn("sessionStorage", app_js) self.assertIn("/system/info", app_js) self.assertIn("serverInfo", app_js) + self.assertIn("slugifyGroupId", app_js) + self.assertIn("GROUP_ID_TRANSLITERATION_MAP", app_js) self.assertIn("Комнаты, сцены и свет", index_html) self.assertIn("Устройства и группы", index_html) self.assertIn("Собрать комнату из найденных ламп", index_html) + self.assertLess( + index_html.index('placeholder="Название (Спальня)"'), + index_html.index('placeholder="ID (bedroom)"'), + ) self.assertIn("Повторяющееся расписание", index_html) self.assertIn("Гостевые и админ-ключи", index_html) self.assertIn("О сервере", index_html)