diff --git a/__test__/features/Calendars/CalendarAccessRights.test.tsx b/__test__/features/Calendars/CalendarAccessRights.test.tsx index dcee911..6e86a45 100644 --- a/__test__/features/Calendars/CalendarAccessRights.test.tsx +++ b/__test__/features/Calendars/CalendarAccessRights.test.tsx @@ -87,6 +87,8 @@ const baseCalendar: Calendar = { events: {}, invite: [], owner: { emails: ["user1@example.com"] }, + access: { write: true }, + delegated: true, }; describe("CalendarAccessRights", () => { @@ -102,7 +104,8 @@ describe("CalendarAccessRights", () => { value={[]} onChange={mockOnChange} onInvitesLoaded={mockOnInvitesLoaded} - /> + />, + { ...userState } ); expect( @@ -113,7 +116,7 @@ describe("CalendarAccessRights", () => { it("shows a list of users already passed in via value prop", () => { const users: UserWithAccess[] = [ { - openpaasId: "abc", + openpaasId: "user1", displayName: "Alice", email: "alice@example.com", accessRight: 2, @@ -136,10 +139,10 @@ describe("CalendarAccessRights", () => { it("calls onChange with user removed when remove button is clicked", () => { const users: UserWithAccess[] = [ { - openpaasId: "abc", + openpaasId: "user1", displayName: "Alice", email: "alice@example.com", - accessRight: 2, + accessRight: 5, }, ]; @@ -149,7 +152,8 @@ describe("CalendarAccessRights", () => { value={users} onChange={mockOnChange} onInvitesLoaded={mockOnInvitesLoaded} - /> + />, + { ...userState } ); fireEvent.click(screen.getByLabelText(/remove/i)); @@ -182,7 +186,8 @@ describe("CalendarAccessRights", () => { value={[]} onChange={mockOnChange} onInvitesLoaded={mockOnInvitesLoaded} - /> + />, + { ...userState } ); await waitFor(() => expect(getUserDetails).toHaveBeenCalledWith("bob123")); @@ -219,7 +224,8 @@ describe("CalendarAccessRights", () => { value={[]} onChange={mockOnChange} onInvitesLoaded={mockOnInvitesLoaded} - /> + />, + { ...userState } ); await waitFor(() => expect(mockOnInvitesLoaded).toHaveBeenCalledWith([])); @@ -249,7 +255,8 @@ describe("CalendarAccessRights", () => { value={[]} onChange={mockOnChange} onInvitesLoaded={mockOnInvitesLoaded} - /> + />, + { ...userState } ); expect(document.querySelector('[role="progressbar"]')).toBeInTheDocument(); @@ -286,7 +293,7 @@ describe("AccessTab – conditional rendering of CalendarAccessRights", () => { ).toBeInTheDocument(); }); - it("hides CalendarAccessRights for a non-owner without admin delegation", () => { + it("hides CalendarAccessRights textfield for a non-owner without admin delegation", () => { const foreignCalendar: Calendar = { ...baseCalendar, id: "otherUser/cal1", @@ -306,9 +313,10 @@ describe("AccessTab – conditional rendering of CalendarAccessRights", () => { } ); + expect(screen.queryByText("peopleSearch.label")).not.toBeInTheDocument(); expect( - screen.queryByText("calendarPopover.access.grantAccessRights") - ).not.toBeInTheDocument(); + screen.getByText("calendarPopover.access.accessRights") + ).toBeInTheDocument(); }); it("shows CalendarAccessRights when the user has access=5 (admin) delegation", () => { diff --git a/src/components/Calendar/AccessTab.tsx b/src/components/Calendar/AccessTab.tsx index 7f35cb1..3c6cbe4 100644 --- a/src/components/Calendar/AccessTab.tsx +++ b/src/components/Calendar/AccessTab.tsx @@ -1,4 +1,3 @@ -import { useAppSelector } from "@/app/hooks"; import { exportCalendar, getSecretLink, @@ -36,11 +35,6 @@ export function AccessTab({ onInvitesLoaded, }: AccessTabProps) { const { t } = useI18n(); - const userData = useAppSelector((state) => state.user.userData); - const isPersonalCalendar = userData.openpaasId === calendar.id.split("/")[0]; - const isDelegatedWithAdministration = calendar.invite?.find( - (invite) => invite.href.includes(userData.email) && invite.access === 5 - ); const calDAVLink = `${window.DAV_BASE_URL}${calendar.link.replace(".json", "")}`; @@ -98,14 +92,12 @@ export function AccessTab({ return ( <> - {(isPersonalCalendar || isDelegatedWithAdministration) && ( - - )} + {!!window.DAV_BASE_URL && ( diff --git a/src/components/Calendar/CalendarAccessRights.tsx b/src/components/Calendar/CalendarAccessRights.tsx index ccf6897..d37fbe2 100644 --- a/src/components/Calendar/CalendarAccessRights.tsx +++ b/src/components/Calendar/CalendarAccessRights.tsx @@ -1,5 +1,8 @@ +import { useAppSelector } from "@/app/hooks"; import { AccessRight, Calendar } from "@/features/Calendars/CalendarTypes"; import { getUserDetails } from "@/features/User/userAPI"; +import { makeDisplayName } from "@/utils/makeDisplayName"; +import { normalizeEmail } from "@/utils/normalizeEmail"; import { AutocompleteRenderInputParams, Avatar, @@ -38,6 +41,17 @@ export function CalendarAccessRights({ onInvitesLoaded, }: CalendarAccessRightsProps) { const { t } = useI18n(); + const userData = useAppSelector((state) => state.user.userData); + const isPersonalCalendar = userData?.openpaasId === calendar.id.split("/")[0]; + const currentUserEmail = normalizeEmail(userData?.email); + const isDelegatedWithAdministration = !!calendar.invite?.some((invite) => { + const invitedEmail = normalizeEmail(invite.href.replace(/^mailto:/i, "")); + return invitedEmail === currentUserEmail && invite.access === 5; + }); + + const ownerEmail = + calendar.owner?.preferredEmail ?? calendar.owner?.emails?.[0] ?? ""; + const ownerName = makeDisplayName(calendar) ?? ownerEmail; const containerRef = useRef(null); const [searchWidth, setSearchWidth] = useState(undefined); @@ -115,8 +129,6 @@ export function CalendarAccessRights({ }; }, [calendar.invite, onChange, onInvitesLoaded]); - const normalizeEmail = (email?: string) => email?.trim().toLowerCase() ?? ""; - const handleUserSelect = (_event: unknown, users: User[]) => { const updated: UserWithAccess[] = users.map((user) => { const existing = usersWithAccess.find( @@ -153,188 +165,217 @@ export function CalendarAccessRights({ return ( - - {}} - customSlotProps={{ - popper: { - anchorEl: containerRef.current, - placement: "bottom-start", - sx: { - minWidth: searchWidth, - "& .MuiPaper-root": { - width: "100%", - }, - }, - modifiers: [ - { - name: "offset", - options: { - offset: [0, 8], + {(isPersonalCalendar || isDelegatedWithAdministration) && ( + + {}} + customSlotProps={{ + popper: { + anchorEl: containerRef.current, + placement: "bottom-start", + sx: { + minWidth: searchWidth, + "& .MuiPaper-root": { + width: "100%", }, }, - ], - }, - }} - customRenderInput={( - params: AutocompleteRenderInputParams, - query: string, - setQuery: (value: string) => void - ) => ( - { - const ref = params.InputProps.ref; - if (typeof ref === "function") { - ref(el); - } else if (ref && "current" in ref) { - ( - ref as React.MutableRefObject - ).current = el; - } - }} - onChange={(e) => setQuery(e.target.value)} - variant="outlined" - inputProps={{ - ...params.inputProps, - sx: { - fontSize: "14px", - "&::placeholder": { fontSize: "14px" }, - }, - }} - InputProps={{ - ...params.InputProps, - startAdornment: ( - - - - ), - endAdornment: ( - - - - ), - }} - /> - )} - /> - - - {inviteLoading ? ( - - - - ) : ( - usersWithAccess.length > 0 && ( - - {usersWithAccess.map((user) => ( - void + ) => ( + { + const ref = params.InputProps.ref; + if (typeof ref === "function") { + ref(el); + } else if (ref && "current" in ref) { + ( + ref as React.MutableRefObject + ).current = el; + } }} - > - - - {user.displayName?.[0]?.toUpperCase()} - - - {user.displayName} - - {user.email} - - - - - - + setAccessRight(e.target.value as AccessRight) + } + variant="standard" + disableUnderline + sx={{ + fontSize: "0.875rem", + color: "text.secondary", + "& .MuiSelect-select": { + paddingRight: "24px !important", + paddingY: 0, + }, + "& .MuiSelect-icon": { fontSize: "1rem" }, + "&:before, &:after": { display: "none" }, + }} > - {opt.label} - - ))} - + {accessRightOptions.map((opt) => ( + + {opt.label} + + ))} + + + ), + }} + /> + )} + /> + + )} + + + + + + + {ownerName} + + {ownerEmail} + + + + + + + {t("calendarPopover.access.owner")} + + + + {inviteLoading ? ( + + + + ) : ( + usersWithAccess.length > 0 && + usersWithAccess.map((user) => ( + + + + + {user.displayName} + + {user.email} + + + + + + + {(isPersonalCalendar || isDelegatedWithAdministration) && ( - + )} - ))} - - ) - )} + + )) + )} + ); } diff --git a/src/components/Calendar/CalendarModal.tsx b/src/components/Calendar/CalendarModal.tsx index cb3b201..79d271b 100644 --- a/src/components/Calendar/CalendarModal.tsx +++ b/src/components/Calendar/CalendarModal.tsx @@ -39,6 +39,16 @@ function CalendarPopover({ const isOwn = calendar?.id ? extractEventBaseUuid(calendar.id) === userData.openpaasId : true; + const canManageInvites = + isOwn || + !!calendar?.invite?.some((invite) => { + const inviteEmail = invite.href + .replace(/^mailto:/i, "") + .trim() + .toLowerCase(); + const currentEmail = userData.email?.trim().toLowerCase(); + return inviteEmail === currentEmail && invite.access === 5; + }); // existing calendar params const [name, setName] = useState(""); @@ -146,7 +156,7 @@ function CalendarPopover({ .filter((u) => !!normaliseEmail(u) && !currentMap.has(normaliseEmail(u))) .map((u) => ({ "dav:href": `mailto:${normaliseEmail(u)}` })); - if (set.length === 0 && remove.length === 0) return; + if ((set.length === 0 && remove.length === 0) || !canManageInvites) return; await dispatch( updateDelegationCalendarAsync({ diff --git a/src/locales/en.json b/src/locales/en.json index 2b21b2a..6e4da30 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -84,8 +84,10 @@ }, "access": { "grantAccessRights": "Grant Access rights", + "accessRights": "Access rights", "viewAllEvents": "View all events", "editor": "Editor", + "owner": "Owner", "administrator": "Administrator" } }, diff --git a/src/locales/fr.json b/src/locales/fr.json index 15c5968..57772a7 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -86,6 +86,7 @@ }, "access": { "grantAccessRights": "Accorder des droits d'accès", + "accessRights": "Droits d'accès", "viewAllEvents": "Voir tous les événements", "editor": "Éditeur", "administrator": "Administrateur" diff --git a/src/locales/ru.json b/src/locales/ru.json index 3c9a335..5e68d79 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -86,6 +86,7 @@ }, "access": { "grantAccessRights": "Предоставить права доступа", + "accessRights": "Права доступа", "viewAllEvents": "Просмотр всех событий", "editor": "Редактор", "administrator": "Администратор" diff --git a/src/locales/vi.json b/src/locales/vi.json index c1b07e5..5592656 100644 --- a/src/locales/vi.json +++ b/src/locales/vi.json @@ -84,6 +84,7 @@ }, "access": { "grantAccessRights": "Cấp quyền truy cập", + "accessRights": "Quyền truy cập", "viewAllEvents": "Xem tất cả sự kiện", "editor": "Biên tập viên", "administrator": "Quản trị viên" diff --git a/src/utils/normalizeEmail.tsx b/src/utils/normalizeEmail.tsx new file mode 100644 index 0000000..18f35f6 --- /dev/null +++ b/src/utils/normalizeEmail.tsx @@ -0,0 +1,3 @@ +export function normalizeEmail(email?: string) { + return email?.trim().toLowerCase() ?? ""; +}