mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-30 16:16:19 +00:00
fix: name the remaining search boxes, filter fields and settings inputs that only had a placeholder
This commit is contained in:
@@ -345,6 +345,7 @@ export function PolicyPage() {
|
||||
<div className="sm:col-span-4">
|
||||
<label className={fieldLabelClass}>Name</label>
|
||||
<input
|
||||
aria-label="Name"
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
@@ -358,6 +359,7 @@ export function PolicyPage() {
|
||||
<label className={fieldLabelClass}>Relay URL</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
aria-label="Relay URL"
|
||||
type="url"
|
||||
inputMode="url"
|
||||
autoComplete="off"
|
||||
@@ -439,6 +441,7 @@ export function PolicyPage() {
|
||||
<div className="sm:col-span-4">
|
||||
<label className={fieldLabelClass}>Name</label>
|
||||
<input
|
||||
aria-label="Name"
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
@@ -452,6 +455,7 @@ export function PolicyPage() {
|
||||
<div className="sm:col-span-5">
|
||||
<label className={fieldLabelClass}>URL</label>
|
||||
<input
|
||||
aria-label="URL"
|
||||
type="url"
|
||||
inputMode="url"
|
||||
autoComplete="off"
|
||||
@@ -474,6 +478,7 @@ export function PolicyPage() {
|
||||
</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
aria-label="Icon"
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
|
||||
@@ -7,6 +7,8 @@ import { TemplateForm } from '@/components/templates/template-form';
|
||||
import { MvSearchForm } from '@/components/search/mv-search-form';
|
||||
import { DEFAULT_SEARCH_FILTERS } from '@/lib/jmap/search-utils';
|
||||
import { JmapServersSection } from '@/app/(main)/admin/_pages/_jmap-servers-section';
|
||||
import { ContactGroupForm } from '@/components/contacts/contact-group-form';
|
||||
import { PromptDialog } from '@/components/ui/prompt-dialog';
|
||||
|
||||
// Every field of the big forms has a name a screen reader reads out (WCAG
|
||||
// 1.3.1, 3.3.2, 4.1.2): a <label> tied to it or an aria-label, never just
|
||||
@@ -63,4 +65,14 @@ describe('form fields have labels', () => {
|
||||
expect(document.querySelectorAll('input').length).toBeGreaterThanOrEqual(8);
|
||||
expect(unnamedFields()).toEqual([]);
|
||||
});
|
||||
|
||||
it('contact group form', () => {
|
||||
render(<ContactGroupForm individuals={[]} onSave={vi.fn()} onCancel={vi.fn()} />);
|
||||
expect(unnamedFields()).toEqual([]);
|
||||
});
|
||||
|
||||
it('the shared one-field prompt', () => {
|
||||
render(<PromptDialog isOpen onClose={vi.fn()} onSubmit={vi.fn()} title="Rename" placeholder="New name" />);
|
||||
expect(unnamedFields()).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -644,6 +644,7 @@ export function EventDetailPopover({
|
||||
{noteExpanded ? (
|
||||
<div className="space-y-2">
|
||||
<textarea
|
||||
aria-label={t("detail.add_note")}
|
||||
ref={noteInputRef}
|
||||
value={noteText}
|
||||
onChange={(e) => setNoteText(e.target.value)}
|
||||
|
||||
@@ -106,6 +106,7 @@ export function ContactGroupForm({
|
||||
{t("groups.name_label")}
|
||||
</label>
|
||||
<Input
|
||||
aria-label={t("groups.name_label")}
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder={t("groups.name_placeholder")}
|
||||
@@ -120,6 +121,7 @@ export function ContactGroupForm({
|
||||
<div className="relative mb-2">
|
||||
<Search className="absolute start-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<Input
|
||||
aria-label={t("groups.search_members")}
|
||||
placeholder={t("groups.search_members")}
|
||||
value={memberSearch}
|
||||
onChange={(e) => setMemberSearch(e.target.value)}
|
||||
|
||||
@@ -477,6 +477,7 @@ export function ContactList({
|
||||
<div>
|
||||
<label className="text-xs text-muted-foreground mb-1 block">{t("filters.organization")}</label>
|
||||
<Input
|
||||
aria-label={t("filters.organization")}
|
||||
value={filters.organization}
|
||||
onChange={(e) => setFilters((f) => ({ ...f, organization: e.target.value }))}
|
||||
placeholder={t("filters.organization_placeholder")}
|
||||
@@ -486,6 +487,7 @@ export function ContactList({
|
||||
<div>
|
||||
<label className="text-xs text-muted-foreground mb-1 block">{t("filters.job_title")}</label>
|
||||
<Input
|
||||
aria-label={t("filters.job_title")}
|
||||
value={filters.jobTitle}
|
||||
onChange={(e) => setFilters((f) => ({ ...f, jobTitle: e.target.value }))}
|
||||
placeholder={t("filters.job_title_placeholder")}
|
||||
@@ -498,6 +500,7 @@ export function ContactList({
|
||||
<div>
|
||||
<label className="text-xs text-muted-foreground mb-1 block">{t("filters.location")}</label>
|
||||
<Input
|
||||
aria-label={t("filters.location")}
|
||||
value={filters.location}
|
||||
onChange={(e) => setFilters((f) => ({ ...f, location: e.target.value }))}
|
||||
placeholder={t("filters.location_placeholder")}
|
||||
@@ -507,6 +510,7 @@ export function ContactList({
|
||||
<div>
|
||||
<label className="text-xs text-muted-foreground mb-1 block">{t("filters.email_domain")}</label>
|
||||
<Input
|
||||
aria-label={t("filters.email_domain")}
|
||||
value={filters.emailDomain}
|
||||
onChange={(e) => setFilters((f) => ({ ...f, emailDomain: e.target.value }))}
|
||||
placeholder={t("filters.email_domain_placeholder")}
|
||||
|
||||
@@ -256,6 +256,7 @@ export function FilePickerDialog({ client, onClose, onPick }: FilePickerDialogPr
|
||||
<div className="relative">
|
||||
<Search className="absolute start-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<Input
|
||||
aria-label={t("search_placeholder")}
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder={t("search_placeholder")}
|
||||
|
||||
@@ -475,6 +475,7 @@ export function FilterRuleModal({
|
||||
{t("rule_name")}
|
||||
</label>
|
||||
<Input
|
||||
aria-label={t("rule_name")}
|
||||
ref={nameRef}
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
|
||||
@@ -56,6 +56,7 @@ export function IconPicker({ value, onChange, className }: IconPickerProps) {
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute start-2.5 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-muted-foreground" />
|
||||
<Input
|
||||
aria-label={t('search_icons')}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
placeholder={t('search_icons')}
|
||||
|
||||
@@ -379,6 +379,7 @@ function ContactsPanel({ onClose }: { onClose: () => void }) {
|
||||
{searching && (
|
||||
<div className="px-4 pb-2">
|
||||
<input
|
||||
aria-label={tContacts("search_placeholder")}
|
||||
autoFocus
|
||||
type="search"
|
||||
value={query}
|
||||
|
||||
@@ -4640,6 +4640,7 @@ export function MailApp({ linkSegments: routeSegments }: MailAppProps = {}) {
|
||||
// is running; Mountain View's form waits for its Search button instead.
|
||||
const renderSearchFolderSelect = (className: string, rerun: boolean) => (
|
||||
<select
|
||||
aria-label={t('advanced_search.search_in')}
|
||||
value={searchMailboxId}
|
||||
onChange={(e) => {
|
||||
setSearchMailboxId(e.target.value);
|
||||
|
||||
@@ -42,6 +42,7 @@ function AddressBookEditRow({
|
||||
{t("name_label")}
|
||||
</label>
|
||||
<input
|
||||
aria-label={t("name_label")}
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
|
||||
@@ -132,6 +132,7 @@ export function CalendarEditForm({
|
||||
{t('name')}
|
||||
</label>
|
||||
<input
|
||||
aria-label={t('name')}
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
|
||||
@@ -190,6 +190,7 @@ export function ComposingSettings() {
|
||||
}}
|
||||
>
|
||||
<input
|
||||
aria-label={t('attachment_reminder.add_placeholder')}
|
||||
type="text"
|
||||
value={newKeyword}
|
||||
onChange={(e) => setNewKeyword(e.target.value)}
|
||||
|
||||
@@ -77,6 +77,7 @@ function TemplateEditor({
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-stretch gap-2">
|
||||
<input
|
||||
aria-label={label}
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={value}
|
||||
|
||||
@@ -529,6 +529,7 @@ export function FolderSettings() {
|
||||
</span>
|
||||
)}
|
||||
<input
|
||||
aria-label={parentId ? t('subfolder_name') : t('new_folder_name')}
|
||||
type="text"
|
||||
value={newFolderName}
|
||||
onChange={(e) => setNewFolderName(e.target.value)}
|
||||
@@ -583,6 +584,7 @@ export function FolderSettings() {
|
||||
<div className="flex items-center gap-2 py-2 px-3" style={{ paddingInlineStart: 12 + depth * 16 }}>
|
||||
<Icon className="w-4 h-4 text-muted-foreground flex-shrink-0" />
|
||||
<input
|
||||
aria-label={t('new_folder_name')}
|
||||
type="text"
|
||||
value={editingName}
|
||||
onChange={(e) => setEditingName(e.target.value)}
|
||||
|
||||
@@ -242,6 +242,7 @@ function KeywordEditForm({
|
||||
{t("label_field")}
|
||||
</label>
|
||||
<input
|
||||
aria-label={t("label_field")}
|
||||
type="text"
|
||||
value={label}
|
||||
onChange={(e) => setLabel(e.target.value)}
|
||||
|
||||
@@ -194,6 +194,7 @@ export function ShareCollectionDialog({
|
||||
</div>
|
||||
<div className="relative">
|
||||
<select
|
||||
aria-label={t("access_for", { name: principal?.name || principal?.email || principalId })}
|
||||
value={preset}
|
||||
onChange={(e) => handleSetRights(principalId, e.target.value as RolePreset)}
|
||||
disabled={savingId === principalId}
|
||||
@@ -240,6 +241,7 @@ export function ShareCollectionDialog({
|
||||
{showAdd && (
|
||||
<div className="space-y-2 border border-border rounded-md p-3">
|
||||
<input
|
||||
aria-label={t("search_placeholder")}
|
||||
type="text"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
|
||||
@@ -84,6 +84,7 @@ function AppForm({
|
||||
<div>
|
||||
<label className="text-sm font-medium">{t("name_label")}</label>
|
||||
<Input
|
||||
aria-label={t("name_label")}
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
placeholder={t("name_placeholder")}
|
||||
@@ -95,6 +96,7 @@ function AppForm({
|
||||
<div>
|
||||
<label className="text-sm font-medium">{t("url_label")}</label>
|
||||
<Input
|
||||
aria-label={t("url_label")}
|
||||
value={formData.url}
|
||||
onChange={(e) => setFormData({ ...formData, url: e.target.value })}
|
||||
placeholder="https://example.com"
|
||||
|
||||
@@ -237,6 +237,7 @@ export function TemplatePicker({ isOpen, onClose, onSelect, accountId }: Templat
|
||||
<div className="relative">
|
||||
<Search className="absolute start-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<Input
|
||||
aria-label={t('search_placeholder')}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder={t('search_placeholder')}
|
||||
|
||||
@@ -122,6 +122,7 @@ export function PromptDialog({
|
||||
}
|
||||
>
|
||||
<Input
|
||||
aria-label={title}
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={value}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
// dom-accessibility-api ships types, but its package.json "exports" hides
|
||||
// them from this project's module resolution. The one function the tests use:
|
||||
declare module 'dom-accessibility-api' {
|
||||
export function computeAccessibleName(root: Element, options?: Record<string, unknown>): string;
|
||||
}
|
||||
@@ -4781,7 +4781,8 @@
|
||||
"readWrite": "قراءة وكتابة",
|
||||
"manager": "مدير",
|
||||
"custom": "مخصص"
|
||||
}
|
||||
},
|
||||
"access_for": "صلاحية الوصول لـ {name}"
|
||||
},
|
||||
"delegates": {
|
||||
"tab": "المشاركة",
|
||||
|
||||
@@ -4781,7 +4781,8 @@
|
||||
"readWrite": "Lectura i escriptura",
|
||||
"manager": "Gestor",
|
||||
"custom": "Personalitzat"
|
||||
}
|
||||
},
|
||||
"access_for": "Accés per a {name}"
|
||||
},
|
||||
"delegates": {
|
||||
"tab": "Compartició",
|
||||
|
||||
@@ -5221,7 +5221,8 @@
|
||||
"readWrite": "Čtení a zápis",
|
||||
"manager": "Správce",
|
||||
"custom": "Vlastní"
|
||||
}
|
||||
},
|
||||
"access_for": "Přístup pro {name}"
|
||||
},
|
||||
"delegates": {
|
||||
"tab": "Sdílení",
|
||||
|
||||
@@ -4781,7 +4781,8 @@
|
||||
"readWrite": "Læs & skriv",
|
||||
"manager": "Administrator",
|
||||
"custom": "Brugerdefineret"
|
||||
}
|
||||
},
|
||||
"access_for": "Adgang for {name}"
|
||||
},
|
||||
"delegates": {
|
||||
"tab": "Deling",
|
||||
|
||||
@@ -5221,7 +5221,8 @@
|
||||
"readWrite": "Lesen & schreiben",
|
||||
"manager": "Verwalten",
|
||||
"custom": "Benutzerdefiniert"
|
||||
}
|
||||
},
|
||||
"access_for": "Zugriff für {name}"
|
||||
},
|
||||
"delegates": {
|
||||
"tab": "Freigaben",
|
||||
|
||||
@@ -4781,7 +4781,8 @@
|
||||
"readWrite": "Read & write",
|
||||
"manager": "Manager",
|
||||
"custom": "Custom"
|
||||
}
|
||||
},
|
||||
"access_for": "Access for {name}"
|
||||
},
|
||||
"delegates": {
|
||||
"tab": "Sharing",
|
||||
|
||||
@@ -5221,7 +5221,8 @@
|
||||
"readWrite": "Lectura y escritura",
|
||||
"manager": "Administrador",
|
||||
"custom": "Personalizado"
|
||||
}
|
||||
},
|
||||
"access_for": "Acceso para {name}"
|
||||
},
|
||||
"delegates": {
|
||||
"tab": "Uso compartido",
|
||||
|
||||
@@ -4781,7 +4781,8 @@
|
||||
"readWrite": "Lugemine ja kirjutamine",
|
||||
"manager": "Haldur",
|
||||
"custom": "Kohandatud"
|
||||
}
|
||||
},
|
||||
"access_for": "Juurdepääs: {name}"
|
||||
},
|
||||
"delegates": {
|
||||
"tab": "Jagamine",
|
||||
|
||||
@@ -4781,7 +4781,8 @@
|
||||
"readWrite": "خواندن و نوشتن",
|
||||
"manager": "مدیر",
|
||||
"custom": "سفارشی"
|
||||
}
|
||||
},
|
||||
"access_for": "دسترسی برای {name}"
|
||||
},
|
||||
"delegates": {
|
||||
"tab": "اشتراکگذاری",
|
||||
|
||||
@@ -5221,7 +5221,8 @@
|
||||
"readWrite": "Lecture & écriture",
|
||||
"manager": "Gestionnaire",
|
||||
"custom": "Personnalisé"
|
||||
}
|
||||
},
|
||||
"access_for": "Accès pour {name}"
|
||||
},
|
||||
"delegates": {
|
||||
"tab": "Partage",
|
||||
|
||||
@@ -5193,7 +5193,8 @@
|
||||
"readWrite": "קרא וכתוב",
|
||||
"manager": "מנהל",
|
||||
"custom": "מותאם אישית"
|
||||
}
|
||||
},
|
||||
"access_for": "גישה עבור {name}"
|
||||
},
|
||||
"delegates": {
|
||||
"tab": "שיתוף",
|
||||
|
||||
@@ -4781,7 +4781,8 @@
|
||||
"readWrite": "Olvasás és írás",
|
||||
"manager": "Kezelő",
|
||||
"custom": "Egyéni"
|
||||
}
|
||||
},
|
||||
"access_for": "Hozzáférés: {name}"
|
||||
},
|
||||
"delegates": {
|
||||
"tab": "Megosztás",
|
||||
|
||||
@@ -5221,7 +5221,8 @@
|
||||
"readWrite": "Lettura e scrittura",
|
||||
"manager": "Gestore",
|
||||
"custom": "Personalizzato"
|
||||
}
|
||||
},
|
||||
"access_for": "Accesso per {name}"
|
||||
},
|
||||
"delegates": {
|
||||
"tab": "Condivisione",
|
||||
|
||||
@@ -5221,7 +5221,8 @@
|
||||
"readWrite": "読み取り・書き込み",
|
||||
"manager": "管理者",
|
||||
"custom": "カスタム"
|
||||
}
|
||||
},
|
||||
"access_for": "{name} のアクセス権"
|
||||
},
|
||||
"delegates": {
|
||||
"tab": "共有",
|
||||
|
||||
@@ -5221,7 +5221,8 @@
|
||||
"readWrite": "읽기 및 쓰기",
|
||||
"manager": "관리자",
|
||||
"custom": "사용자 지정"
|
||||
}
|
||||
},
|
||||
"access_for": "{name}의 액세스 권한"
|
||||
},
|
||||
"delegates": {
|
||||
"tab": "공유",
|
||||
|
||||
@@ -5221,7 +5221,8 @@
|
||||
"readWrite": "Lasīšana un rakstīšana",
|
||||
"manager": "Pārvaldnieks",
|
||||
"custom": "Pielāgots"
|
||||
}
|
||||
},
|
||||
"access_for": "Piekļuve: {name}"
|
||||
},
|
||||
"delegates": {
|
||||
"tab": "Kopīgošana",
|
||||
|
||||
@@ -4781,7 +4781,8 @@
|
||||
"readWrite": "Унших, бичих",
|
||||
"manager": "Менежер",
|
||||
"custom": "Захиалгат"
|
||||
}
|
||||
},
|
||||
"access_for": "{name}-н хандалт"
|
||||
},
|
||||
"delegates": {
|
||||
"tab": "Хуваалцах",
|
||||
|
||||
@@ -4781,7 +4781,8 @@
|
||||
"readWrite": "Kan lese og skrive",
|
||||
"manager": "Kan administrere",
|
||||
"custom": "Egendefinert"
|
||||
}
|
||||
},
|
||||
"access_for": "Tilgang for {name}"
|
||||
},
|
||||
"delegates": {
|
||||
"tab": "Deling",
|
||||
|
||||
@@ -5221,7 +5221,8 @@
|
||||
"readWrite": "Lezen en schrijven",
|
||||
"manager": "Beheerder",
|
||||
"custom": "Aangepast"
|
||||
}
|
||||
},
|
||||
"access_for": "Toegang voor {name}"
|
||||
},
|
||||
"delegates": {
|
||||
"tab": "Delen",
|
||||
|
||||
@@ -5221,7 +5221,8 @@
|
||||
"readWrite": "Odczyt i zapis",
|
||||
"manager": "Menedżer",
|
||||
"custom": "Niestandardowe"
|
||||
}
|
||||
},
|
||||
"access_for": "Dostęp dla {name}"
|
||||
},
|
||||
"delegates": {
|
||||
"tab": "Udostępnianie",
|
||||
|
||||
@@ -5221,7 +5221,8 @@
|
||||
"readWrite": "Leitura e escrita",
|
||||
"manager": "Gerente",
|
||||
"custom": "Personalizado"
|
||||
}
|
||||
},
|
||||
"access_for": "Acesso para {name}"
|
||||
},
|
||||
"delegates": {
|
||||
"tab": "Compartilhamento",
|
||||
|
||||
@@ -4781,7 +4781,8 @@
|
||||
"readWrite": "Citire și scriere",
|
||||
"manager": "Manager",
|
||||
"custom": "Personalizat"
|
||||
}
|
||||
},
|
||||
"access_for": "Acces pentru {name}"
|
||||
},
|
||||
"delegates": {
|
||||
"tab": "Partajare",
|
||||
|
||||
@@ -5221,7 +5221,8 @@
|
||||
"readWrite": "Чтение и запись",
|
||||
"manager": "Управляющий",
|
||||
"custom": "Пользовательский"
|
||||
}
|
||||
},
|
||||
"access_for": "Доступ для {name}"
|
||||
},
|
||||
"delegates": {
|
||||
"tab": "Общий доступ",
|
||||
|
||||
@@ -4781,7 +4781,8 @@
|
||||
"readWrite": "Čítanie a zápis",
|
||||
"manager": "Správca",
|
||||
"custom": "Vlastné"
|
||||
}
|
||||
},
|
||||
"access_for": "Prístup pre {name}"
|
||||
},
|
||||
"delegates": {
|
||||
"tab": "Zdieľanie",
|
||||
|
||||
@@ -2137,7 +2137,8 @@
|
||||
"readWrite": "อ่านและเขียน",
|
||||
"manager": "ผู้จัดการ (จัดการสิทธิ์ได้)",
|
||||
"custom": "กำหนดเอง"
|
||||
}
|
||||
},
|
||||
"access_for": "สิทธิ์การเข้าถึงของ {name}"
|
||||
},
|
||||
"delegates": {
|
||||
"tab": "การแชร์",
|
||||
|
||||
@@ -4781,7 +4781,8 @@
|
||||
"readWrite": "Okuma ve yazma",
|
||||
"manager": "Yönetici",
|
||||
"custom": "Özel"
|
||||
}
|
||||
},
|
||||
"access_for": "{name} için erişim"
|
||||
},
|
||||
"delegates": {
|
||||
"tab": "Paylaşım",
|
||||
|
||||
@@ -5221,7 +5221,8 @@
|
||||
"readWrite": "Читання та запис",
|
||||
"manager": "Керівник",
|
||||
"custom": "Власне"
|
||||
}
|
||||
},
|
||||
"access_for": "Доступ для {name}"
|
||||
},
|
||||
"delegates": {
|
||||
"tab": "Спільний доступ",
|
||||
|
||||
@@ -4781,7 +4781,8 @@
|
||||
"readWrite": "讀取與寫入",
|
||||
"manager": "管理員",
|
||||
"custom": "自訂"
|
||||
}
|
||||
},
|
||||
"access_for": "{name} 的存取權限"
|
||||
},
|
||||
"delegates": {
|
||||
"tab": "共用",
|
||||
|
||||
@@ -5221,7 +5221,8 @@
|
||||
"readWrite": "读写",
|
||||
"manager": "管理员",
|
||||
"custom": "自定义"
|
||||
}
|
||||
},
|
||||
"access_for": "{name} 的访问权限"
|
||||
},
|
||||
"delegates": {
|
||||
"tab": "共享",
|
||||
|
||||
Reference in New Issue
Block a user