fix(devices): unicode escape bug + onSuccess race + en locale + apiErr surface

Code-review follow-up on 321c65b.

- prettier ate the raw glyphs (✓/✕/✏️) in AdminUserDetail.tsx and emitted
  \u2713 / \u2715 / \u270F\uFE0F as JSX text children. JSX does NOT
  process \u… escapes inside element children, so admin users saw the
  literal six-character strings on Save/Cancel/Rename buttons. Wrap each
  glyph in a JS expression {'…'} so it survives both prettier and JSX.
- renameDeviceMutation.onSuccess on Subscription.tsx no longer
  unconditionally clears the edit state. If the user already navigated
  to a different device while the previous save was in flight, we keep
  their in-progress input. Same shape applied to AdminUserDetail.tsx —
  the alias is snapshot before the await so a race can't smuggle the
  wrong value into the request, and the post-success reset is guarded.
- AdminUserDetail rename error path now surfaces the backend's
  response.data.detail (e.g. '64-char limit' message) instead of the
  generic localized userActions.error toast.
- Add the new rename i18n keys to src/locales/en.json so English users
  no longer fall back to Russian labels. ua/zh/fa to follow in a
  dedicated translation pass.
This commit is contained in:
Fringg
2026-05-16 03:53:49 +03:00
parent 321c65b68b
commit d1e5ce873b
3 changed files with 28 additions and 13 deletions

View File

@@ -468,6 +468,11 @@
"confirmDeleteAllDevices": "Delete all devices?", "confirmDeleteAllDevices": "Delete all devices?",
"deviceDeleted": "Device deleted", "deviceDeleted": "Device deleted",
"allDevicesDeleted": "All devices deleted", "allDevicesDeleted": "All devices deleted",
"renameDevice": "Rename",
"renameDeviceSave": "Save",
"renameDeviceCancel": "Cancel",
"renameDevicePlaceholder": "Device name",
"deviceRenamed": "Device name updated",
"platform": "Platform", "platform": "Platform",
"model": "Model", "model": "Model",
"connectedAt": "Connected", "connectedAt": "Connected",
@@ -3176,7 +3181,10 @@
"none": "No connected devices", "none": "No connected devices",
"resetAll": "Reset all", "resetAll": "Reset all",
"deleted": "Device removed", "deleted": "Device removed",
"allDeleted": "All devices reset" "allDeleted": "All devices reset",
"rename": "Rename",
"renameSave": "Save",
"renamed": "Device name updated"
}, },
"referral": { "referral": {
"title": "Referral program", "title": "Referral program",

View File

@@ -774,15 +774,19 @@ export default function AdminUserDetail() {
const handleRenameDevice = async (hwid: string) => { const handleRenameDevice = async (hwid: string) => {
if (!userId) return; if (!userId) return;
setRenameSaving(true); setRenameSaving(true);
// Snapshot inputs BEFORE the await so a fast click on another device
// mid-flight doesn't smuggle a different alias into this hwid's request.
const snapshotName = editingDeviceName.trim();
try { try {
const trimmed = editingDeviceName.trim(); await adminUsersApi.renameUserDevice(userId, hwid, snapshotName || null);
await adminUsersApi.renameUserDevice(userId, hwid, trimmed || null);
notify.success(t('admin.users.detail.devices.renamed', 'Имя устройства обновлено')); notify.success(t('admin.users.detail.devices.renamed', 'Имя устройства обновлено'));
setEditingDeviceHwid(null); // Reset edit state only if user is still on the saved row.
setEditingDeviceName(''); setEditingDeviceHwid((current) => (current === hwid ? null : current));
await loadDevices(); await loadDevices();
} catch { } catch (err) {
notify.error(t('admin.users.userActions.error'), t('common.error')); const apiMessage = (err as { response?: { data?: { detail?: string } } })?.response?.data
?.detail;
notify.error(apiMessage || t('admin.users.userActions.error'), t('common.error'));
} finally { } finally {
setRenameSaving(false); setRenameSaving(false);
} }
@@ -2527,7 +2531,7 @@ export default function AdminUserDetail() {
), ),
)} )}
> >
\u2713 {'\u2713'}
</button> </button>
<button <button
type="button" type="button"
@@ -2542,7 +2546,7 @@ export default function AdminUserDetail() {
'\u041E\u0442\u043C\u0435\u043D\u0430', '\u041E\u0442\u043C\u0435\u043D\u0430',
)} )}
> >
\u2715 {'\u2715'}
</button> </button>
</> </>
) : ( ) : (
@@ -2559,7 +2563,7 @@ export default function AdminUserDetail() {
'\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',
)} )}
> >
\u270F\uFE0F {'\u270F\uFE0F'}
</button> </button>
<button <button
onClick={() => onClick={() =>

View File

@@ -318,10 +318,13 @@ export default function Subscription() {
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: () => { onSuccess: (_data, variables) => {
queryClient.invalidateQueries({ queryKey: ['devices', subscriptionId] }); queryClient.invalidateQueries({ queryKey: ['devices', subscriptionId] });
setEditingDeviceHwid(null); // Не сбрасываем edit-state, если пользователь уже перешёл на другой
setEditingDeviceName(''); // девайс пока шёл запрос — иначе теряем его новый input. Имя не чистим
// безусловно: оно либо принадлежит уже другому девайсу (нужно сохранить),
// либо инпут уже закрылся (значение не отображается).
setEditingDeviceHwid((current) => (current === variables.hwid ? null : current));
}, },
}); });