mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-09-09 07:08:20 +00:00
feat: implement pre-selection for nationality and nation fields by repurposing the placeholder field to store default values
This commit is contained in:
@@ -13,7 +13,13 @@ import {
|
||||
} from '@mantine/core';
|
||||
import { IconPlus, IconTrash } from '@tabler/icons-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { BilingualInput, ModalFooter } from '@ema-platform/ui';
|
||||
import {
|
||||
BilingualInput,
|
||||
CountrySelect,
|
||||
countryCodeToValue,
|
||||
countryValueToCode,
|
||||
ModalFooter,
|
||||
} from '@ema-platform/ui';
|
||||
import type { FormFieldConfig, FormSchemaPalette } from '@ema-platform/api';
|
||||
import { ConditionBuilder, type ConditionValue } from './ConditionBuilder';
|
||||
import type { ConditionTarget } from '../config/schema-paths';
|
||||
@@ -74,6 +80,8 @@ export function FieldEditorDrawer({
|
||||
|
||||
const typeInfo = fieldTypes.find((f) => f.type === draft.type);
|
||||
const isNew = !field;
|
||||
const isCountryType = draft.type === 'NATIONALITY' || draft.type === 'NATION';
|
||||
const isDemonym = draft.type === 'NATIONALITY';
|
||||
|
||||
function save() {
|
||||
if (!draft.key.trim() || !KEY_PATTERN.test(draft.key.trim())) {
|
||||
@@ -170,11 +178,28 @@ export function FieldEditorDrawer({
|
||||
}}
|
||||
/>
|
||||
|
||||
<BilingualInput
|
||||
label={t('certReq.field.placeholder', 'Placeholder')}
|
||||
value={{ en: draft.placeholder?.en ?? '', am: draft.placeholder?.am ?? '' }}
|
||||
onChange={(v) => setDraft((d) => ({ ...d, placeholder: v }))}
|
||||
/>
|
||||
{isCountryType ? (
|
||||
// The picker has its own placeholder, so this slot chooses what is
|
||||
// pre-selected instead. It travels in `placeholder.en` — the one
|
||||
// optional text slot the schema already has — as the English name,
|
||||
// the same form the applicant's answer takes (countryCodeToValue).
|
||||
<CountrySelect
|
||||
label={t('certReq.field.defaultValue', 'Default value')}
|
||||
description={t('certReq.field.defaultValueHelp', 'Pre-selected for the applicant, who can still change it')}
|
||||
demonym={isDemonym}
|
||||
value={countryValueToCode(draft.placeholder?.en, isDemonym)}
|
||||
onChange={(code) => {
|
||||
const name = countryCodeToValue(code, isDemonym);
|
||||
setDraft((d) => ({ ...d, placeholder: name ? { en: name, am: '' } : undefined }));
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<BilingualInput
|
||||
label={t('certReq.field.placeholder', 'Placeholder')}
|
||||
value={{ en: draft.placeholder?.en ?? '', am: draft.placeholder?.am ?? '' }}
|
||||
onChange={(v) => setDraft((d) => ({ ...d, placeholder: v }))}
|
||||
/>
|
||||
)}
|
||||
|
||||
<BilingualInput
|
||||
label={t('certReq.field.helpText', 'Help text')}
|
||||
|
||||
@@ -1956,6 +1956,8 @@ export const am: Translations = {
|
||||
typeNation: "አመልካቹ ከሙሉ የአገር ዝርዝር ይመርጣል (ኢትዮጵያ፣ ኬንያ፣ …)",
|
||||
required: "የግድ ያስፈልጋል",
|
||||
placeholder: "ምሳሌ ጽሑፍ",
|
||||
defaultValue: "ነባሪ ዋጋ",
|
||||
defaultValueHelp: "ለአመልካቹ አስቀድሞ ይመረጣል፤ አሁንም መቀየር ይችላል",
|
||||
helpText: "የእገዛ ጽሑፍ",
|
||||
min: "ዝቅተኛ",
|
||||
max: "ከፍተኛ",
|
||||
|
||||
@@ -1971,6 +1971,8 @@ export const en = {
|
||||
typeNation: 'Applicant picks from the full country list (Ethiopia, Kenya, …)',
|
||||
required: 'Required',
|
||||
placeholder: 'Placeholder',
|
||||
defaultValue: 'Default value',
|
||||
defaultValueHelp: 'Pre-selected for the applicant, who can still change it',
|
||||
helpText: 'Help text',
|
||||
min: 'Minimum',
|
||||
max: 'Maximum',
|
||||
|
||||
@@ -22,10 +22,8 @@ import {
|
||||
import {
|
||||
AmharicDatePicker,
|
||||
CountrySelect,
|
||||
getCountryCode,
|
||||
getCountryName,
|
||||
getNationalityCode,
|
||||
getNationalityName,
|
||||
countryCodeToValue,
|
||||
countryValueToCode,
|
||||
PhoneInput,
|
||||
} from '@ema-platform/ui';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -95,30 +93,6 @@ export function fillFromVessel(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* NATIONALITY/NATION fields are stored the way the profile's Address tab and
|
||||
* seafarer registration store them — the English demonym ("Ethiopian") or
|
||||
* country name ("Ethiopia"), so a certificate prints the word rather than
|
||||
* "ET" — while CountrySelect works in alpha-2 codes. These two convert
|
||||
* between the two forms, and tolerate a value already saved as a bare code.
|
||||
*/
|
||||
function storedToCountryCode(stored: unknown, demonym: boolean): string | null {
|
||||
const raw = typeof stored === 'string' ? stored.trim() : '';
|
||||
if (!raw) return null;
|
||||
// Both lookups run either way: a field switched between the two types in the
|
||||
// builder still resolves an answer stored in the other form.
|
||||
const code = demonym
|
||||
? getNationalityCode(raw) ?? getCountryCode(raw)
|
||||
: getCountryCode(raw) ?? getNationalityCode(raw);
|
||||
return code ?? raw;
|
||||
}
|
||||
|
||||
/** English-pinned deliberately: the stored answer must not change with the UI language. */
|
||||
function countryCodeToStored(code: string | null, demonym: boolean): string | null {
|
||||
if (!code) return null;
|
||||
return (demonym ? getNationalityName(code) : getCountryName(code)) || code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for a SELECT field.
|
||||
*
|
||||
@@ -255,10 +229,10 @@ export function ConfigDrivenSection({
|
||||
<CountrySelect
|
||||
{...common}
|
||||
demonym={field.type === 'NATIONALITY'}
|
||||
placeholder={localized(field.placeholder) || undefined}
|
||||
value={storedToCountryCode(value, field.type === 'NATIONALITY')}
|
||||
// `placeholder` holds the default the page seeds, not hint text.
|
||||
value={countryValueToCode(value, field.type === 'NATIONALITY')}
|
||||
onChange={(v) =>
|
||||
onChange(field.key, countryCodeToStored(v, field.type === 'NATIONALITY'))
|
||||
onChange(field.key, countryCodeToValue(v, field.type === 'NATIONALITY'))
|
||||
}
|
||||
/>
|
||||
) : isLegacyNationality ? (
|
||||
|
||||
@@ -319,11 +319,28 @@ export function LicenseApplicationPage() {
|
||||
const next = { ...prev };
|
||||
for (const section of config.licenseType.formSchema.sections) {
|
||||
for (const field of section.fields) {
|
||||
const source = field.source ?? LEGACY_PROFILE_SOURCES[field.key];
|
||||
if (!source) continue;
|
||||
const current = next[section.key]?.[field.key];
|
||||
const untouched =
|
||||
current === undefined || current === null || current === "";
|
||||
const source = field.source ?? LEGACY_PROFILE_SOURCES[field.key];
|
||||
if (!source) {
|
||||
// A NATIONALITY/NATION field's `placeholder.en` is the default the
|
||||
// builder chose ("Ethiopian"). It only ever fills a blank — the
|
||||
// profile prefills above ran first, and an answer the applicant
|
||||
// cleared on purpose is not re-filled until the draft is next
|
||||
// seeded.
|
||||
const isCountryPicker =
|
||||
field.type === "NATIONALITY" || field.type === "NATION";
|
||||
const preset = field.placeholder?.en?.trim();
|
||||
if (untouched && isCountryPicker && preset) {
|
||||
next[section.key] = {
|
||||
...next[section.key],
|
||||
[field.key]: preset,
|
||||
};
|
||||
changed = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (!field.readOnly && !untouched) continue;
|
||||
const raw = readSourcePath(context, source);
|
||||
// Profile dates arrive as ISO datetimes; a DATE field's picker wants
|
||||
|
||||
@@ -104,6 +104,12 @@ export interface FormFieldConfig {
|
||||
label: Bilingual;
|
||||
type: FormFieldType;
|
||||
required?: boolean;
|
||||
/**
|
||||
* Hint text — except on NATIONALITY/NATION fields, where the builder puts
|
||||
* the pre-selected answer here (`en`, as the English demonym or country
|
||||
* name) and the wizard seeds it into the draft. The server stores it
|
||||
* unchanged either way, so this stays frontend-only.
|
||||
*/
|
||||
placeholder?: Bilingual;
|
||||
helpText?: Bilingual;
|
||||
options?: { value: string; label: Bilingual }[];
|
||||
|
||||
@@ -56,6 +56,28 @@ export function getNationalityCode(name: string | null | undefined, lang: Countr
|
||||
return demonymCode || getCountryCode(name, lang);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stored form of a NATIONALITY/NATION answer — the English demonym or country
|
||||
* name, the way the profile's Address tab and seafarer registration store it,
|
||||
* so a certificate prints the word rather than "ET" — resolved back to the
|
||||
* alpha-2 code the picker works in. Both lookups run either way so an answer
|
||||
* saved in the other form (or as a bare code) still resolves.
|
||||
*/
|
||||
export function countryValueToCode(stored: unknown, demonym: boolean): string | null {
|
||||
const raw = typeof stored === 'string' ? stored.trim() : '';
|
||||
if (!raw) return null;
|
||||
const code = demonym
|
||||
? getNationalityCode(raw) ?? getCountryCode(raw)
|
||||
: getCountryCode(raw) ?? getNationalityCode(raw);
|
||||
return code ?? raw;
|
||||
}
|
||||
|
||||
/** The reverse: English-pinned deliberately, so the stored answer never depends on the UI language. */
|
||||
export function countryCodeToValue(code: string | null, demonym: boolean): string | null {
|
||||
if (!code) return null;
|
||||
return (demonym ? getNationalityName(code) : getCountryName(code)) || code;
|
||||
}
|
||||
|
||||
export function CountryFlag({ code }: { code: string }) {
|
||||
const Flag = Flags[code as keyof typeof Flags];
|
||||
return Flag ? <Flag style={{ width: 22, borderRadius: 2, display: 'block' }} /> : null;
|
||||
|
||||
Reference in New Issue
Block a user