mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 00:12:39 +00:00
fix: keep a category typed without Enter when the contact is saved
This commit is contained in:
@@ -86,6 +86,26 @@ describe('ContactForm', () => {
|
||||
expect(savedData.media).toBeNull();
|
||||
});
|
||||
|
||||
it('adds a category typed without Enter when the contact is saved, in the spelling of an existing one', async () => {
|
||||
const onSave = vi.fn().mockResolvedValue(undefined);
|
||||
render(<ContactForm contact={existingContact} allKeywords={['Friends']} onSave={onSave} onCancel={vi.fn()} />);
|
||||
|
||||
fireEvent.click(screen.getByText('categories'));
|
||||
fireEvent.change(screen.getByPlaceholderText('categories_placeholder'), { target: { value: 'friends, Gym ' } });
|
||||
fireEvent.submit(screen.getByText('save').closest('form')!);
|
||||
|
||||
await waitFor(() => expect(onSave).toHaveBeenCalledOnce());
|
||||
expect(onSave.mock.calls[0][0].keywords).toEqual({ Friends: true, Gym: true });
|
||||
});
|
||||
|
||||
it('counts a typed category as a change, so leaving asks first', () => {
|
||||
const onDirtyChange = vi.fn();
|
||||
render(<ContactForm contact={existingContact} onSave={vi.fn()} onCancel={vi.fn()} onDirtyChange={onDirtyChange} />);
|
||||
fireEvent.click(screen.getByText('categories'));
|
||||
fireEvent.change(screen.getByPlaceholderText('categories_placeholder'), { target: { value: 'Gym' } });
|
||||
expect(onDirtyChange).toHaveBeenLastCalledWith(true);
|
||||
});
|
||||
|
||||
describe('photos (P0-3)', () => {
|
||||
// The same PHOTO as ContactCard/get returns it on each version.
|
||||
const photo0_16_23: ContactMedia = { kind: 'photo', uri: 'data:image/jpeg;base64,/9j/', mediaType: 'image/jpeg' };
|
||||
|
||||
@@ -398,6 +398,9 @@ export function ContactForm({ contact, addressBooks, allKeywords, defaultAddress
|
||||
const [keywordsStr, setKeywordsStr] = useState(
|
||||
contact?.keywords ? Object.keys(contact.keywords).filter(k => contact.keywords![k]).join(", ") : ""
|
||||
);
|
||||
// Text typed in the Categories field but not added with Enter yet: Save
|
||||
// adds it, so it isn't lost.
|
||||
const [categoryDraft, setCategoryDraft] = useState("");
|
||||
|
||||
const [note, setNote] = useState(
|
||||
contact?.notes ? Object.values(contact.notes)[0]?.note || "" : ""
|
||||
@@ -455,7 +458,7 @@ export function ContactForm({ contact, addressBooks, allKeywords, defaultAddress
|
||||
const fields = JSON.stringify([
|
||||
prefix, givenName, additionalName, surname, suffix, nickname, emails, phones, organization,
|
||||
department, isOrg, jobTitle, role, addresses, onlineServices, anniversaries, personalInfoEntries,
|
||||
keywordsStr, note, genderSex, genderIdentity, calendarUri, schedulingUri, freeBusyUri, selectedBookId,
|
||||
keywordsStr, categoryDraft, note, genderSex, genderIdentity, calendarUri, schedulingUri, freeBusyUri, selectedBookId,
|
||||
]);
|
||||
const [initialFields] = useState(fields);
|
||||
const dirty = fields !== initialFields || photoEdit.kind !== "unchanged";
|
||||
@@ -614,11 +617,14 @@ export function ContactForm({ contact, addressBooks, allKeywords, defaultAddress
|
||||
});
|
||||
|
||||
const keywordsMap: Record<string, boolean> = {};
|
||||
if (keywordsStr.trim()) {
|
||||
keywordsStr.split(",").map(k => k.trim()).filter(Boolean).forEach(k => {
|
||||
keywordsMap[k] = true;
|
||||
});
|
||||
}
|
||||
keywordsStr.split(",").map(k => k.trim()).filter(Boolean).forEach(k => {
|
||||
keywordsMap[k] = true;
|
||||
});
|
||||
// A typed category that matches an existing one takes its spelling.
|
||||
categoryDraft.split(",").map(k => k.trim()).filter(Boolean).forEach(k => {
|
||||
const existing = (allKeywords || []).find(kw => kw.toLowerCase() === k.toLowerCase());
|
||||
keywordsMap[existing ?? k] = true;
|
||||
});
|
||||
|
||||
// Only send `kind` when this form owns the answer: switching a card between
|
||||
// person and organization. Leave other kinds (group, location, ...) untouched.
|
||||
@@ -1126,6 +1132,8 @@ export function ContactForm({ contact, addressBooks, allKeywords, defaultAddress
|
||||
<CategoryComboBox
|
||||
keywordsStr={keywordsStr}
|
||||
onChange={setKeywordsStr}
|
||||
inputValue={categoryDraft}
|
||||
onInputChange={setCategoryDraft}
|
||||
allKeywords={allKeywords || []}
|
||||
placeholder={t("categories_placeholder")}
|
||||
hint={t("categories_hint")}
|
||||
@@ -1201,6 +1209,8 @@ export function ContactForm({ contact, addressBooks, allKeywords, defaultAddress
|
||||
function CategoryComboBox({
|
||||
keywordsStr,
|
||||
onChange,
|
||||
inputValue,
|
||||
onInputChange: setInputValue,
|
||||
allKeywords,
|
||||
placeholder,
|
||||
hint,
|
||||
@@ -1208,6 +1218,9 @@ function CategoryComboBox({
|
||||
}: {
|
||||
keywordsStr: string;
|
||||
onChange: (value: string) => void;
|
||||
/** The text typed but not added yet; the form adds it on Save. */
|
||||
inputValue: string;
|
||||
onInputChange: (value: string) => void;
|
||||
allKeywords: string[];
|
||||
placeholder: string;
|
||||
hint: string;
|
||||
@@ -1215,7 +1228,6 @@ function CategoryComboBox({
|
||||
}) {
|
||||
const t = useTranslations("contacts.form");
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// Parse current keywords from comma-separated string
|
||||
@@ -1243,7 +1255,7 @@ function CategoryComboBox({
|
||||
const next = [...currentKeywords, trimmed].join(", ");
|
||||
onChange(next);
|
||||
setInputValue("");
|
||||
}, [currentKeywords, onChange]);
|
||||
}, [currentKeywords, onChange, setInputValue]);
|
||||
|
||||
const removeKeyword = useCallback((keyword: string) => {
|
||||
const next = currentKeywords.filter(k => k !== keyword).join(", ");
|
||||
|
||||
Reference in New Issue
Block a user