refactor(devices): close all MED/LOW review items

- Extract DEVICE_ALIAS_MAX_LENGTH to src/constants/devices.ts so the
  user page and admin page share one source of truth (mirroring the
  bot's ALIAS_MAX_LENGTH=64). Was duplicated as a magic 64 in both
  files (MED-5).
- Replace ✓ / ✕ / ✏️ glyphs in AdminUserDetail.tsx with inline SVGs,
  matching the icon style already used by Subscription.tsx for the
  same rename/save/cancel actions. Eliminates the platform-dependent
  emoji rendering inconsistency (MED-4).
- Add haptic feedback to the user-side renameDeviceMutation —
  notification('success') on save, notification('error') on failure.
  Brings it in line with the other Subscription.tsx mutations
  (LOW-9).
- Translate the new rename keys into zh.json and fa.json (en.json
  already covered in the previous follow-up). Cabinet now ships full
  parity across all 4 locales for the rename UI.
This commit is contained in:
Fringg
2026-05-16 04:07:25 +03:00
parent d1e5ce873b
commit e964780e6f
5 changed files with 88 additions and 10 deletions

8
src/constants/devices.ts Normal file
View File

@@ -0,0 +1,8 @@
/**
* Maximum length of a user-set local device alias.
*
* Mirrors `ALIAS_MAX_LENGTH` in the bot's `user_device_aliases` table.
* Keep this value in sync with `app/database/crud/user_device_alias.py`
* (Python side enforces it at the DB column width AND at the API layer).
*/
export const DEVICE_ALIAS_MAX_LENGTH = 64;

View File

@@ -454,6 +454,11 @@
"month": "ماه",
"myDevices": "دستگاه‌های من",
"noDevices": "هیچ دستگاه متصلی نیست",
"renameDevice": "تغییر نام",
"renameDeviceSave": "ذخیره",
"renameDeviceCancel": "لغو",
"renameDevicePlaceholder": "نام دستگاه",
"deviceRenamed": "نام دستگاه به‌روزرسانی شد",
"perExtraDevice": "/ دستگاه اضافی",
"perMonth": "/ماه",
"purchasedTraffic": "ترافیک خریداری شده",
@@ -2667,7 +2672,10 @@
"none": "دستگاه متصلی وجود ندارد",
"resetAll": "بازنشانی همه",
"deleted": "دستگاه حذف شد",
"allDeleted": "همه دستگاه‌ها بازنشانی شدند"
"allDeleted": "همه دستگاه‌ها بازنشانی شدند",
"rename": "تغییر نام",
"renameSave": "ذخیره",
"renamed": "نام دستگاه به‌روزرسانی شد"
},
"referral": {
"title": "برنامه ارجاع",

View File

@@ -454,6 +454,11 @@
"month": "月",
"myDevices": "我的设备",
"noDevices": "没有已连接的设备",
"renameDevice": "重命名",
"renameDeviceSave": "保存",
"renameDeviceCancel": "取消",
"renameDevicePlaceholder": "设备名称",
"deviceRenamed": "设备名称已更新",
"perExtraDevice": "/ 额外设备",
"perMonth": "/月",
"purchasedTraffic": "已购流量",
@@ -2666,7 +2671,10 @@
"none": "没有已连接的设备",
"resetAll": "重置全部",
"deleted": "设备已删除",
"allDeleted": "所有设备已重置"
"allDeleted": "所有设备已重置",
"rename": "重命名",
"renameSave": "保存",
"renamed": "设备名称已更新"
},
"referral": {
"title": "推荐计划",

View File

@@ -2,6 +2,7 @@ import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
import { useParams, useNavigate } from 'react-router';
import { useTranslation } from 'react-i18next';
import i18n from '../i18n';
import { DEVICE_ALIAS_MAX_LENGTH } from '../constants/devices';
import { useCurrency } from '../hooks/useCurrency';
import { useNotify } from '../platform/hooks/useNotify';
import {
@@ -2479,7 +2480,7 @@ export default function AdminUserDetail() {
type="text"
autoFocus
value={editingDeviceName}
maxLength={64}
maxLength={DEVICE_ALIAS_MAX_LENGTH}
placeholder={
device.platform ||
device.device_model ||
@@ -2522,7 +2523,7 @@ export default function AdminUserDetail() {
type="button"
onClick={() => handleRenameDevice(device.hwid)}
disabled={renameSaving}
className="rounded-lg px-2 py-1 text-xs text-success-400 transition-all hover:bg-success-500/15 disabled:opacity-50"
className="rounded-lg px-2 py-1 text-success-400 transition-all hover:bg-success-500/15 disabled:opacity-50"
title={t(
'admin.users.detail.devices.renameSave',
t(
@@ -2530,8 +2531,24 @@ export default function AdminUserDetail() {
'\u0421\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u044C',
),
)}
aria-label={t(
'admin.users.detail.devices.renameSave',
'\u0421\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u044C',
)}
>
{'\u2713'}
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="M5 13l4 4L19 7" />
</svg>
</button>
<button
type="button"
@@ -2540,13 +2557,29 @@ export default function AdminUserDetail() {
setEditingDeviceName('');
}}
disabled={renameSaving}
className="rounded-lg px-2 py-1 text-xs text-dark-500 transition-all hover:bg-dark-700 disabled:opacity-50"
className="rounded-lg px-2 py-1 text-dark-500 transition-all hover:bg-dark-700 disabled:opacity-50"
title={t(
'common.cancel',
'\u041E\u0442\u043C\u0435\u043D\u0430',
)}
aria-label={t(
'common.cancel',
'\u041E\u0442\u043C\u0435\u043D\u0430',
)}
>
{'\u2715'}
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</>
) : (
@@ -2557,13 +2590,29 @@ export default function AdminUserDetail() {
setEditingDeviceHwid(device.hwid);
setEditingDeviceName(device.local_name || '');
}}
className="rounded-lg px-2 py-1 text-xs text-dark-500 transition-all hover:bg-accent-500/15 hover:text-accent-400"
className="rounded-lg px-2 py-1 text-dark-500 transition-all hover:bg-accent-500/15 hover:text-accent-400"
title={t(
'admin.users.detail.devices.rename',
'\u041F\u0435\u0440\u0435\u0438\u043C\u0435\u043D\u043E\u0432\u0430\u0442\u044C',
)}
aria-label={t(
'admin.users.detail.devices.rename',
'\u041F\u0435\u0440\u0435\u0438\u043C\u0435\u043D\u043E\u0432\u0430\u0442\u044C',
)}
>
{'\u270F\uFE0F'}
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125" />
</svg>
</button>
<button
onClick={() =>

View File

@@ -3,6 +3,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { Navigate, useNavigate, useParams } from 'react-router';
import { subscriptionApi } from '../api/subscription';
import { DEVICE_ALIAS_MAX_LENGTH } from '../constants/devices';
import { WebBackButton } from '../components/WebBackButton';
import { useDestructiveConfirm } from '../platform/hooks/useNativeDialog';
import { usePlatform } from '../platform';
@@ -313,19 +314,23 @@ export default function Subscription() {
// identifier of the row being edited.
const [editingDeviceHwid, setEditingDeviceHwid] = useState<string | null>(null);
const [editingDeviceName, setEditingDeviceName] = useState('');
const DEVICE_ALIAS_MAX_LENGTH = 64;
const renameDeviceMutation = useMutation({
mutationFn: ({ hwid, name }: { hwid: string; name: string | null }) =>
subscriptionApi.renameDevice(hwid, name, subscriptionId),
onSuccess: (_data, variables) => {
queryClient.invalidateQueries({ queryKey: ['devices', subscriptionId] });
// Soft success-tap, like other mutations on this page.
haptic.notification('success');
// Не сбрасываем edit-state, если пользователь уже перешёл на другой
// девайс пока шёл запрос — иначе теряем его новый input. Имя не чистим
// безусловно: оно либо принадлежит уже другому девайсу (нужно сохранить),
// либо инпут уже закрылся (значение не отображается).
setEditingDeviceHwid((current) => (current === variables.hwid ? null : current));
},
onError: () => {
haptic.notification('error');
},
});
// Pause subscription mutation