155 lines
4.6 KiB
TypeScript
155 lines
4.6 KiB
TypeScript
import { useCallback } from "react";
|
|
import { useNavigate } from "react-router-dom";
|
|
|
|
import {
|
|
Button,
|
|
AsyncSurface,
|
|
DirtyNavigationDialog,
|
|
ErrorSummary,
|
|
Form,
|
|
FormActions,
|
|
FormPage,
|
|
FormField,
|
|
useAppForm,
|
|
useDirtyNavigationGuard,
|
|
} from "../../../presentation/design-system/index.ts";
|
|
import {
|
|
REFERENCE_FORM_DEFAULTS,
|
|
referenceResourceFormSchema,
|
|
toCreateReferenceCommand,
|
|
type ReferenceResourceFormValues,
|
|
} from "./reference-resource-form.ts";
|
|
import { useReferenceCreate } from "./use-reference-feature.ts";
|
|
|
|
const FIELD_LABELS = Object.freeze({
|
|
name: "새 항목 이름",
|
|
note: "설명",
|
|
}) satisfies Record<keyof ReferenceResourceFormValues, string>;
|
|
|
|
export default function ReferenceResourceFormPage() {
|
|
const navigate = useNavigate();
|
|
const mutation = useReferenceCreate();
|
|
const submit = useCallback(
|
|
(command: ReturnType<typeof toCreateReferenceCommand>) =>
|
|
mutation.submit(command),
|
|
[mutation],
|
|
);
|
|
const form = useAppForm({
|
|
schema: referenceResourceFormSchema,
|
|
defaultValues: REFERENCE_FORM_DEFAULTS,
|
|
allowedServerFields: ["name", "note"],
|
|
mapToCommand: toCreateReferenceCommand,
|
|
submit,
|
|
});
|
|
const mutationEffectUnknown =
|
|
mutation.state.indicator === "mutation-effect-unknown";
|
|
const mutationBlocked =
|
|
mutationEffectUnknown || mutation.state.indicator === "mutation-pending";
|
|
const guard = useDirtyNavigationGuard(form.dirty && !form.pending);
|
|
|
|
return (
|
|
<Form
|
|
id={form.formId}
|
|
pending={form.pending}
|
|
onSubmit={(event) => {
|
|
if (mutationBlocked) {
|
|
event.preventDefault();
|
|
return;
|
|
}
|
|
void form.submitForm(event);
|
|
}}
|
|
>
|
|
<FormPage
|
|
breadcrumb={
|
|
<button
|
|
className="ui-button ui-button--ghost"
|
|
type="button"
|
|
onClick={() => navigate("/examples/reference-resources")}
|
|
>
|
|
목록으로 돌아가기
|
|
</button>
|
|
}
|
|
heading={{
|
|
eyebrow: "FormPage",
|
|
title: "Reference resource 만들기",
|
|
description:
|
|
"presentation schema, command mapper, 422/conflict와 dirty navigation 정책을 실행합니다.",
|
|
}}
|
|
errorSummary={
|
|
<ErrorSummary
|
|
fieldErrors={form.fieldErrors}
|
|
formErrors={form.formErrors}
|
|
fieldLabels={FIELD_LABELS}
|
|
fieldId={form.fieldId}
|
|
onFocusField={form.focusField}
|
|
/>
|
|
}
|
|
fields={
|
|
<>
|
|
<FormField
|
|
{...form.field("name")}
|
|
label={FIELD_LABELS.name}
|
|
description="앞뒤 공백은 command mapper 전에 제거됩니다."
|
|
autoComplete="off"
|
|
required
|
|
/>
|
|
<FormField
|
|
{...form.field("note")}
|
|
label={FIELD_LABELS.note}
|
|
description="선택 입력이며 비어 있으면 command에 포함되지 않습니다."
|
|
autoComplete="off"
|
|
/>
|
|
</>
|
|
}
|
|
formActions={
|
|
<FormActions sticky>
|
|
<Button
|
|
variant="secondary"
|
|
onClick={() => navigate("/examples/reference-resources")}
|
|
disabled={form.pending || mutationBlocked}
|
|
>
|
|
취소
|
|
</Button>
|
|
<Button
|
|
type="submit"
|
|
disabled={form.pending || mutationBlocked}
|
|
>
|
|
{form.pending ? "저장 중…" : "저장"}
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
onClick={() => form.reset()}
|
|
disabled={!form.dirty || form.pending || mutationBlocked}
|
|
>
|
|
초기화
|
|
</Button>
|
|
</FormActions>
|
|
}
|
|
feedback={
|
|
mutationEffectUnknown ? (
|
|
<AsyncSurface
|
|
state={mutation.state}
|
|
onReconcileUnknownEffect={(resolution) => {
|
|
void mutation.reconcileUnknownEffect(resolution).then(() => {
|
|
if (resolution === "APPLIED") form.settleApplied();
|
|
});
|
|
}}
|
|
/>
|
|
) : form.result === "success" ? (
|
|
<p role="status">저장했습니다.</p>
|
|
) : form.result === "conflict" ? (
|
|
<p role="status">충돌을 해결한 뒤 다시 제출할 수 있습니다.</p>
|
|
) : null
|
|
}
|
|
aside={
|
|
<p>
|
|
form value는 URL, storage, telemetry에 저장되지 않고 submit 시에만
|
|
application command로 변환됩니다.
|
|
</p>
|
|
}
|
|
guard={<DirtyNavigationDialog guard={guard} />}
|
|
/>
|
|
</Form>
|
|
);
|
|
}
|