fix(ui): QA report P3/P4 batch — 7 cosmetic/UX bugs
QA report cleanup (после P1/P2 ship'нутых в v2.31.4): - **BUG-009**: Excel/SQL export форматы получили явный «скоро» badge поверх opacity-only disabled state. Title attribute оставлен. - **BUG-010**: «Доступ актора» (audit scope column) переименован в «Scope» — компактнее, не переносится на 2 строки. - **BUG-011**: Все audit table headers получили title attribute → hover tooltip когда text truncate'ится («ИЗМЕНЕН…» → full «Изменения»). - **UX-001**: Scope chips в /dictionaries теперь показывают ✓ glyph когда selected → визуально clear что multi-select. Aria-label расширен: «Фильтр по области (можно выбрать несколько)». - **UX-002**: Breadcrumb segments получили title attribute → hover показывает full label когда truncate'ится в narrow viewport. - **UX-003**: При смене вкладки в редакторе справочника (records → relations и т.п.) URL params q/scopes сбрасываются — раньше search фильтр оставался применённым после возврата на records. - **UX-004**: TimeTravel slider boundary метки получили 2-digit year: «05.05.26» вместо «05.05» — disambiguates конец декабря / начало января. Title attribute exposes full локальную дату. - **UX-009**: «Удалить» button в RecordDrawer переименован в «Закрыть запись» — same action как row context menu «Закрыть» (bitemporal close, не truly delete). Consistent labeling across UI.
This commit is contained in:
@@ -208,15 +208,21 @@ export function ExportModal({
|
|||||||
disabled={f.disabled}
|
disabled={f.disabled}
|
||||||
title={f.reason}
|
title={f.reason}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex flex-col items-center justify-center gap-0.5 h-[68px] rounded-md border transition-colors',
|
'relative flex flex-col items-center justify-center gap-0.5 h-[68px] rounded-md border transition-colors',
|
||||||
format === f.id
|
format === f.id
|
||||||
? 'border-ink bg-surface text-ink ring-1 ring-ink'
|
? 'border-ink bg-surface text-ink ring-1 ring-ink'
|
||||||
: 'border-line bg-surface text-ink-2 hover:border-line-2 hover:bg-surface-2',
|
: 'border-line bg-surface text-ink-2 hover:border-line-2 hover:bg-surface-2',
|
||||||
f.disabled && 'opacity-40 cursor-not-allowed hover:bg-surface hover:border-line',
|
f.disabled && 'opacity-60 cursor-not-allowed hover:bg-surface hover:border-line',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span className="text-title-md font-semibold leading-none">{f.label}</span>
|
<span className="text-title-md font-semibold leading-none">{f.label}</span>
|
||||||
<span className="text-mono text-mute leading-none">{f.ext}</span>
|
<span className="text-mono text-mute leading-none">{f.ext}</span>
|
||||||
|
{/* QA BUG-009: явный «скоро» badge вместо опоры только на opacity. */}
|
||||||
|
{f.disabled && (
|
||||||
|
<span className="absolute top-1 right-1 text-[9px] font-semibold text-mute bg-surface-2 px-1 rounded-sm uppercase tracking-wider">
|
||||||
|
{t('export.format.soon', { defaultValue: 'скоро' })}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -48,9 +48,11 @@ export function TopBar({ onMenuClick }: { onMenuClick?: () => void }) {
|
|||||||
return (
|
return (
|
||||||
<span key={i} className="flex items-center gap-2 min-w-0">
|
<span key={i} className="flex items-center gap-2 min-w-0">
|
||||||
{i > 0 && <span className="text-mute text-title-md font-light">/</span>}
|
{i > 0 && <span className="text-mute text-title-md font-light">/</span>}
|
||||||
|
{/* QA UX-002: title attribute → hover tooltip когда truncate. */}
|
||||||
{crumb.to && !isLast ? (
|
{crumb.to && !isLast ? (
|
||||||
<Link
|
<Link
|
||||||
to={crumb.to}
|
to={crumb.to}
|
||||||
|
title={typeof crumb.label === 'string' ? crumb.label : undefined}
|
||||||
className={
|
className={
|
||||||
isFirstOfTwoPlus
|
isFirstOfTwoPlus
|
||||||
? 'text-cell text-mute hover:text-ink truncate transition-colors inline-flex items-center gap-1'
|
? 'text-cell text-mute hover:text-ink truncate transition-colors inline-flex items-center gap-1'
|
||||||
@@ -61,7 +63,10 @@ export function TopBar({ onMenuClick }: { onMenuClick?: () => void }) {
|
|||||||
{crumb.label}
|
{crumb.label}
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
<span className={isLast ? 'text-ink font-semibold truncate' : 'text-ink-2 truncate'}>
|
<span
|
||||||
|
title={typeof crumb.label === 'string' ? crumb.label : undefined}
|
||||||
|
className={isLast ? 'text-ink font-semibold truncate' : 'text-ink-2 truncate'}
|
||||||
|
>
|
||||||
{crumb.label}
|
{crumb.label}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -571,9 +571,15 @@ function TimelineSlider({
|
|||||||
* чтобы две DD.MM метки ~ 36px @ text-cap не пересекались на типичной
|
* чтобы две DD.MM метки ~ 36px @ text-cap не пересекались на типичной
|
||||||
* ширине трека ~340px. */}
|
* ширине трека ~340px. */}
|
||||||
<div className="relative h-4 mt-1 text-mono text-cap text-mute">
|
<div className="relative h-4 mt-1 text-mono text-cap text-mute">
|
||||||
|
{/* QA UX-004: добавлен 2-digit year к boundary меткам — раньше «05.05»
|
||||||
|
* без года был неоднозначным на конец декабря / начало января. */}
|
||||||
{pct > 12 && (
|
{pct > 12 && (
|
||||||
<span className="absolute left-0">
|
<span
|
||||||
|
className="absolute left-0"
|
||||||
|
title={new Date(range.min).toLocaleDateString()}
|
||||||
|
>
|
||||||
{new Date(range.min).toLocaleDateString(undefined, {
|
{new Date(range.min).toLocaleDateString(undefined, {
|
||||||
|
year: '2-digit',
|
||||||
month: '2-digit',
|
month: '2-digit',
|
||||||
day: '2-digit',
|
day: '2-digit',
|
||||||
})}
|
})}
|
||||||
@@ -586,8 +592,12 @@ function TimelineSlider({
|
|||||||
{dateLabel}
|
{dateLabel}
|
||||||
</span>
|
</span>
|
||||||
{pct < 88 && (
|
{pct < 88 && (
|
||||||
<span className="absolute right-0">
|
<span
|
||||||
|
className="absolute right-0"
|
||||||
|
title={new Date(range.max).toLocaleDateString()}
|
||||||
|
>
|
||||||
{new Date(range.max).toLocaleDateString(undefined, {
|
{new Date(range.max).toLocaleDateString(undefined, {
|
||||||
|
year: '2-digit',
|
||||||
month: '2-digit',
|
month: '2-digit',
|
||||||
day: '2-digit',
|
day: '2-digit',
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -278,7 +278,7 @@ i18n
|
|||||||
// User confusion: 'правки в public были, а тут RESTRICTED' —
|
// User confusion: 'правки в public были, а тут RESTRICTED' —
|
||||||
// потому что у пользователя scope=RESTRICTED привилегия. Label
|
// потому что у пользователя scope=RESTRICTED привилегия. Label
|
||||||
// 'Доступ актора' разъясняет это.
|
// 'Доступ актора' разъясняет это.
|
||||||
'audit.col.scope': 'Доступ актора',
|
'audit.col.scope': 'Scope',
|
||||||
'audit.col.trace': 'Trace',
|
'audit.col.trace': 'Trace',
|
||||||
'audit.col.diff': 'Изменения',
|
'audit.col.diff': 'Изменения',
|
||||||
'audit.action.expand': 'Раскрыть',
|
'audit.action.expand': 'Раскрыть',
|
||||||
@@ -334,7 +334,7 @@ i18n
|
|||||||
'dict.list.deps.short': 'СО СВЯЗЯМИ',
|
'dict.list.deps.short': 'СО СВЯЗЯМИ',
|
||||||
// Catalog v2 (URL-driven filters + sort + density)
|
// Catalog v2 (URL-driven filters + sort + density)
|
||||||
'dict.list.heading': 'Каталог справочников',
|
'dict.list.heading': 'Каталог справочников',
|
||||||
'dict.list.filter.scope': 'Фильтр по области',
|
'dict.list.filter.scope': 'Фильтр по области (можно выбрать несколько)',
|
||||||
'dict.list.sort.label': 'Сортировка',
|
'dict.list.sort.label': 'Сортировка',
|
||||||
'dict.list.sort.name': 'По названию',
|
'dict.list.sort.name': 'По названию',
|
||||||
'dict.list.sort.recordCount': 'По количеству записей',
|
'dict.list.sort.recordCount': 'По количеству записей',
|
||||||
@@ -424,7 +424,7 @@ i18n
|
|||||||
'form.cancel': 'Отмена',
|
'form.cancel': 'Отмена',
|
||||||
'form.save': 'Сохранить',
|
'form.save': 'Сохранить',
|
||||||
'form.saving': 'Сохранение...',
|
'form.saving': 'Сохранение...',
|
||||||
'form.delete': 'Удалить',
|
'form.delete': 'Закрыть запись',
|
||||||
'form.error.required': 'Обязательное поле',
|
'form.error.required': 'Обязательное поле',
|
||||||
'form.unsavedNFields_one': 'не сохранено · {{count}} поле',
|
'form.unsavedNFields_one': 'не сохранено · {{count}} поле',
|
||||||
'form.unsavedNFields_few': 'не сохранено · {{count}} поля',
|
'form.unsavedNFields_few': 'не сохранено · {{count}} поля',
|
||||||
@@ -1103,7 +1103,7 @@ i18n
|
|||||||
'audit.col.dictionary': 'Dictionary',
|
'audit.col.dictionary': 'Dictionary',
|
||||||
'audit.col.businessKey': 'Business key',
|
'audit.col.businessKey': 'Business key',
|
||||||
'audit.col.user': 'User',
|
'audit.col.user': 'User',
|
||||||
'audit.col.scope': 'Actor scope',
|
'audit.col.scope': 'Scope',
|
||||||
'audit.col.trace': 'Trace',
|
'audit.col.trace': 'Trace',
|
||||||
'audit.col.diff': 'Diff',
|
'audit.col.diff': 'Diff',
|
||||||
'audit.action.expand': 'Expand',
|
'audit.action.expand': 'Expand',
|
||||||
@@ -1157,7 +1157,7 @@ i18n
|
|||||||
'dict.list.deps.short': 'WITH LINKS',
|
'dict.list.deps.short': 'WITH LINKS',
|
||||||
// Catalog v2
|
// Catalog v2
|
||||||
'dict.list.heading': 'Dictionary catalog',
|
'dict.list.heading': 'Dictionary catalog',
|
||||||
'dict.list.filter.scope': 'Scope filter',
|
'dict.list.filter.scope': 'Scope filter (multi-select)',
|
||||||
'dict.list.sort.label': 'Sort',
|
'dict.list.sort.label': 'Sort',
|
||||||
'dict.list.sort.name': 'By name',
|
'dict.list.sort.name': 'By name',
|
||||||
'dict.list.sort.recordCount': 'By record count',
|
'dict.list.sort.recordCount': 'By record count',
|
||||||
@@ -1239,7 +1239,7 @@ i18n
|
|||||||
'form.cancel': 'Cancel',
|
'form.cancel': 'Cancel',
|
||||||
'form.save': 'Save',
|
'form.save': 'Save',
|
||||||
'form.saving': 'Saving...',
|
'form.saving': 'Saving...',
|
||||||
'form.delete': 'Delete',
|
'form.delete': 'Close record',
|
||||||
'form.unsavedNFields_one': 'unsaved · {{count}} field',
|
'form.unsavedNFields_one': 'unsaved · {{count}} field',
|
||||||
'form.unsavedNFields_other': 'unsaved · {{count}} fields',
|
'form.unsavedNFields_other': 'unsaved · {{count}} fields',
|
||||||
'drawer.collapse': 'Collapse to 520px',
|
'drawer.collapse': 'Collapse to 520px',
|
||||||
|
|||||||
@@ -441,14 +441,15 @@ function AuditTable({ rows }: { rows: AuditEntry[] }) {
|
|||||||
<Table>
|
<Table>
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHeaderCell>{t('audit.col.time')}</TableHeaderCell>
|
{/* QA BUG-011 fix: title attribute → hover tooltip когда text truncate'ится. */}
|
||||||
<TableHeaderCell>{t('audit.col.action')}</TableHeaderCell>
|
<TableHeaderCell title={t('audit.col.time')}>{t('audit.col.time')}</TableHeaderCell>
|
||||||
<TableHeaderCell>{t('audit.col.dictionary')}</TableHeaderCell>
|
<TableHeaderCell title={t('audit.col.action')}>{t('audit.col.action')}</TableHeaderCell>
|
||||||
<TableHeaderCell>{t('audit.col.businessKey')}</TableHeaderCell>
|
<TableHeaderCell title={t('audit.col.dictionary')}>{t('audit.col.dictionary')}</TableHeaderCell>
|
||||||
<TableHeaderCell>{t('audit.col.user')}</TableHeaderCell>
|
<TableHeaderCell title={t('audit.col.businessKey')}>{t('audit.col.businessKey')}</TableHeaderCell>
|
||||||
<TableHeaderCell>{t('audit.col.scope')}</TableHeaderCell>
|
<TableHeaderCell title={t('audit.col.user')}>{t('audit.col.user')}</TableHeaderCell>
|
||||||
<TableHeaderCell>{t('audit.col.trace')}</TableHeaderCell>
|
<TableHeaderCell title={t('audit.col.scope')}>{t('audit.col.scope')}</TableHeaderCell>
|
||||||
<TableHeaderCell>{t('audit.col.diff')}</TableHeaderCell>
|
<TableHeaderCell title={t('audit.col.trace')}>{t('audit.col.trace')}</TableHeaderCell>
|
||||||
|
<TableHeaderCell title={t('audit.col.diff')}>{t('audit.col.diff')}</TableHeaderCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
|
|||||||
@@ -839,6 +839,11 @@ function DictionaryDetail() {
|
|||||||
search: (prev) => ({
|
search: (prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
tab: tab === 'records' ? undefined : tab,
|
tab: tab === 'records' ? undefined : tab,
|
||||||
|
// QA UX-003: сбросить records-tab фильтры (search/q + column filters
|
||||||
|
// + scopes) когда пользователь перешёл на другую вкладку. Иначе
|
||||||
|
// search:'абв' оставался применённым после возврата на records.
|
||||||
|
q: undefined,
|
||||||
|
scopes: undefined,
|
||||||
}),
|
}),
|
||||||
replace: true,
|
replace: true,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -281,12 +281,15 @@ function DictionariesPage() {
|
|||||||
// Scope chips всегда покрашены в свой scope-color (filled bg
|
// Scope chips всегда покрашены в свой scope-color (filled bg
|
||||||
// + colored text) per user feedback ("плашки то покрась").
|
// + colored text) per user feedback ("плашки то покрась").
|
||||||
// Selected state — ring-1 ring-{scope} + slightly opaque bg.
|
// Selected state — ring-1 ring-{scope} + slightly opaque bg.
|
||||||
className={`h-8 text-cap tracking-[0.14em] uppercase font-semibold px-2.5 rounded-md border inline-flex items-center transition-all focus:outline-none focus:ring-2 focus:ring-accent/40 ${SCOPE_BG_TINT[scope]} ${SCOPE_TEXT[scope]} ${
|
// QA UX-001: ✓ glyph когда selected — clarifies что это
|
||||||
|
// multi-select (checkbox-like), не radio.
|
||||||
|
className={`h-8 text-cap tracking-[0.14em] uppercase font-semibold px-2.5 rounded-md border inline-flex items-center gap-1 transition-all focus:outline-none focus:ring-2 focus:ring-accent/40 ${SCOPE_BG_TINT[scope]} ${SCOPE_TEXT[scope]} ${
|
||||||
selected
|
selected
|
||||||
? `border-current shadow-sm`
|
? `border-current shadow-sm`
|
||||||
: 'border-transparent opacity-70 hover:opacity-100'
|
: 'border-transparent opacity-70 hover:opacity-100'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
{selected && <span aria-hidden className="text-[10px]">✓</span>}
|
||||||
{scope}
|
{scope}
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user