Auto-fill group IDs from names in web UI

This commit is contained in:
Artem Kokos
2026-05-21 21:54:28 +07:00
parent f55e00bce1
commit 928e4c71b7
3 changed files with 88 additions and 2 deletions

View File

@@ -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) {

View File

@@ -233,8 +233,8 @@
<div class="section-kicker">Новая группа</div>
<h2 class="text-xl font-black tracking-tight mb-5">Собрать комнату из найденных ламп</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-3 mb-5">
<input v-model="newGroup.id" placeholder="ID (bedroom)" class="bg-black/30 border border-slate-700/50 p-3 rounded-xl focus:border-orange-500 outline-none text-sm mono">
<input v-model="newGroup.name" placeholder="Название (Спальня)" class="bg-black/30 border border-slate-700/50 p-3 rounded-xl focus:border-orange-500 outline-none text-sm">
<input v-model="newGroup.name" @input="handleNewGroupNameInput" placeholder="Название (Спальня)" class="bg-black/30 border border-slate-700/50 p-3 rounded-xl focus:border-orange-500 outline-none text-sm">
<input v-model="newGroup.id" @input="handleNewGroupIdInput" placeholder="ID (bedroom)" class="bg-black/30 border border-slate-700/50 p-3 rounded-xl focus:border-orange-500 outline-none text-sm mono">
<button @click="createGroup" :disabled="!newGroup.id || !newGroup.name || !newGroup.macs.length" class="accent-button disabled:opacity-30 disabled:cursor-not-allowed">СОЗДАТЬ ГРУППУ</button>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">

View File

@@ -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)