[#82] added cozy-ui for internationnalisation (#267)

Co-authored-by: Camille Moussu <cmoussu@linagora.com>
This commit is contained in:
Camille Moussu
2025-11-06 15:46:58 +01:00
committed by GitHub
parent c1f37e3ebd
commit 36bc027d6d
49 changed files with 12003 additions and 5002 deletions
+9 -2
View File
@@ -3,10 +3,13 @@ import { Box, IconButton, TextField } from "@mui/material";
import { useState } from "react";
import { Calendars } from "../../features/Calendars/CalendarTypes";
import { SnackbarAlert } from "../Loading/SnackBarAlert";
import { useI18n } from "cozy-ui/transpiled/react/providers/I18n";
export function AccessTab({ calendar }: { calendar: Calendars }) {
const { t } = useI18n();
const calDAVLink = `${(window as any).CALENDAR_BASE_URL}${calendar.link.replace(".json", "")}`;
const [open, setOpen] = useState(false);
const handleCopy = () => {
navigator.clipboard.writeText(calDAVLink);
setOpen(true);
@@ -18,7 +21,7 @@ export function AccessTab({ calendar }: { calendar: Calendars }) {
<TextField
disabled
fullWidth
label="CalDAV access"
label={t("calendar.caldav_access")}
value={calDAVLink}
slotProps={{
input: {
@@ -31,7 +34,11 @@ export function AccessTab({ calendar }: { calendar: Calendars }) {
}}
/>
</Box>
<SnackbarAlert setOpen={setOpen} open={open} message="Link copied!" />
<SnackbarAlert
setOpen={setOpen}
open={open}
message={t("common.link_copied")}
/>
</>
);
}
+10 -4
View File
@@ -42,6 +42,7 @@ import { MiniCalendar } from "./MiniCalendar";
import { User } from "../Attendees/PeopleSearch";
import { useTheme } from "@mui/material/styles";
import { updateDarkColor } from "./utils/calendarColorsUtils";
import { useI18n } from "cozy-ui/transpiled/react/providers/I18n";
interface CalendarAppProps {
calendarRef: React.RefObject<CalendarApi | null>;
@@ -286,6 +287,8 @@ export default function CalendarApp({
(window as any).__calendarRef = calendarRef;
}
const { t, lang } = useI18n();
return (
<main className="main-layout">
<Box
@@ -312,7 +315,7 @@ export default function CalendarApp({
eventHandlers.handleDateSelect(null as unknown as DateSelectArg)
}
>
<AddIcon /> <p>Create Event</p>
<AddIcon /> <p>{t("event.createEvent")}</p>
</Button>
</Box>
@@ -356,6 +359,7 @@ export default function CalendarApp({
initialView="timeGridWeek"
firstDay={1}
editable={true}
locale={lang}
selectable={true}
timeZone={timezone}
height={"100%"}
@@ -382,7 +386,9 @@ export default function CalendarApp({
weekNumberContent={(arg) => {
return (
<div className="weekSelector">
<div>{arg.text}</div>
<div>
{t("menubar.views.week")} {arg.num}
</div>
<TimezoneSelector
value={timezone}
onChange={(newTimezone: string) =>
@@ -393,7 +399,7 @@ export default function CalendarApp({
);
}}
dayCellContent={(arg) => {
const month = arg.date.toLocaleDateString("en-US", {
const month = arg.date.toLocaleDateString(t("locale"), {
month: "short",
});
if (arg.view.type === "dayGridMonth") {
@@ -453,7 +459,7 @@ export default function CalendarApp({
dayHeaderContent={(arg) => {
const date = arg.date.getDate();
const weekDay = arg.date
.toLocaleDateString("en-US", { weekday: "short" })
.toLocaleDateString(t("locale"), { weekday: "short" })
.toUpperCase();
return (
<div className="fc-daygrid-day-top">
@@ -10,6 +10,7 @@ import {
} from "@mui/material";
import { useState } from "react";
import { HexColorPicker } from "react-colorful";
import { useI18n } from "cozy-ui/transpiled/react/providers/I18n";
import { defaultColors, getAccessiblePair } from "./utils/calendarColorsUtils";
export function ColorPicker({
@@ -54,6 +55,7 @@ export function ColorPicker({
</Box>
);
}
function ColorBox({
color,
onChange,
@@ -107,11 +109,11 @@ function ColorPickerBox({
onChange: (color: Record<string, string>) => void;
selectedColor: Record<string, string>;
}) {
const { t } = useI18n();
const [oldColor] = useState(
selectedColor ?? { light: "#ffffff", dark: "#808080" }
);
const [color, setColor] = useState(oldColor);
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
const open = Boolean(anchorEl);
const theme = useTheme();
@@ -139,12 +141,13 @@ function ColorPickerBox({
setColor(newColor);
onChange(newColor);
};
return (
<>
<Box
key={"colorPicker"}
role="button"
aria-label={`select custom color`}
aria-label={t("colorPicker.selectCustom")}
onClick={handleClick}
style={{
width: "46px",
@@ -198,10 +201,10 @@ function ColorPickerBox({
}}
>
<Typography variant="subtitle1" fontWeight="600">
Choose custom colour
{t("colorPicker.title")}
</Typography>
<Typography variant="body2" sx={{ mb: 2, color: "text.secondary" }}>
Choose a background colour for this calendar
{t("colorPicker.subtitle")}
</Typography>
<Box sx={{ mb: 2 }}>
@@ -214,7 +217,7 @@ function ColorPickerBox({
<Box sx={{ display: "flex", alignItems: "center", mb: 2 }}>
<Typography variant="body2" sx={{ mr: 1 }}>
Hex
{t("colorPicker.hex")}
</Typography>
<TextField
value={color.light?.toUpperCase()}
@@ -226,13 +229,13 @@ function ColorPickerBox({
</Box>
<Box sx={{ display: "flex", justifyContent: "flex-end", gap: 1 }}>
<Button onClick={handleClose}>Cancel</Button>
<Button onClick={handleClose}>{t("common.cancel")}</Button>
<Button
variant="contained"
onClick={handleSave}
sx={{ textTransform: "none" }}
>
Save
{t("actions.save")}
</Button>
</Box>
</Popover>
+2 -2
View File
@@ -4,9 +4,9 @@ import { Calendars } from "../../features/Calendars/CalendarTypes";
import { CalendarName } from "./CalendarName";
export function CalendarItemList(
userPersonnalCalendars: Calendars[]
userPersonalCalendars: Calendars[]
): React.ReactNode {
return Object.values(userPersonnalCalendars).map((calendar) => (
return Object.values(userPersonalCalendars).map((calendar) => (
<MenuItem key={calendar.id} value={calendar.id}>
<CalendarName calendar={calendar} />
</MenuItem>
+19 -5
View File
@@ -13,6 +13,7 @@ import { AccessTab } from "./AccessTab";
import { ImportTab } from "./ImportTab";
import { SettingsTab } from "./SettingsTab";
import { defaultColors } from "./utils/calendarColorsUtils";
import { useI18n } from "cozy-ui/transpiled/react/providers/I18n";
function CalendarPopover({
open,
@@ -26,6 +27,7 @@ function CalendarPopover({
) => void;
calendar?: Calendars;
}) {
const { t } = useI18n();
const dispatch = useAppDispatch();
const userId =
useAppSelector((state) => state.user.userData?.openpaasId) ?? "";
@@ -181,10 +183,18 @@ function CalendarPopover({
<Tabs value={tab} onChange={(e, v) => setTab(v)}>
<Tab
value="settings"
label={calendar ? "Settings" : "Add new calendar"}
label={
calendar
? t("calendarPopover.tabs.settings")
: t("calendarPopover.tabs.addNew")
}
/>
{calendar && <Tab value="access" label="Access" />}
{isOwn && <Tab value="import" label="Import" />}
{calendar && (
<Tab value="access" label={t("calendarPopover.tabs.access")} />
)}
{isOwn && (
<Tab value="import" label={t("calendarPopover.tabs.import")} />
)}
</Tabs>
}
actions={
@@ -193,14 +203,18 @@ function CalendarPopover({
variant="outlined"
onClick={(e) => handleClose({}, "backdropClick")}
>
Cancel
{t("common.cancel")}
</Button>
<Button
disabled={tab === "import" ? !importedContent : !name.trim()}
variant="contained"
onClick={tab === "import" ? handleImport : handleSave}
>
{tab === "import" ? "Import" : calendar ? "Save" : "Create"}
{tab === "import"
? t("actions.import")
: calendar
? t("actions.save")
: t("actions.create")}
</Button>
</DialogActions>
}
+27 -17
View File
@@ -2,23 +2,19 @@ import CloseIcon from "@mui/icons-material/Close";
import Avatar from "@mui/material/Avatar";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import Card from "@mui/material/Card";
import CardActions from "@mui/material/CardActions";
import CardContent from "@mui/material/CardContent";
import CardHeader from "@mui/material/CardHeader";
import IconButton from "@mui/material/IconButton";
import Modal from "@mui/material/Modal";
import { useTheme } from "@mui/material/styles";
import Typography from "@mui/material/Typography";
import { useI18n } from "cozy-ui/transpiled/react/providers/I18n";
import { useState } from "react";
import { useAppDispatch, useAppSelector } from "../../app/hooks";
import { getCalendars } from "../../features/Calendars/CalendarApi";
import { addSharedCalendarAsync } from "../../features/Calendars/CalendarSlice";
import { ColorPicker } from "./CalendarColorPicker";
import { Calendars } from "../../features/Calendars/CalendarTypes";
import { User, PeopleSearch } from "../Attendees/PeopleSearch";
import { defaultColors, getAccessiblePair } from "./utils/calendarColorsUtils";
import { useTheme } from "@mui/material/styles";
import { PeopleSearch, User } from "../Attendees/PeopleSearch";
import { ResponsiveDialog } from "../Dialog";
import { ColorPicker } from "./CalendarColorPicker";
import { defaultColors, getAccessiblePair } from "./utils/calendarColorsUtils";
interface CalendarWithOwner {
cal: Record<string, any>;
@@ -104,6 +100,7 @@ function SelectedCalendarsList({
color: Record<string, string>
) => void;
}) {
const { t } = useI18n();
if (selectedCal.length === 0) return null;
const groupedByOwner = selectedCal.reduce<
@@ -144,7 +141,7 @@ function SelectedCalendarsList({
return (
<Box mt={2}>
<Typography variant="subtitle1" gutterBottom>
Name
{t("common.name")}
</Typography>
{Object.values(groupedByOwner).map(
@@ -160,14 +157,26 @@ function SelectedCalendarsList({
onColorChange={(color) => onColorChange(cal, color)}
/>
) : (
<Typography color="textSecondary">
No publicly available calendars for {owner.displayName}
<Typography
key={t("calendar.noPublicCalendarsFor", {
name: owner.displayName,
})}
color="textSecondary"
>
{t("calendar.noPublicCalendarsFor", {
name: owner.displayName,
})}
</Typography>
)
)
) : alreadyExisting ? (
<Typography color="textSecondary">
No more Calendar for {owner.displayName}
<Typography
key={t("calendar.noMoreCalendarsFor", {
name: owner.displayName,
})}
color="textSecondary"
>
{t("calendar.noMoreCalendarsFor", { name: owner.displayName })}
</Typography>
) : null}
</Box>
@@ -230,6 +239,7 @@ export default function CalendarSearch({
setSelectedUsers([]);
}
};
const { t } = useI18n();
return (
<ResponsiveDialog
@@ -240,17 +250,17 @@ export default function CalendarSearch({
setSelectedCalendars([]);
setSelectedUsers([]);
}}
title="Browse other calendars"
title={t("calendar.browseOtherCalendars")}
actions={
<>
<Button
variant="outlined"
onClick={() => onClose({}, "backdropClick")}
>
Cancel
{t("common.cancel")}
</Button>
<Button variant="contained" onClick={handleSave}>
Add
{t("actions.add")}
</Button>
</>
}
+17 -18
View File
@@ -16,6 +16,7 @@ import { Divider, ListItem, Menu, MenuItem } from "@mui/material";
import { removeCalendarAsync } from "../../features/Calendars/CalendarSlice";
import { DeleteCalendarDialog } from "./DeleteCalendarDialog";
import { trimLongTextWithoutSpace } from "../../utils/textUtils";
import { useI18n } from "cozy-ui/transpiled/react/providers/I18n";
function CalendarAccordion({
title,
@@ -80,7 +81,7 @@ function CalendarAccordion({
key={id}
calendars={allCalendars}
id={id}
isPersonnal={defaultExpanded}
isPersonal={defaultExpanded}
selectedCalendars={selectedCalendars}
handleCalendarToggle={handleToggle}
setOpen={() => setOpen(id)}
@@ -98,11 +99,12 @@ export default function CalendarSelection({
selectedCalendars: string[];
setSelectedCalendars: Function;
}) {
const { t } = useI18n();
const userId =
useAppSelector((state) => state.user.userData?.openpaasId) ?? "";
const calendars = useAppSelector((state) => state.calendars.list);
const personnalCalendars = Object.keys(calendars).filter(
const personalCalendars = Object.keys(calendars).filter(
(id) => id.split("/")[0] === userId
);
const delegatedCalendars = Object.keys(calendars).filter(
@@ -127,8 +129,8 @@ export default function CalendarSelection({
<>
<div>
<CalendarAccordion
title="Personnal Calendars"
calendars={personnalCalendars}
title={t("calendar.personal")}
calendars={personalCalendars}
selectedCalendars={selectedCalendars}
handleToggle={handleCalendarToggle}
showAddButton
@@ -141,7 +143,7 @@ export default function CalendarSelection({
/>
<CalendarAccordion
title="Delegated Calendars"
title={t("calendar.delegated")}
calendars={delegatedCalendars}
selectedCalendars={selectedCalendars}
handleToggle={handleCalendarToggle}
@@ -155,7 +157,7 @@ export default function CalendarSelection({
/>
<CalendarAccordion
title="Other Calendars"
title={t("calendar.other")}
calendars={sharedCalendars}
selectedCalendars={selectedCalendars}
showAddButton
@@ -197,18 +199,19 @@ export default function CalendarSelection({
function CalendarSelector({
calendars,
id,
isPersonnal,
isPersonal,
selectedCalendars,
handleCalendarToggle,
setOpen,
}: {
calendars: Record<string, Calendars>;
id: string;
isPersonnal: boolean;
isPersonal: boolean;
selectedCalendars: string[];
handleCalendarToggle: (name: string) => void;
setOpen: Function;
}) {
const { t } = useI18n();
const dispatch = useAppDispatch();
const calLink =
useAppSelector((state) => state.calendars.list[id].link) ?? "";
@@ -222,7 +225,7 @@ function CalendarSelector({
setAnchorEl(null);
};
const [userId, calId] = id.split("/");
const isDefault = isPersonnal && userId === calId;
const isDefault = isPersonal && userId === calId;
const [deletePopupOpen, setDeletePopupOpen] = useState(false);
const handleDeleteConfirm = () => {
@@ -244,14 +247,10 @@ function CalendarSelector({
justifyContent: "space-between",
transition: "background-color 0.2s ease",
padding: "8px 24px 8px 16px",
"& .MoreBtn": {
opacity: 0,
},
"& .MoreBtn": { opacity: 0 },
"&:hover": {
backgroundColor: "#F3F3F6",
"& .MoreBtn": {
opacity: 1,
},
"& .MoreBtn": { opacity: 1 },
},
}}
>
@@ -293,12 +292,12 @@ function CalendarSelector({
handleClose();
}}
>
Modify
{t("actions.modify")}
</MenuItem>
{!isDefault && <Divider />}
{!isDefault && (
<MenuItem onClick={() => setDeletePopupOpen(!deletePopupOpen)}>
{isPersonnal ? "Delete" : "Remove"}
{isPersonal ? t("actions.delete") : t("actions.remove")}
</MenuItem>
)}
</Menu>
@@ -308,7 +307,7 @@ function CalendarSelector({
setDeletePopupOpen={setDeletePopupOpen}
calendars={calendars}
id={id}
isPersonnal={isPersonnal}
isPersonal={isPersonal}
handleDeleteConfirm={handleDeleteConfirm}
/>
</>
@@ -5,37 +5,45 @@ import DialogContent from "@mui/material/DialogContent";
import DialogContentText from "@mui/material/DialogContentText";
import DialogActions from "@mui/material/DialogActions";
import Button from "@mui/material/Button";
import { useI18n } from "cozy-ui/transpiled/react/providers/I18n";
export function DeleteCalendarDialog({
deletePopupOpen,
setDeletePopupOpen,
calendars,
id,
isPersonnal,
isPersonal,
handleDeleteConfirm,
}: {
deletePopupOpen: boolean;
setDeletePopupOpen: (e: boolean) => void;
calendars: Record<string, Calendars>;
id: string;
isPersonnal: boolean;
isPersonal: boolean;
handleDeleteConfirm: () => void;
}) {
const { t } = useI18n();
return (
<Dialog open={deletePopupOpen} onClose={() => setDeletePopupOpen(false)}>
<DialogTitle>Remove {calendars[id].name}?</DialogTitle>
<DialogTitle>
{t("calendar.delete.title", { name: calendars[id].name })}
</DialogTitle>
<DialogContent>
<DialogContentText>
Are you sure you want to remove this calendar?{" "}
{isPersonnal
? "You will loose all events in this calendar."
: "You will loose access to its events. You will still be able to add it back later."}
{isPersonal
? t("calendar.delete.personalWarning")
: t("calendar.delete.sharedWarning")}
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={() => setDeletePopupOpen(false)}>Cancel</Button>
<Button onClick={() => setDeletePopupOpen(false)}>
{t("common.cancel")}
</Button>
<Button onClick={handleDeleteConfirm} variant="contained">
{isPersonnal ? "Delete" : "Remove"}
{isPersonal ? t("actions.delete") : t("actions.remove")}
</Button>
</DialogActions>
</Dialog>
+12 -10
View File
@@ -14,6 +14,7 @@ import { useEffect, useState } from "react";
import { useAppSelector } from "../../app/hooks";
import { CalendarItemList } from "./CalendarItemList";
import { SettingsTab } from "./SettingsTab";
import { useI18n } from "cozy-ui/transpiled/react/providers/I18n";
export function ImportTab({
userId,
@@ -37,11 +38,12 @@ export function ImportTab({
setVisibility: Function;
};
}) {
const { t } = useI18n();
const [importMode, setImportMode] = useState<"file" | "url">("file");
const [importFile, setImportFile] = useState<File | null>(null);
const [importUrl, setImportUrl] = useState("");
const calendars = useAppSelector((state) => state.calendars.list);
const personnalCalendars = Object.values(calendars).filter(
const personalCalendars = Object.values(calendars).filter(
(cal) => cal.id.split("/")[0] === userId
);
@@ -59,14 +61,14 @@ export function ImportTab({
size="small"
sx={{ mb: 2 }}
>
<ToggleButton value="file">File</ToggleButton>
{/* <ToggleButton value="url">URL</ToggleButton> */}
<ToggleButton value="file">{t("common.import_file")}</ToggleButton>
{/* <ToggleButton value="url">{t("common.import_url")}</ToggleButton> */}
</ToggleButtonGroup>
{importMode === "file" && (
<>
<Button variant="outlined" component="label" sx={{ mb: 1 }}>
Select file
{t("common.select_file")}
<input
type="file"
hidden
@@ -85,7 +87,7 @@ export function ImportTab({
display="block"
mb={2}
>
Import events from an ICS file to one of your calendars.
{t("calendar.import_file_description")}
</Typography>
</>
)}
@@ -93,7 +95,7 @@ export function ImportTab({
{importMode === "url" && (
<TextField
fullWidth
label="ICS feed URL"
label={t("calendar.ics_feed_url")}
value={importUrl}
onChange={(e) => setImportUrl(e.target.value)}
size="small"
@@ -102,15 +104,15 @@ export function ImportTab({
)}
<FormControl fullWidth size="small" sx={{ mt: 2 }}>
<InputLabel id="import-to-label">Import to</InputLabel>
<InputLabel id="import-to-label">{t("calendar.import_to")}</InputLabel>
<Select
labelId="import-to-label"
label="Import to"
label={t("calendar.import_to")}
value={importTarget}
onChange={(e) => setImportTarget(e.target.value)}
>
<MenuItem value="new">New calendar</MenuItem>
{CalendarItemList(personnalCalendars)}
<MenuItem value="new">{t("calendar.new_calendar")}</MenuItem>
{CalendarItemList(personalCalendars)}
</Select>
</FormControl>
+6 -1
View File
@@ -12,6 +12,7 @@ import { useAppDispatch, useAppSelector } from "../../app/hooks";
import { refreshCalendars } from "../Event/utils/eventUtils";
import { getCalendarDetailAsync } from "../../features/Calendars/CalendarSlice";
import { useEffect, useState } from "react";
import { useI18n } from "cozy-ui/transpiled/react/providers/I18n";
export function MiniCalendar({
calendarRef,
@@ -29,10 +30,14 @@ export function MiniCalendar({
const dispatch = useAppDispatch();
const calendars = useAppSelector((state) => state.calendars);
const [visibleDate, setVisibleDate] = useState(selectedDate);
const { t } = useI18n();
useEffect(() => setVisibleDate(selectedDate), [selectedDate]);
return (
<LocalizationProvider dateAdapter={AdapterMoment} adapterLocale="en-gb">
<LocalizationProvider
dateAdapter={AdapterMoment}
adapterLocale={t("locale") ?? "en-gb"}
>
<DateCalendar
value={moment(visibleDate)}
onChange={(dateMoment) => {
+14 -8
View File
@@ -9,7 +9,8 @@ import {
ToggleButtonGroup,
Typography,
} from "@mui/material";
import { useState } from "react";
import { useI18n } from "cozy-ui/transpiled/react/providers/I18n";
import { useState, useEffect } from "react";
import { useAppSelector } from "../../app/hooks";
import { Calendars } from "../../features/Calendars/CalendarTypes";
import { ColorPicker } from "./CalendarColorPicker";
@@ -35,16 +36,21 @@ export function SettingsTab({
setVisibility: Function;
calendar?: Calendars;
}) {
const { t } = useI18n();
const [toggleDesc, setToggleDesc] = useState(Boolean(description));
const userId =
useAppSelector((state) => state.user.userData?.openpaasId) ?? "";
const isOwn = calendar ? calendar.id.split("/")[0] === userId : true;
useEffect(() => {
if (description) setToggleDesc(true);
}, [description]);
return (
<Box mt={2}>
<TextField
fullWidth
label="Name"
label={t("common.name")}
value={name}
onChange={(e) => setName(e.target.value)}
size="small"
@@ -58,14 +64,14 @@ export function SettingsTab({
onClick={() => setToggleDesc(!toggleDesc)}
startIcon={<FormatListBulletedIcon />}
>
Add description
{t("calendar.addDescription")}
</Button>
)}
{toggleDesc && (
<TextField
fullWidth
label="Description"
label={t("common.description")}
value={description}
onChange={(e) => setDescription(e.target.value)}
size="small"
@@ -77,7 +83,7 @@ export function SettingsTab({
<Box mt={2}>
<Typography variant="body2" gutterBottom>
Color
{t("calendar.color")}
</Typography>
<ColorPicker
onChange={(color) => setColor(color)}
@@ -88,7 +94,7 @@ export function SettingsTab({
{isOwn && (
<Box mt={2}>
<Typography variant="body2" gutterBottom>
New events created will be visible to:
{t("calendar.newEventsVisibility")}
</Typography>
<ToggleButtonGroup
value={visibility}
@@ -98,12 +104,12 @@ export function SettingsTab({
>
<ToggleButton value="public">
<PublicIcon fontSize="small" />
All
{t("common.all")}
</ToggleButton>
<ToggleButton value="private">
<LockIcon fontSize="small" />
You
{t("common.you")}
</ToggleButton>
</ToggleButtonGroup>
</Box>
+3 -2
View File
@@ -1,4 +1,5 @@
import { Button, Popover } from "@mui/material";
import { useI18n } from "cozy-ui/transpiled/react/providers/I18n";
import { MouseEvent, useMemo, useState } from "react";
import { TIMEZONES } from "../../utils/timezone-data";
import { TimezoneAutocomplete } from "../Timezone/TimezoneAutocomplete";
@@ -34,7 +35,7 @@ export function TimezoneSelector({ value, onChange }: TimezoneSelectProps) {
};
const open = Boolean(anchorEl);
const { t } = useI18n();
return (
<>
<Button
@@ -49,7 +50,7 @@ export function TimezoneSelector({ value, onChange }: TimezoneSelectProps) {
lineHeight: 1.2,
}}
>
{selectedOffset || "Select Timezone"}
{selectedOffset || t("common.select_timezone")}
</Button>
<Popover