fix: делает "Предложение по улучшению" необязательным для жалобы, убирает дубль заголовка
Каждый вид обращения раньше показывал одинаковый набор обязательных полей. getAppealFieldsConfig теперь описывает и видимость, и обязательность отдельно: жалоба — предложение по-прежнему можно оставить, но требовать его как для самостоятельного вида "Предложение" неверно. validateFeedback читает то же правило, что и форма, — иначе одно поле могло бы требовать то, что другое не показывает. FormCard в FeedbackForm дублировал заголовок и подзаголовок PageShell — убран (пустой title у ui-kit не создаёт строки, гэпа не остаётся). Подсказка у переключателя "Обращение анонимное" разбита на две сноски (* и **), как в исходной форме, а не слитный абзац. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,9 @@
|
|||||||
color: $gray-500;
|
color: $gray-500;
|
||||||
font-family: $font-sans;
|
font-family: $font-sans;
|
||||||
font-size: $fs-caption;
|
font-size: $fs-caption;
|
||||||
|
// Подсказка может содержать несколько сносок через \n (см. anonymousHint) —
|
||||||
|
// pre-line превращает переносы строк в тексте в реальные строки на экране.
|
||||||
|
white-space: pre-line;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Красная рамка у контрола внутри — ui-kit не знает о состоянии ошибки,
|
// Красная рамка у контрола внутри — ui-kit не знает о состоянии ошибки,
|
||||||
|
|||||||
@@ -24,7 +24,10 @@ export const FeedbackForm: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormCard title={TEXTS.formTitle} subtitle={TEXTS.pageSubtitle}>
|
// FormCard требует title, но заголовок страницы уже показан в PageShell
|
||||||
|
// (App.tsx) — второй здесь был бы дублем. Пустая строка убирает текст,
|
||||||
|
// а гэпа не оставляет: у ui-kit пустой title-блок не создаёт строки.
|
||||||
|
<FormCard title="">
|
||||||
{formError ? (
|
{formError ? (
|
||||||
<div className={styles.notice}>
|
<div className={styles.notice}>
|
||||||
<Alert variant="err">{formError}</Alert>
|
<Alert variant="err">{formError}</Alert>
|
||||||
|
|||||||
@@ -45,27 +45,42 @@ export const KIND_OPTIONS: ISelectOption[] = [
|
|||||||
{ value: 'other', label: 'Другое' }
|
{ value: 'other', label: 'Другое' }
|
||||||
];
|
];
|
||||||
|
|
||||||
/** Какие текстовые поля показываем для выбранного вида обращения. */
|
/** Показывать поле или нет, и если показывать — обязательно ли оно. */
|
||||||
export interface IVisibleAppealFields {
|
export interface IAppealFieldConfig {
|
||||||
situation: boolean;
|
visible: boolean;
|
||||||
suggestion: boolean;
|
required: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Настройка текстовых полей для выбранного вида обращения. */
|
||||||
|
export interface IAppealFieldsConfig {
|
||||||
|
situation: IAppealFieldConfig;
|
||||||
|
suggestion: IAppealFieldConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Состав полей зависит от вида обращения:
|
* Состав полей зависит от вида обращения:
|
||||||
* жалоба — что случилось и что предлагаете изменить;
|
* жалоба — обязательно описание, а предложение — на желание автора;
|
||||||
* предложение — только само предложение, «ситуации» ещё нет;
|
* предложение — только само предложение, «ситуации» ещё нет;
|
||||||
* другое — свободное описание, без предложения.
|
* другое — свободное описание, без предложения.
|
||||||
* Одно правило на форму и на валидацию, чтобы они не разошлись.
|
* Одно правило на форму и на валидацию, чтобы они не разошлись.
|
||||||
*/
|
*/
|
||||||
export function getVisibleAppealFields(kind: AppealKind): IVisibleAppealFields {
|
export function getAppealFieldsConfig(kind: AppealKind): IAppealFieldsConfig {
|
||||||
switch (kind) {
|
switch (kind) {
|
||||||
case 'complaint':
|
case 'complaint':
|
||||||
return { situation: true, suggestion: true };
|
return {
|
||||||
|
situation: { visible: true, required: true },
|
||||||
|
suggestion: { visible: true, required: false }
|
||||||
|
};
|
||||||
case 'suggestion':
|
case 'suggestion':
|
||||||
return { situation: false, suggestion: true };
|
return {
|
||||||
|
situation: { visible: false, required: false },
|
||||||
|
suggestion: { visible: true, required: true }
|
||||||
|
};
|
||||||
case 'other':
|
case 'other':
|
||||||
return { situation: true, suggestion: false };
|
return {
|
||||||
|
situation: { visible: true, required: true },
|
||||||
|
suggestion: { visible: false, required: false }
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { FormGrid, Select, TextArea, TextInput } from '@tnsu/ui-kit';
|
import { FormGrid, Select, TextArea, TextInput } from '@tnsu/ui-kit';
|
||||||
import { Field } from '../../../components/Field/Field';
|
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 { TEXTS } from '../texts';
|
||||||
import type { ISectionProps } from './ISectionProps';
|
import type { ISectionProps } from './ISectionProps';
|
||||||
|
|
||||||
@@ -9,7 +9,7 @@ const SUGGESTION_ROWS = 4;
|
|||||||
|
|
||||||
/** Суть обращения: вид и зависящие от него текстовые поля. */
|
/** Суть обращения: вид и зависящие от него текстовые поля. */
|
||||||
export const AppealSection: React.FC<ISectionProps> = ({ data, errors, set }) => {
|
export const AppealSection: React.FC<ISectionProps> = ({ data, errors, set }) => {
|
||||||
const visible = getVisibleAppealFields(data.kind);
|
const fields = getAppealFieldsConfig(data.kind);
|
||||||
const isOther = data.kind === 'other';
|
const isOther = data.kind === 'other';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -39,11 +39,11 @@ export const AppealSection: React.FC<ISectionProps> = ({ data, errors, set }) =>
|
|||||||
</Field>
|
</Field>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{visible.situation ? (
|
{fields.situation.visible ? (
|
||||||
<Field
|
<Field
|
||||||
label={isOther ? TEXTS.labels.situationOther : TEXTS.labels.situation}
|
label={isOther ? TEXTS.labels.situationOther : TEXTS.labels.situation}
|
||||||
htmlFor="situation"
|
htmlFor="situation"
|
||||||
required
|
required={fields.situation.required}
|
||||||
full
|
full
|
||||||
error={errors.situation}
|
error={errors.situation}
|
||||||
>
|
>
|
||||||
@@ -59,11 +59,11 @@ export const AppealSection: React.FC<ISectionProps> = ({ data, errors, set }) =>
|
|||||||
</Field>
|
</Field>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{visible.suggestion ? (
|
{fields.suggestion.visible ? (
|
||||||
<Field
|
<Field
|
||||||
label={TEXTS.labels.suggestion}
|
label={TEXTS.labels.suggestion}
|
||||||
htmlFor="suggestion"
|
htmlFor="suggestion"
|
||||||
required
|
required={fields.suggestion.required}
|
||||||
full
|
full
|
||||||
error={errors.suggestion}
|
error={errors.suggestion}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -6,10 +6,11 @@ export const TEXTS = {
|
|||||||
pageTitle: 'Обращение',
|
pageTitle: 'Обращение',
|
||||||
pageSubtitle: 'Данное обращение будет направлено в HR департамент',
|
pageSubtitle: 'Данное обращение будет направлено в HR департамент',
|
||||||
|
|
||||||
formTitle: 'Обращение в HR департамент',
|
// Две сноски с разным числом звёздочек — как в исходной форме, чтобы читалось
|
||||||
|
// как два отдельных примечания, а не слитный абзац. Field рендерит их построчно.
|
||||||
anonymousHint:
|
anonymousHint:
|
||||||
'Анонимное обращение: мы не сможем дать вам обратную связь. Именные обращения обрабатываются в течение 3–5 рабочих дней, если вопрос не требует большего срока.',
|
'* Если обращение анонимное, мы не сможем дать вам обратную связь.\n' +
|
||||||
|
'** Именные обращения обрабатываются в течение 3–5 рабочих дней, если вопрос не требует большего срока.',
|
||||||
|
|
||||||
labels: {
|
labels: {
|
||||||
company: 'Компания',
|
company: 'Компания',
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getVisibleAppealFields, type FeedbackErrors, type IFeedbackData } from './model';
|
import { getAppealFieldsConfig, type FeedbackErrors, type IFeedbackData } from './model';
|
||||||
import { TEXTS } from './texts';
|
import { TEXTS } from './texts';
|
||||||
|
|
||||||
/** Ниже этой длины текст не считаем содержательным описанием. */
|
/** Ниже этой длины текст не считаем содержательным описанием. */
|
||||||
@@ -26,22 +26,24 @@ export function validateFeedback(data: IFeedbackData): FeedbackErrors {
|
|||||||
errors.kindOther = TEXTS.errors.required;
|
errors.kindOther = TEXTS.errors.required;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Спрашиваем только то, что реально показано для выбранного вида обращения:
|
// Правило видимости/обязательности общее с формой (AppealSection), поэтому
|
||||||
// иначе форма ругалась бы на скрытое поле, и пользователь не понял бы, где ошибка.
|
// валидируем по нему же — иначе форма ругалась бы на скрытое или необязательное поле.
|
||||||
const visible = getVisibleAppealFields(data.kind);
|
const fields = getAppealFieldsConfig(data.kind);
|
||||||
|
|
||||||
if (visible.situation) {
|
if (fields.situation.visible) {
|
||||||
if (!data.situation.trim()) {
|
const value = data.situation.trim();
|
||||||
|
if (fields.situation.required && !value) {
|
||||||
errors.situation = TEXTS.errors.required;
|
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;
|
errors.situation = TEXTS.errors.tooShort;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (visible.suggestion) {
|
if (fields.suggestion.visible) {
|
||||||
if (!data.suggestion.trim()) {
|
const value = data.suggestion.trim();
|
||||||
|
if (fields.suggestion.required && !value) {
|
||||||
errors.suggestion = TEXTS.errors.required;
|
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;
|
errors.suggestion = TEXTS.errors.tooShort;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user