mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-01 16:12:02 +00:00
fix: make the tag and address book row buttons and New tag full-size buttons with a touch hit area, and name each tag button after its tag
This commit is contained in:
@@ -89,6 +89,17 @@ describe('KeywordSettings', () => {
|
||||
expect(useSettingsStore.getState().emailKeywords.find((k) => k.id === 'red')).toBeUndefined();
|
||||
});
|
||||
|
||||
// A list of "Edit" / "Delete" with nothing else said which tag a screen
|
||||
// reader user was on; each button is named after its tag now, as a full
|
||||
// size icon button (32px, 44px hit area on touch).
|
||||
it('names each tag row button after its tag', () => {
|
||||
render(<KeywordSettings />);
|
||||
expect(screen.getAllByRole('button', { name: 'edit_tag' })).toHaveLength(DEFAULT_KEYWORDS.length);
|
||||
const deletes = screen.getAllByRole('button', { name: 'delete_tag' });
|
||||
expect(deletes).toHaveLength(DEFAULT_KEYWORDS.length);
|
||||
expect(deletes[0].className).toContain('h-8 w-8');
|
||||
});
|
||||
|
||||
it('shows edit form when edit button clicked', () => {
|
||||
render(<KeywordSettings />);
|
||||
const editButtons = screen.getAllByTitle('edit');
|
||||
|
||||
@@ -45,7 +45,8 @@ describe('deleting a tag', () => {
|
||||
render(<KeywordSettings />);
|
||||
const second = DEFAULT_KEYWORDS[1];
|
||||
|
||||
fireEvent.click(screen.getAllByRole('button', { name: 'delete' })[1]);
|
||||
// Each Delete is named after its tag (the mocked t returns the key).
|
||||
fireEvent.click(screen.getAllByRole('button', { name: 'delete_tag' })[1]);
|
||||
|
||||
expect(useSettingsStore.getState().emailKeywords.map((k) => k.id)).not.toContain(second.id);
|
||||
expect(screen.queryByRole('alertdialog')).toBeNull();
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useManagedAccountStore } from "@/stores/managed-account-store";
|
||||
import { toast } from "@/stores/toast-store";
|
||||
import { SettingsSection } from "./settings-section";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { IconButton } from "@/components/ui/icon-button";
|
||||
import type { AddressBook, AddressBookRights } from "@/lib/jmap/types";
|
||||
import { ShareCollectionDialog } from "./share-collection-dialog";
|
||||
import { PromptDialog } from "@/components/ui/prompt-dialog";
|
||||
@@ -207,10 +208,10 @@ export function AddressBookManagementSettings() {
|
||||
>
|
||||
<Book className="w-4 h-4 text-muted-foreground flex-shrink-0" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<span className="text-sm font-medium truncate block">{book.name}</span>
|
||||
<span className="text-sm font-medium truncate block" title={book.name} dir="auto">{book.name}</span>
|
||||
</div>
|
||||
{book.isDefault && (
|
||||
<span className="text-xs text-muted-foreground bg-muted px-2 py-0.5 rounded-full">
|
||||
<span className="flex-shrink-0 text-xs text-muted-foreground bg-muted px-2 py-0.5 rounded-full">
|
||||
{t("default")}
|
||||
</span>
|
||||
)}
|
||||
@@ -227,49 +228,31 @@ export function AddressBookManagementSettings() {
|
||||
</span>
|
||||
);
|
||||
})()}
|
||||
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 focus-within:opacity-100 pointer-coarse:opacity-100 transition-opacity">
|
||||
<div className="flex items-center gap-0.5 opacity-0 group-hover:opacity-100 focus-within:opacity-100 pointer-coarse:opacity-100 transition-opacity">
|
||||
{canRename && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setEditingId(book.id)}
|
||||
className="p-1.5 rounded-md hover:bg-muted text-muted-foreground hover:text-foreground transition-colors"
|
||||
title={t("rename")}
|
||||
>
|
||||
<Pencil className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<IconButton label={t("rename")} onClick={() => setEditingId(book.id)} className="text-muted-foreground">
|
||||
<Pencil />
|
||||
</IconButton>
|
||||
)}
|
||||
{canSetDefault && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleSetDefault(book)}
|
||||
disabled={isLoading}
|
||||
className="p-1.5 rounded-md hover:bg-muted text-muted-foreground hover:text-foreground transition-colors disabled:opacity-50"
|
||||
title={t("set_default")}
|
||||
>
|
||||
<Star className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<IconButton label={t("set_default")} onClick={() => handleSetDefault(book)} disabled={isLoading} className="text-muted-foreground">
|
||||
<Star />
|
||||
</IconButton>
|
||||
)}
|
||||
{!book.isShared && book.myRights?.mayShare && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSharingId(book.id)}
|
||||
className="p-1.5 rounded-md hover:bg-muted text-muted-foreground hover:text-foreground transition-colors"
|
||||
title={t("share")}
|
||||
>
|
||||
<Users className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<IconButton label={t("share")} onClick={() => setSharingId(book.id)} className="text-muted-foreground">
|
||||
<Users />
|
||||
</IconButton>
|
||||
)}
|
||||
{canDelete && (
|
||||
<button
|
||||
type="button"
|
||||
<IconButton
|
||||
label={t("delete")}
|
||||
onClick={() => void handleDelete(book)}
|
||||
disabled={isLoading}
|
||||
className="p-1.5 rounded-md hover:bg-destructive/10 text-muted-foreground hover:text-destructive transition-colors disabled:opacity-50"
|
||||
title={t("delete")}
|
||||
aria-label={t("delete")}
|
||||
className="text-muted-foreground hover:text-destructive hover:bg-destructive/10"
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<Trash2 />
|
||||
</IconButton>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -16,6 +16,8 @@ import { useEmailStore } from "@/stores/email-store";
|
||||
import { useFilterStore } from "@/stores/filter-store";
|
||||
import { SettingsSection, SettingItem, ToggleSwitch, Select, AdvancedOnly } from './settings-section';
|
||||
import { Plus, Pencil, Trash2, GripVertical, Check, X, Loader2, Search } from "@/components/icons";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { IconButton } from "@/components/ui/icon-button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { KEYWORD_PREFIX } from "@/lib/thread-utils";
|
||||
import { findUnrecognizedKeywords, type UnrecognizedKeyword } from "@/lib/keyword-discovery";
|
||||
@@ -93,6 +95,7 @@ function KeywordRow({
|
||||
}) {
|
||||
const t = useTranslations("settings.keywords");
|
||||
const hasChildren = hasChildKeywords(keyword.id, keywords);
|
||||
const tagLabel = keyword.label || keyword.id;
|
||||
// Measured with the prefix attached, since that is what occupies the column.
|
||||
const keywordCandidates = (nestedTags ? keywordRenderings(keywordLevels(keyword.id)) : [keyword.id])
|
||||
.map((rendering) => KEYWORD_PREFIX + rendering);
|
||||
@@ -134,24 +137,19 @@ function KeywordRow({
|
||||
ariaLabel={t("visibility_field")}
|
||||
className="text-xs py-1"
|
||||
/>
|
||||
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 focus-within:opacity-100 pointer-coarse:opacity-100 transition-opacity">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onEdit}
|
||||
className="p-1.5 rounded-md hover:bg-muted text-muted-foreground hover:text-foreground transition-colors"
|
||||
title={t("edit")}
|
||||
>
|
||||
<Pencil className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
<div className="flex items-center gap-0.5 opacity-0 group-hover:opacity-100 focus-within:opacity-100 pointer-coarse:opacity-100 transition-opacity">
|
||||
<IconButton label={t("edit_tag", { name: tagLabel })} title={t("edit")} onClick={onEdit} className="text-muted-foreground">
|
||||
<Pencil />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
label={t("delete_tag", { name: tagLabel })}
|
||||
title={hasChildren ? t("has_children_delete") : t("delete")}
|
||||
onClick={onDelete}
|
||||
disabled={hasChildren}
|
||||
className="p-1.5 rounded-md hover:bg-destructive/10 text-muted-foreground hover:text-destructive transition-colors disabled:opacity-40 disabled:hover:bg-transparent disabled:hover:text-muted-foreground"
|
||||
title={hasChildren ? t("has_children_delete") : t("delete")}
|
||||
className="text-muted-foreground hover:text-destructive hover:bg-destructive/10"
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<Trash2 />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -727,17 +725,18 @@ export function KeywordSettings() {
|
||||
/>
|
||||
) : (
|
||||
<div className="flex items-center gap-2 pt-1">
|
||||
<button
|
||||
type="button"
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setIsAdding(true);
|
||||
setEditingId(null);
|
||||
}}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-md border border-dashed border-border hover:border-primary hover:bg-accent transition-colors text-muted-foreground hover:text-foreground"
|
||||
className="border-dashed text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<Plus className="w-3.5 h-3.5" />
|
||||
<Plus />
|
||||
{t("add_keyword")}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1353,6 +1353,8 @@
|
||||
"id_exists": "معرّف الوسم هذا موجود بالفعل",
|
||||
"edit": "تعديل الوسم",
|
||||
"delete": "حذف الوسم",
|
||||
"edit_tag": "تعديل {name}",
|
||||
"delete_tag": "حذف {name}",
|
||||
"deleted": "تم حذف الوسم",
|
||||
"save": "حفظ",
|
||||
"add": "إضافة",
|
||||
|
||||
@@ -1353,6 +1353,8 @@
|
||||
"id_exists": "Aquest ID d'etiqueta ja existeix",
|
||||
"edit": "Edita l'etiqueta",
|
||||
"delete": "Suprimeix l'etiqueta",
|
||||
"edit_tag": "Edita {name}",
|
||||
"delete_tag": "Suprimeix {name}",
|
||||
"deleted": "Etiqueta suprimida",
|
||||
"save": "Desa",
|
||||
"add": "Afegeix",
|
||||
|
||||
@@ -1350,6 +1350,8 @@
|
||||
"id_exists": "Toto ID štítku již existuje",
|
||||
"edit": "Upravit štítek",
|
||||
"delete": "Odstranit štítek",
|
||||
"edit_tag": "Upravit {name}",
|
||||
"delete_tag": "Smazat {name}",
|
||||
"deleted": "Štítek byl odstraněn",
|
||||
"save": "Uložit",
|
||||
"add": "Přidat",
|
||||
|
||||
@@ -1353,6 +1353,8 @@
|
||||
"id_exists": "Dette tag-ID findes allerede",
|
||||
"edit": "Redigér tag",
|
||||
"delete": "Slet tag",
|
||||
"edit_tag": "Rediger {name}",
|
||||
"delete_tag": "Slet {name}",
|
||||
"deleted": "Tag slettet",
|
||||
"save": "Gem",
|
||||
"add": "Tilføj",
|
||||
|
||||
@@ -1350,6 +1350,8 @@
|
||||
"id_exists": "Diese Label-ID existiert bereits",
|
||||
"edit": "Label bearbeiten",
|
||||
"delete": "Label löschen",
|
||||
"edit_tag": "{name} bearbeiten",
|
||||
"delete_tag": "{name} löschen",
|
||||
"deleted": "Label gelöscht",
|
||||
"save": "Speichern",
|
||||
"add": "Hinzufügen",
|
||||
|
||||
@@ -1353,6 +1353,8 @@
|
||||
"id_exists": "This tag ID already exists",
|
||||
"edit": "Edit tag",
|
||||
"delete": "Delete tag",
|
||||
"edit_tag": "Edit {name}",
|
||||
"delete_tag": "Delete {name}",
|
||||
"deleted": "Tag deleted",
|
||||
"save": "Save",
|
||||
"add": "Add",
|
||||
|
||||
@@ -1350,6 +1350,8 @@
|
||||
"id_exists": "Este ID de etiqueta ya existe",
|
||||
"edit": "Editar etiqueta",
|
||||
"delete": "Eliminar etiqueta",
|
||||
"edit_tag": "Editar {name}",
|
||||
"delete_tag": "Eliminar {name}",
|
||||
"deleted": "Etiqueta eliminada",
|
||||
"save": "Guardar",
|
||||
"add": "Añadir",
|
||||
|
||||
@@ -1353,6 +1353,8 @@
|
||||
"id_exists": "See sildi ID on juba kasutusel",
|
||||
"edit": "Muuda silti",
|
||||
"delete": "Kustuta silt",
|
||||
"edit_tag": "Muuda {name}",
|
||||
"delete_tag": "Kustuta {name}",
|
||||
"deleted": "Silt kustutatud",
|
||||
"save": "Salvesta",
|
||||
"add": "Lisa",
|
||||
|
||||
@@ -1353,6 +1353,8 @@
|
||||
"id_exists": "این شناسه قبلاً وجود دارد",
|
||||
"edit": "ویرایش برچسب",
|
||||
"delete": "حذف برچسب",
|
||||
"edit_tag": "ویرایش {name}",
|
||||
"delete_tag": "حذف {name}",
|
||||
"deleted": "برچسب حذف شد",
|
||||
"save": "ذخیره",
|
||||
"add": "افزودن",
|
||||
|
||||
@@ -1350,6 +1350,8 @@
|
||||
"id_exists": "Cet ID d'étiquette existe déjà",
|
||||
"edit": "Éditer l'étiquette",
|
||||
"delete": "Supprimer l'étiquette",
|
||||
"edit_tag": "Modifier {name}",
|
||||
"delete_tag": "Supprimer {name}",
|
||||
"deleted": "Étiquette supprimée",
|
||||
"save": "Enregistrer",
|
||||
"add": "Ajouter",
|
||||
|
||||
@@ -1315,6 +1315,8 @@
|
||||
"id_exists": "מזהה מילת מפתח זה כבר קיים",
|
||||
"edit": "ערוך מילת מפתח",
|
||||
"delete": "מחק מילת מפתח",
|
||||
"edit_tag": "עריכת {name}",
|
||||
"delete_tag": "מחיקת {name}",
|
||||
"deleted": "מילת המפתח נמחקה",
|
||||
"save": "שמור",
|
||||
"add": "לְהוֹסִיף",
|
||||
|
||||
@@ -1353,6 +1353,8 @@
|
||||
"id_exists": "Ez a címke azonosító már létezik",
|
||||
"edit": "Címke szerkesztése",
|
||||
"delete": "Címke törlése",
|
||||
"edit_tag": "{name} szerkesztése",
|
||||
"delete_tag": "{name} törlése",
|
||||
"deleted": "Címke törölve",
|
||||
"save": "Mentés",
|
||||
"add": "Hozzáadás",
|
||||
|
||||
@@ -1350,6 +1350,8 @@
|
||||
"id_exists": "Questo ID etichetta esiste già",
|
||||
"edit": "Modifica etichetta",
|
||||
"delete": "Elimina etichetta",
|
||||
"edit_tag": "Modifica {name}",
|
||||
"delete_tag": "Elimina {name}",
|
||||
"deleted": "Etichetta eliminata",
|
||||
"save": "Salva",
|
||||
"add": "Aggiungi",
|
||||
|
||||
@@ -1350,6 +1350,8 @@
|
||||
"id_exists": "このラベルIDは既に存在します",
|
||||
"edit": "ラベルを編集",
|
||||
"delete": "ラベルを削除",
|
||||
"edit_tag": "{name} を編集",
|
||||
"delete_tag": "{name} を削除",
|
||||
"deleted": "ラベルを削除しました",
|
||||
"save": "保存",
|
||||
"add": "追加",
|
||||
|
||||
@@ -1350,6 +1350,8 @@
|
||||
"id_exists": "이 태그 ID는 이미 존재합니다",
|
||||
"edit": "태그 편집",
|
||||
"delete": "태그 삭제",
|
||||
"edit_tag": "{name} 편집",
|
||||
"delete_tag": "{name} 삭제",
|
||||
"deleted": "태그가 삭제되었어요",
|
||||
"save": "저장",
|
||||
"add": "추가",
|
||||
|
||||
@@ -1350,6 +1350,8 @@
|
||||
"id_exists": "Šāds taga identifikators jau pastāv",
|
||||
"edit": "Rediģēt tagu",
|
||||
"delete": "Dzēst tagu",
|
||||
"edit_tag": "Rediģēt {name}",
|
||||
"delete_tag": "Dzēst {name}",
|
||||
"deleted": "Tags izdzēsts",
|
||||
"save": "Saglabāt",
|
||||
"add": "Pievienot",
|
||||
|
||||
@@ -1353,6 +1353,8 @@
|
||||
"id_exists": "Энэ хаягийн ID аль хэдийн байна",
|
||||
"edit": "Таг засах",
|
||||
"delete": "Тагыг устгах",
|
||||
"edit_tag": "{name}-г засах",
|
||||
"delete_tag": "{name}-г устгах",
|
||||
"deleted": "Таг устгасан",
|
||||
"save": "Хадгалах",
|
||||
"add": "Нэмэх",
|
||||
|
||||
@@ -1353,6 +1353,8 @@
|
||||
"id_exists": "Denne etikett-ID-en finnes allerede",
|
||||
"edit": "Rediger etikett",
|
||||
"delete": "Slett etikett",
|
||||
"edit_tag": "Rediger {name}",
|
||||
"delete_tag": "Slett {name}",
|
||||
"deleted": "Etikett slettet",
|
||||
"save": "Lagre",
|
||||
"add": "Legg til",
|
||||
|
||||
@@ -1350,6 +1350,8 @@
|
||||
"id_exists": "Deze label-ID bestaat al",
|
||||
"edit": "Label bewerken",
|
||||
"delete": "Label verwijderen",
|
||||
"edit_tag": "{name} bewerken",
|
||||
"delete_tag": "{name} verwijderen",
|
||||
"deleted": "Label verwijderd",
|
||||
"save": "Opslaan",
|
||||
"add": "Toevoegen",
|
||||
|
||||
@@ -1350,6 +1350,8 @@
|
||||
"id_exists": "Ten ID etykiety już istnieje",
|
||||
"edit": "Edytuj etykietę",
|
||||
"delete": "Usuń etykietę",
|
||||
"edit_tag": "Edytuj {name}",
|
||||
"delete_tag": "Usuń {name}",
|
||||
"deleted": "Etykieta usunięta",
|
||||
"save": "Zapisz",
|
||||
"add": "Dodaj",
|
||||
|
||||
@@ -1350,6 +1350,8 @@
|
||||
"id_exists": "Este ID de etiqueta já existe",
|
||||
"edit": "Editar etiqueta",
|
||||
"delete": "Eliminar etiqueta",
|
||||
"edit_tag": "Editar {name}",
|
||||
"delete_tag": "Excluir {name}",
|
||||
"deleted": "Etiqueta eliminada",
|
||||
"save": "Salvar",
|
||||
"add": "Adicionar",
|
||||
|
||||
@@ -1353,6 +1353,8 @@
|
||||
"id_exists": "Această etichetă ID există deja",
|
||||
"edit": "Editați eticheta",
|
||||
"delete": "Șterge eticheta",
|
||||
"edit_tag": "Editați {name}",
|
||||
"delete_tag": "Ștergeți {name}",
|
||||
"deleted": "Eticheta a fost ștearsă",
|
||||
"save": "Salvare",
|
||||
"add": "Adăugați",
|
||||
|
||||
@@ -1350,6 +1350,8 @@
|
||||
"id_exists": "Этот идентификатор тега уже существует",
|
||||
"edit": "Редактировать тег",
|
||||
"delete": "Удалить тег",
|
||||
"edit_tag": "Изменить «{name}»",
|
||||
"delete_tag": "Удалить «{name}»",
|
||||
"deleted": "Тег удалён",
|
||||
"save": "Сохранить",
|
||||
"add": "Добавить",
|
||||
|
||||
@@ -1353,6 +1353,8 @@
|
||||
"id_exists": "Toto ID štítku už existuje",
|
||||
"edit": "Upraviť štítok",
|
||||
"delete": "Odstrániť štítok",
|
||||
"edit_tag": "Upraviť {name}",
|
||||
"delete_tag": "Zmazať {name}",
|
||||
"deleted": "Štítok bol odstránený",
|
||||
"save": "Uložiť",
|
||||
"add": "Pridať",
|
||||
|
||||
@@ -3534,6 +3534,8 @@
|
||||
"id_exists": "รหัสแท็กนี้มีอยู่แล้ว",
|
||||
"edit": "แก้ไขแท็ก",
|
||||
"delete": "ลบแท็ก",
|
||||
"edit_tag": "แก้ไข {name}",
|
||||
"delete_tag": "ลบ {name}",
|
||||
"deleted": "ลบแท็กแล้ว",
|
||||
"save": "บันทึก",
|
||||
"add": "เพิ่ม",
|
||||
|
||||
@@ -1350,6 +1350,8 @@
|
||||
"id_exists": "Bu etiket kimliği zaten mevcut",
|
||||
"edit": "Etiketi düzenle",
|
||||
"delete": "Etiketi sil",
|
||||
"edit_tag": "{name} düzenle",
|
||||
"delete_tag": "{name} sil",
|
||||
"deleted": "Etiket silindi",
|
||||
"save": "Kaydet",
|
||||
"add": "Ekle",
|
||||
|
||||
@@ -1350,6 +1350,8 @@
|
||||
"id_exists": "Цей ідентифікатор ключового слова вже існує",
|
||||
"edit": "Редагувати ключове слово",
|
||||
"delete": "Видалити ключове слово",
|
||||
"edit_tag": "Редагувати «{name}»",
|
||||
"delete_tag": "Видалити «{name}»",
|
||||
"deleted": "Ключове слово видалено",
|
||||
"save": "зберегти",
|
||||
"add": "додати",
|
||||
|
||||
@@ -1353,6 +1353,8 @@
|
||||
"id_exists": "此標籤 ID 已存在",
|
||||
"edit": "編輯標籤",
|
||||
"delete": "刪除標籤",
|
||||
"edit_tag": "編輯 {name}",
|
||||
"delete_tag": "刪除 {name}",
|
||||
"deleted": "標籤已刪除",
|
||||
"save": "儲存",
|
||||
"add": "新增",
|
||||
|
||||
@@ -1350,6 +1350,8 @@
|
||||
"id_exists": "此标签ID已存在",
|
||||
"edit": "编辑标签",
|
||||
"delete": "删除标签",
|
||||
"edit_tag": "编辑 {name}",
|
||||
"delete_tag": "删除 {name}",
|
||||
"deleted": "标签已删除",
|
||||
"save": "保存",
|
||||
"add": "添加",
|
||||
|
||||
Reference in New Issue
Block a user