fix: project create UX + project picker in DocumentEdit
- Projects.tsx: pre-validate empty name, show fieldErrors on Field (root cause of '400 validation_error' was empty name silently rejected) - ProjectPicker component for DocumentEdit; selecting a project autofills default client when no client is yet set Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,33 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { api, type ProjectSummary } from '../api.js';
|
||||||
|
|
||||||
|
export function ProjectPicker({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
value: string | null;
|
||||||
|
onChange: (id: string | null, project?: ProjectSummary | null) => void;
|
||||||
|
}) {
|
||||||
|
const [projects, setProjects] = useState<ProjectSummary[]>([]);
|
||||||
|
useEffect(() => {
|
||||||
|
api.get<{ items: ProjectSummary[] }>('/api/projects?status=active&limit=200')
|
||||||
|
.then((r) => setProjects(r.items))
|
||||||
|
.catch(() => setProjects([]));
|
||||||
|
}, []);
|
||||||
|
return (
|
||||||
|
<select
|
||||||
|
className="field__input"
|
||||||
|
value={value ?? ''}
|
||||||
|
onChange={(e) => {
|
||||||
|
const id = e.target.value || null;
|
||||||
|
const p = id ? projects.find((x) => x.id === id) ?? null : null;
|
||||||
|
onChange(id, p);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="">— без проекта —</option>
|
||||||
|
{projects.map((p) => (
|
||||||
|
<option key={p.id} value={p.id}>{p.name}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import { api, ApiError, type Block, type DocBody, type DocStatus, type DocType,
|
|||||||
import { BlocksEditor } from '../components/BlocksEditor.js';
|
import { BlocksEditor } from '../components/BlocksEditor.js';
|
||||||
import { ClientPicker } from '../components/ClientPicker.js';
|
import { ClientPicker } from '../components/ClientPicker.js';
|
||||||
import { LinesEditor, type LineDraft } from '../components/LinesEditor.js';
|
import { LinesEditor, type LineDraft } from '../components/LinesEditor.js';
|
||||||
|
import { ProjectPicker } from '../components/ProjectPicker.js';
|
||||||
import { Button, Field, Select } from '../components/ui.js';
|
import { Button, Field, Select } from '../components/ui.js';
|
||||||
import { emptyRich } from '../lib/richtext.js';
|
import { emptyRich } from '../lib/richtext.js';
|
||||||
|
|
||||||
@@ -257,6 +258,19 @@ export function DocumentEditPage() {
|
|||||||
value={issuedAt}
|
value={issuedAt}
|
||||||
onChange={(e) => setIssuedAt(e.target.value)}
|
onChange={(e) => setIssuedAt(e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
<label className="field">
|
||||||
|
<span className="field__label">Проект</span>
|
||||||
|
<ProjectPicker
|
||||||
|
value={projectId}
|
||||||
|
onChange={(id, project) => {
|
||||||
|
setProjectId(id);
|
||||||
|
// При смене проекта подставим default-клиента, если у документа клиент ещё не задан
|
||||||
|
if (project && project.defaultClientId && !clientId) {
|
||||||
|
setClientId(project.defaultClientId);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
<label className="field">
|
<label className="field">
|
||||||
<span className="field__label">Клиент</span>
|
<span className="field__label">Клиент</span>
|
||||||
<ClientPicker value={clientId} onChange={setClientId} />
|
<ClientPicker value={clientId} onChange={setClientId} />
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export function ProjectsPage() {
|
|||||||
const [q, setQ] = useState('');
|
const [q, setQ] = useState('');
|
||||||
const [creating, setCreating] = useState<{ name: string } | null>(null);
|
const [creating, setCreating] = useState<{ name: string } | null>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({});
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
@@ -34,9 +35,14 @@ export function ProjectsPage() {
|
|||||||
|
|
||||||
async function create() {
|
async function create() {
|
||||||
if (!creating) return;
|
if (!creating) return;
|
||||||
|
if (!creating.name.trim()) {
|
||||||
|
setFieldErrors({ name: 'обязательное поле' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setFieldErrors({});
|
||||||
try {
|
try {
|
||||||
const p = await api.post<ProjectSummary>('/api/projects', {
|
const p = await api.post<ProjectSummary>('/api/projects', {
|
||||||
name: creating.name,
|
name: creating.name.trim(),
|
||||||
status: 'active',
|
status: 'active',
|
||||||
defaultClientId: null,
|
defaultClientId: null,
|
||||||
defaultTemplateId: null,
|
defaultTemplateId: null,
|
||||||
@@ -46,7 +52,13 @@ export function ProjectsPage() {
|
|||||||
setCreating(null);
|
setCreating(null);
|
||||||
navigate(`/projects/${p.id}`);
|
navigate(`/projects/${p.id}`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError(e instanceof ApiError ? e.prettyMessage() : String(e));
|
if (e instanceof ApiError) {
|
||||||
|
const fe = e.fieldErrors();
|
||||||
|
setFieldErrors(fe);
|
||||||
|
setError(Object.keys(fe).length ? 'Проверьте подсвеченные поля.' : e.prettyMessage());
|
||||||
|
} else {
|
||||||
|
setError(String(e));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,8 +156,12 @@ export function ProjectsPage() {
|
|||||||
<Field
|
<Field
|
||||||
label="Название"
|
label="Название"
|
||||||
value={creating.name}
|
value={creating.name}
|
||||||
onChange={(e) => setCreating({ name: e.target.value })}
|
onChange={(e) => {
|
||||||
|
setCreating({ name: e.target.value });
|
||||||
|
if (fieldErrors.name) setFieldErrors((fe) => { const n = { ...fe }; delete n.name; return n; });
|
||||||
|
}}
|
||||||
placeholder="напр. Свадьба Ивановых, июнь 2026"
|
placeholder="напр. Свадьба Ивановых, июнь 2026"
|
||||||
|
error={fieldErrors.name}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
Reference in New Issue
Block a user