mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-30 02:23:47 +00:00
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:
8
src/constants/devices.ts
Normal file
8
src/constants/devices.ts
Normal 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;
|
||||||
@@ -454,6 +454,11 @@
|
|||||||
"month": "ماه",
|
"month": "ماه",
|
||||||
"myDevices": "دستگاههای من",
|
"myDevices": "دستگاههای من",
|
||||||
"noDevices": "هیچ دستگاه متصلی نیست",
|
"noDevices": "هیچ دستگاه متصلی نیست",
|
||||||
|
"renameDevice": "تغییر نام",
|
||||||
|
"renameDeviceSave": "ذخیره",
|
||||||
|
"renameDeviceCancel": "لغو",
|
||||||
|
"renameDevicePlaceholder": "نام دستگاه",
|
||||||
|
"deviceRenamed": "نام دستگاه بهروزرسانی شد",
|
||||||
"perExtraDevice": "/ دستگاه اضافی",
|
"perExtraDevice": "/ دستگاه اضافی",
|
||||||
"perMonth": "/ماه",
|
"perMonth": "/ماه",
|
||||||
"purchasedTraffic": "ترافیک خریداری شده",
|
"purchasedTraffic": "ترافیک خریداری شده",
|
||||||
@@ -2667,7 +2672,10 @@
|
|||||||
"none": "دستگاه متصلی وجود ندارد",
|
"none": "دستگاه متصلی وجود ندارد",
|
||||||
"resetAll": "بازنشانی همه",
|
"resetAll": "بازنشانی همه",
|
||||||
"deleted": "دستگاه حذف شد",
|
"deleted": "دستگاه حذف شد",
|
||||||
"allDeleted": "همه دستگاهها بازنشانی شدند"
|
"allDeleted": "همه دستگاهها بازنشانی شدند",
|
||||||
|
"rename": "تغییر نام",
|
||||||
|
"renameSave": "ذخیره",
|
||||||
|
"renamed": "نام دستگاه بهروزرسانی شد"
|
||||||
},
|
},
|
||||||
"referral": {
|
"referral": {
|
||||||
"title": "برنامه ارجاع",
|
"title": "برنامه ارجاع",
|
||||||
|
|||||||
@@ -454,6 +454,11 @@
|
|||||||
"month": "月",
|
"month": "月",
|
||||||
"myDevices": "我的设备",
|
"myDevices": "我的设备",
|
||||||
"noDevices": "没有已连接的设备",
|
"noDevices": "没有已连接的设备",
|
||||||
|
"renameDevice": "重命名",
|
||||||
|
"renameDeviceSave": "保存",
|
||||||
|
"renameDeviceCancel": "取消",
|
||||||
|
"renameDevicePlaceholder": "设备名称",
|
||||||
|
"deviceRenamed": "设备名称已更新",
|
||||||
"perExtraDevice": "/ 额外设备",
|
"perExtraDevice": "/ 额外设备",
|
||||||
"perMonth": "/月",
|
"perMonth": "/月",
|
||||||
"purchasedTraffic": "已购流量",
|
"purchasedTraffic": "已购流量",
|
||||||
@@ -2666,7 +2671,10 @@
|
|||||||
"none": "没有已连接的设备",
|
"none": "没有已连接的设备",
|
||||||
"resetAll": "重置全部",
|
"resetAll": "重置全部",
|
||||||
"deleted": "设备已删除",
|
"deleted": "设备已删除",
|
||||||
"allDeleted": "所有设备已重置"
|
"allDeleted": "所有设备已重置",
|
||||||
|
"rename": "重命名",
|
||||||
|
"renameSave": "保存",
|
||||||
|
"renamed": "设备名称已更新"
|
||||||
},
|
},
|
||||||
"referral": {
|
"referral": {
|
||||||
"title": "推荐计划",
|
"title": "推荐计划",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
|
|||||||
import { useParams, useNavigate } from 'react-router';
|
import { useParams, useNavigate } from 'react-router';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import i18n from '../i18n';
|
import i18n from '../i18n';
|
||||||
|
import { DEVICE_ALIAS_MAX_LENGTH } from '../constants/devices';
|
||||||
import { useCurrency } from '../hooks/useCurrency';
|
import { useCurrency } from '../hooks/useCurrency';
|
||||||
import { useNotify } from '../platform/hooks/useNotify';
|
import { useNotify } from '../platform/hooks/useNotify';
|
||||||
import {
|
import {
|
||||||
@@ -2479,7 +2480,7 @@ export default function AdminUserDetail() {
|
|||||||
type="text"
|
type="text"
|
||||||
autoFocus
|
autoFocus
|
||||||
value={editingDeviceName}
|
value={editingDeviceName}
|
||||||
maxLength={64}
|
maxLength={DEVICE_ALIAS_MAX_LENGTH}
|
||||||
placeholder={
|
placeholder={
|
||||||
device.platform ||
|
device.platform ||
|
||||||
device.device_model ||
|
device.device_model ||
|
||||||
@@ -2522,7 +2523,7 @@ export default function AdminUserDetail() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleRenameDevice(device.hwid)}
|
onClick={() => handleRenameDevice(device.hwid)}
|
||||||
disabled={renameSaving}
|
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(
|
title={t(
|
||||||
'admin.users.detail.devices.renameSave',
|
'admin.users.detail.devices.renameSave',
|
||||||
t(
|
t(
|
||||||
@@ -2530,8 +2531,24 @@ export default function AdminUserDetail() {
|
|||||||
'\u0421\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u044C',
|
'\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>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -2540,13 +2557,29 @@ export default function AdminUserDetail() {
|
|||||||
setEditingDeviceName('');
|
setEditingDeviceName('');
|
||||||
}}
|
}}
|
||||||
disabled={renameSaving}
|
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(
|
title={t(
|
||||||
'common.cancel',
|
'common.cancel',
|
||||||
'\u041E\u0442\u043C\u0435\u043D\u0430',
|
'\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>
|
</button>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
@@ -2557,13 +2590,29 @@ export default function AdminUserDetail() {
|
|||||||
setEditingDeviceHwid(device.hwid);
|
setEditingDeviceHwid(device.hwid);
|
||||||
setEditingDeviceName(device.local_name || '');
|
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(
|
title={t(
|
||||||
'admin.users.detail.devices.rename',
|
'admin.users.detail.devices.rename',
|
||||||
'\u041F\u0435\u0440\u0435\u0438\u043C\u0435\u043D\u043E\u0432\u0430\u0442\u044C',
|
'\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>
|
||||||
<button
|
<button
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Navigate, useNavigate, useParams } from 'react-router';
|
import { Navigate, useNavigate, useParams } from 'react-router';
|
||||||
import { subscriptionApi } from '../api/subscription';
|
import { subscriptionApi } from '../api/subscription';
|
||||||
|
import { DEVICE_ALIAS_MAX_LENGTH } from '../constants/devices';
|
||||||
import { WebBackButton } from '../components/WebBackButton';
|
import { WebBackButton } from '../components/WebBackButton';
|
||||||
import { useDestructiveConfirm } from '../platform/hooks/useNativeDialog';
|
import { useDestructiveConfirm } from '../platform/hooks/useNativeDialog';
|
||||||
import { usePlatform } from '../platform';
|
import { usePlatform } from '../platform';
|
||||||
@@ -313,19 +314,23 @@ export default function Subscription() {
|
|||||||
// identifier of the row being edited.
|
// identifier of the row being edited.
|
||||||
const [editingDeviceHwid, setEditingDeviceHwid] = useState<string | null>(null);
|
const [editingDeviceHwid, setEditingDeviceHwid] = useState<string | null>(null);
|
||||||
const [editingDeviceName, setEditingDeviceName] = useState('');
|
const [editingDeviceName, setEditingDeviceName] = useState('');
|
||||||
const DEVICE_ALIAS_MAX_LENGTH = 64;
|
|
||||||
|
|
||||||
const renameDeviceMutation = useMutation({
|
const renameDeviceMutation = useMutation({
|
||||||
mutationFn: ({ hwid, name }: { hwid: string; name: string | null }) =>
|
mutationFn: ({ hwid, name }: { hwid: string; name: string | null }) =>
|
||||||
subscriptionApi.renameDevice(hwid, name, subscriptionId),
|
subscriptionApi.renameDevice(hwid, name, subscriptionId),
|
||||||
onSuccess: (_data, variables) => {
|
onSuccess: (_data, variables) => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['devices', subscriptionId] });
|
queryClient.invalidateQueries({ queryKey: ['devices', subscriptionId] });
|
||||||
|
// Soft success-tap, like other mutations on this page.
|
||||||
|
haptic.notification('success');
|
||||||
// Не сбрасываем edit-state, если пользователь уже перешёл на другой
|
// Не сбрасываем edit-state, если пользователь уже перешёл на другой
|
||||||
// девайс пока шёл запрос — иначе теряем его новый input. Имя не чистим
|
// девайс пока шёл запрос — иначе теряем его новый input. Имя не чистим
|
||||||
// безусловно: оно либо принадлежит уже другому девайсу (нужно сохранить),
|
// безусловно: оно либо принадлежит уже другому девайсу (нужно сохранить),
|
||||||
// либо инпут уже закрылся (значение не отображается).
|
// либо инпут уже закрылся (значение не отображается).
|
||||||
setEditingDeviceHwid((current) => (current === variables.hwid ? null : current));
|
setEditingDeviceHwid((current) => (current === variables.hwid ? null : current));
|
||||||
},
|
},
|
||||||
|
onError: () => {
|
||||||
|
haptic.notification('error');
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Pause subscription mutation
|
// Pause subscription mutation
|
||||||
|
|||||||
Reference in New Issue
Block a user