From 05cd491769a2630904ed6415d701119bbc7017f8 Mon Sep 17 00:00:00 2001 From: "gorbenko.pavel" Date: Fri, 21 Aug 2026 09:44:48 +0500 Subject: [PATCH] =?UTF-8?q?fix:=20=D0=B4=D0=B5=D0=BB=D0=B0=D0=B5=D1=82=20"?= =?UTF-8?q?=D0=9F=D1=80=D0=B5=D0=B4=D0=BB=D0=BE=D0=B6=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=20=D0=BF=D0=BE=20=D1=83=D0=BB=D1=83=D1=87=D1=88=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D1=8E"=20=D0=BD=D0=B5=D0=BE=D0=B1=D1=8F=D0=B7?= =?UTF-8?q?=D0=B0=D1=82=D0=B5=D0=BB=D1=8C=D0=BD=D1=8B=D0=BC=20=D0=B4=D0=BB?= =?UTF-8?q?=D1=8F=20=D0=B6=D0=B0=D0=BB=D0=BE=D0=B1=D1=8B,=20=D1=83=D0=B1?= =?UTF-8?q?=D0=B8=D1=80=D0=B0=D0=B5=D1=82=20=D0=B4=D1=83=D0=B1=D0=BB=D1=8C?= =?UTF-8?q?=20=D0=B7=D0=B0=D0=B3=D0=BE=D0=BB=D0=BE=D0=B2=D0=BA=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Каждый вид обращения раньше показывал одинаковый набор обязательных полей. getAppealFieldsConfig теперь описывает и видимость, и обязательность отдельно: жалоба — предложение по-прежнему можно оставить, но требовать его как для самостоятельного вида "Предложение" неверно. validateFeedback читает то же правило, что и форма, — иначе одно поле могло бы требовать то, что другое не показывает. FormCard в FeedbackForm дублировал заголовок и подзаголовок PageShell — убран (пустой title у ui-kit не создаёт строки, гэпа не остаётся). Подсказка у переключателя "Обращение анонимное" разбита на две сноски (* и **), как в исходной форме, а не слитный абзац. Co-Authored-By: Claude Sonnet 5 --- src/components/Field/Field.module.scss | 3 ++ src/features/feedback/FeedbackForm.tsx | 5 ++- src/features/feedback/model.ts | 33 ++++++++++++++----- .../feedback/sections/AppealSection.tsx | 12 +++---- src/features/feedback/texts.ts | 7 ++-- src/features/feedback/validateFeedback.ts | 22 +++++++------ 6 files changed, 53 insertions(+), 29 deletions(-) diff --git a/src/components/Field/Field.module.scss b/src/components/Field/Field.module.scss index 2bed1ff..c19a82b 100644 --- a/src/components/Field/Field.module.scss +++ b/src/components/Field/Field.module.scss @@ -12,6 +12,9 @@ color: $gray-500; font-family: $font-sans; font-size: $fs-caption; + // Подсказка может содержать несколько сносок через \n (см. anonymousHint) — + // pre-line превращает переносы строк в тексте в реальные строки на экране. + white-space: pre-line; } // Красная рамка у контрола внутри — ui-kit не знает о состоянии ошибки, diff --git a/src/features/feedback/FeedbackForm.tsx b/src/features/feedback/FeedbackForm.tsx index 20868a8..e69cd8e 100644 --- a/src/features/feedback/FeedbackForm.tsx +++ b/src/features/feedback/FeedbackForm.tsx @@ -24,7 +24,10 @@ export const FeedbackForm: React.FC = () => { } return ( - + // FormCard требует title, но заголовок страницы уже показан в PageShell + // (App.tsx) — второй здесь был бы дублем. Пустая строка убирает текст, + // а гэпа не оставляет: у ui-kit пустой title-блок не создаёт строки. + {formError ? (
{formError} diff --git a/src/features/feedback/model.ts b/src/features/feedback/model.ts index 87e7933..43cf2ad 100644 --- a/src/features/feedback/model.ts +++ b/src/features/feedback/model.ts @@ -45,27 +45,42 @@ export const KIND_OPTIONS: ISelectOption[] = [ { value: 'other', label: 'Другое' } ]; -/** Какие текстовые поля показываем для выбранного вида обращения. */ -export interface IVisibleAppealFields { - situation: boolean; - suggestion: boolean; +/** Показывать поле или нет, и если показывать — обязательно ли оно. */ +export interface IAppealFieldConfig { + visible: boolean; + required: boolean; +} + +/** Настройка текстовых полей для выбранного вида обращения. */ +export interface IAppealFieldsConfig { + situation: IAppealFieldConfig; + suggestion: IAppealFieldConfig; } /** * Состав полей зависит от вида обращения: - * жалоба — что случилось и что предлагаете изменить; + * жалоба — обязательно описание, а предложение — на желание автора; * предложение — только само предложение, «ситуации» ещё нет; * другое — свободное описание, без предложения. * Одно правило на форму и на валидацию, чтобы они не разошлись. */ -export function getVisibleAppealFields(kind: AppealKind): IVisibleAppealFields { +export function getAppealFieldsConfig(kind: AppealKind): IAppealFieldsConfig { switch (kind) { case 'complaint': - return { situation: true, suggestion: true }; + return { + situation: { visible: true, required: true }, + suggestion: { visible: true, required: false } + }; case 'suggestion': - return { situation: false, suggestion: true }; + return { + situation: { visible: false, required: false }, + suggestion: { visible: true, required: true } + }; case 'other': - return { situation: true, suggestion: false }; + return { + situation: { visible: true, required: true }, + suggestion: { visible: false, required: false } + }; } } diff --git a/src/features/feedback/sections/AppealSection.tsx b/src/features/feedback/sections/AppealSection.tsx index 69914e9..2997bda 100644 --- a/src/features/feedback/sections/AppealSection.tsx +++ b/src/features/feedback/sections/AppealSection.tsx @@ -1,6 +1,6 @@ import { FormGrid, Select, TextArea, TextInput } from '@tnsu/ui-kit'; import { Field } from '../../../components/Field/Field'; -import { getVisibleAppealFields, KIND_OPTIONS, type AppealKind } from '../model'; +import { getAppealFieldsConfig, KIND_OPTIONS, type AppealKind } from '../model'; import { TEXTS } from '../texts'; import type { ISectionProps } from './ISectionProps'; @@ -9,7 +9,7 @@ const SUGGESTION_ROWS = 4; /** Суть обращения: вид и зависящие от него текстовые поля. */ export const AppealSection: React.FC = ({ data, errors, set }) => { - const visible = getVisibleAppealFields(data.kind); + const fields = getAppealFieldsConfig(data.kind); const isOther = data.kind === 'other'; return ( @@ -39,11 +39,11 @@ export const AppealSection: React.FC = ({ data, errors, set }) => ) : null} - {visible.situation ? ( + {fields.situation.visible ? ( @@ -59,11 +59,11 @@ export const AppealSection: React.FC = ({ data, errors, set }) => ) : null} - {visible.suggestion ? ( + {fields.suggestion.visible ? ( diff --git a/src/features/feedback/texts.ts b/src/features/feedback/texts.ts index 7f5aeea..b3b8f5a 100644 --- a/src/features/feedback/texts.ts +++ b/src/features/feedback/texts.ts @@ -6,10 +6,11 @@ export const TEXTS = { pageTitle: 'Обращение', pageSubtitle: 'Данное обращение будет направлено в HR департамент', - formTitle: 'Обращение в HR департамент', - + // Две сноски с разным числом звёздочек — как в исходной форме, чтобы читалось + // как два отдельных примечания, а не слитный абзац. Field рендерит их построчно. anonymousHint: - 'Анонимное обращение: мы не сможем дать вам обратную связь. Именные обращения обрабатываются в течение 3–5 рабочих дней, если вопрос не требует большего срока.', + '* Если обращение анонимное, мы не сможем дать вам обратную связь.\n' + + '** Именные обращения обрабатываются в течение 3–5 рабочих дней, если вопрос не требует большего срока.', labels: { company: 'Компания', diff --git a/src/features/feedback/validateFeedback.ts b/src/features/feedback/validateFeedback.ts index eba6b3e..9bba66a 100644 --- a/src/features/feedback/validateFeedback.ts +++ b/src/features/feedback/validateFeedback.ts @@ -1,4 +1,4 @@ -import { getVisibleAppealFields, type FeedbackErrors, type IFeedbackData } from './model'; +import { getAppealFieldsConfig, type FeedbackErrors, type IFeedbackData } from './model'; import { TEXTS } from './texts'; /** Ниже этой длины текст не считаем содержательным описанием. */ @@ -26,22 +26,24 @@ export function validateFeedback(data: IFeedbackData): FeedbackErrors { errors.kindOther = TEXTS.errors.required; } - // Спрашиваем только то, что реально показано для выбранного вида обращения: - // иначе форма ругалась бы на скрытое поле, и пользователь не понял бы, где ошибка. - const visible = getVisibleAppealFields(data.kind); + // Правило видимости/обязательности общее с формой (AppealSection), поэтому + // валидируем по нему же — иначе форма ругалась бы на скрытое или необязательное поле. + const fields = getAppealFieldsConfig(data.kind); - if (visible.situation) { - if (!data.situation.trim()) { + if (fields.situation.visible) { + const value = data.situation.trim(); + if (fields.situation.required && !value) { errors.situation = TEXTS.errors.required; - } else if (data.situation.trim().length < MIN_TEXT_LENGTH) { + } else if (value && value.length < MIN_TEXT_LENGTH) { errors.situation = TEXTS.errors.tooShort; } } - if (visible.suggestion) { - if (!data.suggestion.trim()) { + if (fields.suggestion.visible) { + const value = data.suggestion.trim(); + if (fields.suggestion.required && !value) { errors.suggestion = TEXTS.errors.required; - } else if (data.suggestion.trim().length < MIN_TEXT_LENGTH) { + } else if (value && value.length < MIN_TEXT_LENGTH) { errors.suggestion = TEXTS.errors.tooShort; } }