@edifice.io/react 2.6.7-develop-b2school.20261009142401 → 2.6.7-develop-b2school.20261009185039

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/components/Layout/components/Header.js +4 -4
  2. package/dist/components/Layout/components/NavLink.d.ts +3 -3
  3. package/dist/components/Layout/components/NavLink.js +5 -3
  4. package/dist/homepage.js +6 -0
  5. package/dist/modules/homepage/components/Agenda/Agenda.d.ts +10 -0
  6. package/dist/modules/homepage/components/Agenda/Agenda.js +54 -0
  7. package/dist/modules/homepage/components/Agenda/AgendaContainer.d.ts +1 -0
  8. package/dist/modules/homepage/components/Agenda/AgendaContainer.js +14 -0
  9. package/dist/modules/homepage/components/Agenda/AgendaEventCard.d.ts +10 -0
  10. package/dist/modules/homepage/components/Agenda/AgendaEventCard.js +30 -0
  11. package/dist/modules/homepage/components/Agenda/hooks/useAgendaContainer.d.ts +14 -0
  12. package/dist/modules/homepage/components/Agenda/hooks/useAgendaContainer.js +27 -0
  13. package/dist/modules/homepage/components/Agenda/index.d.ts +3 -0
  14. package/dist/modules/homepage/components/Agenda/services/api/agendaService.d.ts +24 -0
  15. package/dist/modules/homepage/components/Agenda/services/api/agendaService.js +45 -0
  16. package/dist/modules/homepage/components/Agenda/services/api/index.d.ts +4 -0
  17. package/dist/modules/homepage/components/Agenda/services/api/index.js +5 -0
  18. package/dist/modules/homepage/components/Agenda/services/queries/agenda.d.ts +19 -0
  19. package/dist/modules/homepage/components/Agenda/services/queries/agenda.js +26 -0
  20. package/dist/modules/homepage/components/Communities/Communities.js +2 -2
  21. package/dist/modules/homepage/components/Header/Header.js +27 -11
  22. package/dist/modules/homepage/components/Header/MyAppsPopover.d.ts +5 -0
  23. package/dist/modules/homepage/components/Header/MyAppsPopover.js +28 -0
  24. package/dist/modules/homepage/components/Header/useSafeHoverPopover.js +32 -0
  25. package/dist/modules/homepage/components/HomeCard/HomeCardHeader.d.ts +5 -1
  26. package/dist/modules/homepage/components/HomeCard/HomeCardHeader.js +1 -1
  27. package/dist/modules/homepage/components/MessageFlashList/MessageFlash.js +1 -1
  28. package/dist/modules/homepage/components/Notifications/NotificationList.js +5 -5
  29. package/dist/modules/homepage/components/Notifications/NotificationSkeleton.js +2 -2
  30. package/dist/modules/homepage/components/Notifications/components/NotificationFilterMenu.js +1 -5
  31. package/dist/modules/homepage/components/Notifications/components/NotificationFilterModal.js +10 -12
  32. package/dist/modules/homepage/components/Notifications/components/NotificationOptionsMenu.js +3 -9
  33. package/dist/modules/homepage/components/Notifications/components/notificationAdapter.js +12 -10
  34. package/dist/modules/homepage/components/Notifications/hooks/useNotificationList.js +9 -10
  35. package/dist/modules/homepage/components/UserSpace/UserSpace.js +1 -1
  36. package/dist/modules/homepage/components/index.d.ts +1 -0
  37. package/package.json +6 -6
@@ -138,7 +138,7 @@ const Header = ({
138
138
  /* @__PURE__ */ jsxs("ul", { className: "navbar-nav", children: [
139
139
  /* @__PURE__ */ jsx(NavItem, { children: /* @__PURE__ */ jsx(NavLink, { link: "/timeline/timeline", translate: t("navbar.home"), children: /* @__PURE__ */ jsx(SvgIconHome, { color: "#fff" }) }) }),
140
140
  /* @__PURE__ */ jsxs(NavItem, { className: "position-relative", ref: appsRef, id: popoverAppsId, "aria-haspopup": "true", "aria-expanded": isAppsHovered, children: [
141
- /* @__PURE__ */ jsx(NavLink, { link: "/welcome", translate: t("navbar.applications"), children: /* @__PURE__ */ jsx(SvgIconMyApps, { color: "#fff" }) }),
141
+ /* @__PURE__ */ jsx(NavLink, { link: "/welcome", translate: t("navbar.applications"), "data-testid": "header-my-apps-button", children: /* @__PURE__ */ jsx(SvgIconMyApps, { color: "#fff" }) }),
142
142
  /* @__PURE__ */ jsxs(Popover, { className: "top-100 widget", id: popoverAppsId, isVisible: isAppsHovered, children: [
143
143
  /* @__PURE__ */ jsx(PopoverBody, { children: /* @__PURE__ */ jsx(WidgetAppsBody, { bookmarkedApps }) }),
144
144
  /* @__PURE__ */ jsx(PopoverFooter, { className: "widget-footer border-top border-ghost", children: /* @__PURE__ */ jsx(WidgetAppsFooter, {}) })
@@ -174,17 +174,17 @@ const Header = ({
174
174
  /* @__PURE__ */ jsx(NavItem, { children: /* @__PURE__ */ jsxs("div", { className: "dropdown", children: [
175
175
  /* @__PURE__ */ jsx("button", { className: "nav-link btn btn-naked d-md-none", type: "button", "aria-controls": "dropdown-navbar", "aria-expanded": !isCollapsed, "aria-label": t("navbar.open.menu"), onClick: toggleCollapsedNav, children: /* @__PURE__ */ jsx(SvgIconRafterDown, { className: "icon rafter-down", width: "20", height: "20", color: "#fff" }) }),
176
176
  /* @__PURE__ */ jsxs("ul", { className: `dropdown-menu dropdown-menu-end ${isCollapsed ? "" : "show"}`, id: "dropdown-navbar", children: [
177
- communitiesWorkflow && /* @__PURE__ */ jsx(NavItem, { children: /* @__PURE__ */ jsxs("a", { href: "/communities", className: "nav-link dropdown-item", children: [
177
+ communitiesWorkflow && /* @__PURE__ */ jsx(NavItem, { children: /* @__PURE__ */ jsxs("a", { href: "/communities", className: "nav-link dropdown-item", "data-testid": "header-community-button", children: [
178
178
  /* @__PURE__ */ jsx(SvgIconCommunities, { className: "icon communities" }),
179
179
  /* @__PURE__ */ jsx("span", { className: "nav-text", children: t("navbar.community") })
180
180
  ] }) }),
181
181
  searchWorkflow ? /* @__PURE__ */ jsx(SearchEngine, {}) : null,
182
- /* @__PURE__ */ jsx(NavItem, { children: /* @__PURE__ */ jsxs("a", { href: "/userbook/mon-compte", className: "nav-link dropdown-item", children: [
182
+ /* @__PURE__ */ jsx(NavItem, { children: /* @__PURE__ */ jsxs("a", { href: "/userbook/mon-compte", className: "nav-link dropdown-item", "data-testid": "header-user-profile-button", children: [
183
183
  /* @__PURE__ */ jsx(Avatar, { alt: userName, size: "sm", src: userAvatar, variant: "circle", className: "bg-white", width: "32", height: "32" }),
184
184
  /* @__PURE__ */ jsx("span", { className: "nav-text", children: t("navbar.myaccount") })
185
185
  ] }) }),
186
186
  /* @__PURE__ */ jsx(NavItem, { children: /* @__PURE__ */ jsx("hr", { className: "dropdown-divider" }) }),
187
- /* @__PURE__ */ jsx(NavItem, { children: /* @__PURE__ */ jsxs("a", { href: "/auth/logout?callback=" + ((theme == null ? void 0 : theme.logoutCallback) ?? ""), className: "nav-link dropdown-item", children: [
187
+ /* @__PURE__ */ jsx(NavItem, { children: /* @__PURE__ */ jsxs("a", { href: "/auth/logout?callback=" + ((theme == null ? void 0 : theme.logoutCallback) ?? ""), className: "nav-link dropdown-item", "data-testid": "header-logout-button", children: [
188
188
  /* @__PURE__ */ jsx(SvgIconDisconnect, { className: "icon logout" }),
189
189
  /* @__PURE__ */ jsx("span", { id: "logout-label", className: "nav-text", children: t("navbar.disconnect") })
190
190
  ] }) })
@@ -1,5 +1,5 @@
1
- import { ReactNode } from 'react';
2
- export interface NavLinkProps<T> {
1
+ import { ComponentPropsWithoutRef, ReactNode } from 'react';
2
+ export interface NavLinkProps<T> extends Omit<ComponentPropsWithoutRef<'a'>, 'className' | 'translate'> {
3
3
  /**
4
4
  * href link
5
5
  */
@@ -21,4 +21,4 @@ export interface NavLinkProps<T> {
21
21
  */
22
22
  button?: boolean;
23
23
  }
24
- export declare function NavLink({ link, className, children, translate, button, ...restProps }: NavLinkProps<string>): import("react/jsx-runtime").JSX.Element;
24
+ export declare const NavLink: import('react').ForwardRefExoticComponent<NavLinkProps<string> & import('react').RefAttributes<HTMLAnchorElement>>;
@@ -1,7 +1,8 @@
1
1
  import { jsxs, jsx } from "react/jsx-runtime";
2
+ import { forwardRef } from "react";
2
3
  import clsx from "clsx";
3
4
  import VisuallyHidden from "../../VisuallyHidden/VisuallyHidden.js";
4
- function NavLink({
5
+ function NavLinkRender({
5
6
  link,
6
7
  className,
7
8
  children,
@@ -9,13 +10,14 @@ function NavLink({
9
10
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
10
11
  button,
11
12
  ...restProps
12
- }) {
13
+ }, ref) {
13
14
  const classes = clsx("nav-link", className);
14
- return /* @__PURE__ */ jsxs("a", { href: link, className: classes, ...restProps, children: [
15
+ return /* @__PURE__ */ jsxs("a", { ref, href: link, className: classes, ...restProps, children: [
15
16
  children,
16
17
  translate && /* @__PURE__ */ jsx(VisuallyHidden, { children: /* @__PURE__ */ jsx("span", { className: "nav-text", children: translate }) })
17
18
  ] });
18
19
  }
20
+ const NavLink = /* @__PURE__ */ forwardRef(NavLinkRender);
19
21
  export {
20
22
  NavLink
21
23
  };
package/dist/homepage.js CHANGED
@@ -11,6 +11,9 @@ import { default as default11 } from "./modules/homepage/components/Notification
11
11
  import { NotificationListContainer } from "./modules/homepage/components/Notifications/NotificationListContainer.js";
12
12
  import { default as default12 } from "./modules/homepage/components/SchoolSpace/SchoolSpace.js";
13
13
  import { default as default13 } from "./modules/homepage/components/UserSpace/UserSpace.js";
14
+ import { Agenda } from "./modules/homepage/components/Agenda/Agenda.js";
15
+ import { AgendaContainer } from "./modules/homepage/components/Agenda/AgendaContainer.js";
16
+ import { AgendaEventCard } from "./modules/homepage/components/Agenda/AgendaEventCard.js";
14
17
  import { CommunitiesContainer } from "./modules/homepage/components/Communities/CommunitiesContainer.js";
15
18
  import { Favorites } from "./modules/homepage/components/Favorites/Favorites.js";
16
19
  import { FavoritesContainer } from "./modules/homepage/components/Favorites/FavoritesContainer.js";
@@ -28,6 +31,9 @@ import { WidgetToggle } from "./modules/homepage/components/WidgetsPersonalizati
28
31
  import { WidgetsPersonalizationPanel } from "./modules/homepage/components/WidgetsPersonalizationPanel/WidgetsPersonalizationPanel.js";
29
32
  import { useCommunities } from "./modules/homepage/components/Communities/useCommunities.js";
30
33
  export {
34
+ Agenda,
35
+ AgendaContainer,
36
+ AgendaEventCard,
31
37
  default2 as Communities,
32
38
  CommunitiesContainer,
33
39
  default3 as CommunitiesItem,
@@ -0,0 +1,10 @@
1
+ import { CalendarEvent } from '@edifice.io/client';
2
+ export interface AgendaProps {
3
+ events: CalendarEvent[];
4
+ onEventClick?: (event: CalendarEvent) => void;
5
+ onOpenAgendaClick?: () => void;
6
+ }
7
+ export declare const Agenda: {
8
+ ({ events, onEventClick, onOpenAgendaClick, }: AgendaProps): import("react/jsx-runtime").JSX.Element;
9
+ displayName: string;
10
+ };
@@ -0,0 +1,54 @@
1
+ import { jsxs, jsx } from "react/jsx-runtime";
2
+ import { useMemo } from "react";
3
+ import { useTranslation } from "react-i18next";
4
+ import illuEmptyAgenda from "@edifice.io/bootstrap/dist/images/homepage/illu-empty-agenda.svg";
5
+ import SvgIconExternalLink from "../../../icons/components/IconExternalLink.js";
6
+ import HomeCard from "../HomeCard/HomeCard.js";
7
+ import { AgendaEventCard } from "./AgendaEventCard.js";
8
+ import useDate from "../../../../hooks/useDate/useDate.js";
9
+ import Flex from "../../../../components/Flex/Flex.js";
10
+ const capitalize = (value) => value.charAt(0).toUpperCase() + value.slice(1), Agenda = ({
11
+ events,
12
+ onEventClick,
13
+ onOpenAgendaClick = () => window.open("/calendar", "_self")
14
+ }) => {
15
+ const {
16
+ t
17
+ } = useTranslation(), {
18
+ formatDate,
19
+ dateIsSame
20
+ } = useDate(), dayGroups = useMemo(() => {
21
+ const now = (/* @__PURE__ */ new Date()).toISOString();
22
+ return events.reduce((groups, event) => {
23
+ const lastGroup = groups[groups.length - 1];
24
+ return lastGroup && dateIsSame(lastGroup.date, event.startMoment, "day") ? (lastGroup.events.push(event), groups) : (groups.push({
25
+ key: event.id,
26
+ date: event.startMoment,
27
+ weekday: capitalize(formatDate(event.startMoment, "ddd")),
28
+ day: formatDate(event.startMoment, "D"),
29
+ month: dateIsSame(event.startMoment, now, "month") ? void 0 : formatDate(event.startMoment, "MMM"),
30
+ events: [event]
31
+ }), groups);
32
+ }, []);
33
+ }, [events, dateIsSame, formatDate]);
34
+ return /* @__PURE__ */ jsxs(HomeCard, { variant: "primary", children: [
35
+ /* @__PURE__ */ jsx(HomeCard.Header, { title: t("homepage.agenda.title"), actionLeftIcon: /* @__PURE__ */ jsx(SvgIconExternalLink, { width: "20", height: "20" }), onActionClick: onOpenAgendaClick, actionProps: {
36
+ "aria-label": t("homepage.agenda.openAgenda"),
37
+ "data-testid": "agenda-button-open"
38
+ } }),
39
+ /* @__PURE__ */ jsx(HomeCard.Content, { children: dayGroups.length === 0 ? /* @__PURE__ */ jsxs(Flex, { direction: "column", align: "center", gap: "8", className: "agenda-empty", children: [
40
+ /* @__PURE__ */ jsx("img", { src: illuEmptyAgenda, alt: "", width: 64, height: 64 }),
41
+ /* @__PURE__ */ jsx("span", { className: "agenda-empty-text", children: t("homepage.agenda.empty") })
42
+ ] }) : /* @__PURE__ */ jsx(Flex, { direction: "column", gap: "8", className: "agenda-content", children: dayGroups.map((group) => /* @__PURE__ */ jsxs(Flex, { gap: "4", className: "agenda-day-group", "data-testid": "agenda-day-group", children: [
43
+ /* @__PURE__ */ jsxs(Flex, { direction: "column", align: "center", className: "agenda-day-group-date", children: [
44
+ /* @__PURE__ */ jsx("span", { children: group.weekday }),
45
+ /* @__PURE__ */ jsx("span", { className: "agenda-day-group-date-number", children: group.day }),
46
+ group.month && /* @__PURE__ */ jsx("span", { children: group.month })
47
+ ] }),
48
+ /* @__PURE__ */ jsx(Flex, { direction: "column", gap: "8", className: "agenda-day-group-events", children: group.events.map((event) => /* @__PURE__ */ jsx(AgendaEventCard, { event, onClick: () => onEventClick == null ? void 0 : onEventClick(event) }, event.id)) })
49
+ ] }, group.key)) }) })
50
+ ] });
51
+ };
52
+ export {
53
+ Agenda
54
+ };
@@ -0,0 +1 @@
1
+ export declare function AgendaContainer(): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,14 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { Agenda } from "./Agenda.js";
3
+ import { useAgendaContainer } from "./hooks/useAgendaContainer.js";
4
+ function AgendaContainer() {
5
+ const {
6
+ events
7
+ } = useAgendaContainer();
8
+ return /* @__PURE__ */ jsx(Agenda, { events, onEventClick: (event) => {
9
+ window.open(`/calendar#/view/${event.calendarId}/event/${event.id}`, "_self");
10
+ } });
11
+ }
12
+ export {
13
+ AgendaContainer
14
+ };
@@ -0,0 +1,10 @@
1
+ import { CalendarEvent } from '@edifice.io/client';
2
+ import { MouseEventHandler } from 'react';
3
+ export interface AgendaEventCardProps {
4
+ event: CalendarEvent;
5
+ onClick?: MouseEventHandler<HTMLButtonElement>;
6
+ }
7
+ export declare const AgendaEventCard: {
8
+ ({ event, onClick }: AgendaEventCardProps): import("react/jsx-runtime").JSX.Element;
9
+ displayName: string;
10
+ };
@@ -0,0 +1,30 @@
1
+ import { jsxs, jsx } from "react/jsx-runtime";
2
+ import { useTranslation } from "react-i18next";
3
+ import SvgIconClock from "../../../icons/components/IconClock.js";
4
+ import useDate from "../../../../hooks/useDate/useDate.js";
5
+ import Flex from "../../../../components/Flex/Flex.js";
6
+ const capitalize = (value) => value.charAt(0).toUpperCase() + value.slice(1), AgendaEventCard = ({
7
+ event,
8
+ onClick
9
+ }) => {
10
+ const {
11
+ t
12
+ } = useTranslation(), {
13
+ formatDate,
14
+ dateIsSame
15
+ } = useDate(), formatTime = (date) => formatDate(date, "HH[h]mm"), timeLabel = event.allDay ? t("homepage.agenda.allDay") : dateIsSame(event.startMoment, event.endMoment, "day") ? `${formatTime(event.startMoment)} - ${formatTime(event.endMoment)}` : t("homepage.agenda.multiDayTime", {
16
+ startTime: formatTime(event.startMoment),
17
+ endDay: capitalize(formatDate(event.endMoment, "ddd D")),
18
+ endTime: formatTime(event.endMoment)
19
+ });
20
+ return /* @__PURE__ */ jsxs("button", { type: "button", className: "agenda-event-card", onClick, "data-testid": "agenda-event-card", children: [
21
+ /* @__PURE__ */ jsxs(Flex, { align: "center", gap: "8", className: "agenda-event-card-time", children: [
22
+ /* @__PURE__ */ jsx(SvgIconClock, { width: "20", height: "20" }),
23
+ /* @__PURE__ */ jsx("span", { children: timeLabel })
24
+ ] }),
25
+ /* @__PURE__ */ jsx("p", { className: "agenda-event-card-title", children: event.title })
26
+ ] });
27
+ };
28
+ export {
29
+ AgendaEventCard
30
+ };
@@ -0,0 +1,14 @@
1
+ import { CalendarEvent } from '@edifice.io/client';
2
+ /** Number of upcoming events shown by the widget. */
3
+ export declare const MAX_VISIBLE_EVENTS = 3;
4
+ export interface UseAgendaContainerReturn {
5
+ events: CalendarEvent[];
6
+ isLoading: boolean;
7
+ error: Error | null;
8
+ }
9
+ /**
10
+ * Loads the current user's calendars, then their events, and keeps only the
11
+ * next `MAX_VISIBLE_EVENTS` events that haven't fully elapsed yet — an event
12
+ * still in progress (end date in the future) counts as "upcoming" too.
13
+ */
14
+ export declare const useAgendaContainer: () => UseAgendaContainerReturn;
@@ -0,0 +1,27 @@
1
+ import dayjs from "dayjs";
2
+ import { useMemo } from "react";
3
+ import { useCalendars, useEvents } from "../services/queries/agenda.js";
4
+ const MAX_VISIBLE_EVENTS = 3, useAgendaContainer = () => {
5
+ const {
6
+ data: calendars,
7
+ isLoading: isLoadingCalendars,
8
+ error: calendarsError
9
+ } = useCalendars(), calendarIds = useMemo(() => (calendars == null ? void 0 : calendars.map((calendar) => calendar.id)) ?? [], [calendars]), {
10
+ data: rawEvents,
11
+ isLoading: isLoadingEvents,
12
+ error: eventsError
13
+ } = useEvents(calendarIds, MAX_VISIBLE_EVENTS);
14
+ return {
15
+ events: useMemo(() => {
16
+ if (!rawEvents) return [];
17
+ const now = dayjs();
18
+ return rawEvents.filter((event) => dayjs(event.endMoment).isAfter(now)).sort((a, b) => dayjs(a.startMoment).diff(dayjs(b.startMoment))).slice(0, MAX_VISIBLE_EVENTS);
19
+ }, [rawEvents]),
20
+ isLoading: isLoadingCalendars || isLoadingEvents,
21
+ error: calendarsError ?? eventsError
22
+ };
23
+ };
24
+ export {
25
+ MAX_VISIBLE_EVENTS,
26
+ useAgendaContainer
27
+ };
@@ -0,0 +1,3 @@
1
+ export * from './Agenda';
2
+ export * from './AgendaContainer';
3
+ export * from './AgendaEventCard';
@@ -0,0 +1,24 @@
1
+ import { Calendar, CalendarEvent } from '@edifice.io/client';
2
+ /**
3
+ * Creates an agenda service with methods to fetch the current user's
4
+ * calendars and upcoming events (homepage "Agenda" widget).
5
+ *
6
+ * Backend contract (provisional, see IMPULS-5880/IMPULS-6095): `GET
7
+ * /calendar/calendars` lists the user's calendars, `GET
8
+ * /calendar/events/widget?calendarId=...&nb=N` returns up to `N` events for
9
+ * the given calendars. Whether that second endpoint already excludes past
10
+ * (fully elapsed) events isn't guaranteed, so callers should re-apply that
11
+ * filter themselves.
12
+ *
13
+ * @param baseURL The base URL for the agenda service API.
14
+ */
15
+ export declare const createAgendaService: (baseURL: string) => {
16
+ /**
17
+ * Get the calendars the current user can see events from.
18
+ */
19
+ getCalendars(): Promise<Calendar[]>;
20
+ /**
21
+ * Get up to `nb` events for the given calendars, in no particular order.
22
+ */
23
+ getEvents(calendarIds: string[], nb: number): Promise<CalendarEvent[]>;
24
+ };
@@ -0,0 +1,45 @@
1
+ import { odeServices } from "@edifice.io/client";
2
+ const toCalendar = ({
3
+ _id,
4
+ title,
5
+ color,
6
+ isExternal
7
+ }) => ({
8
+ id: _id,
9
+ title,
10
+ color,
11
+ isExternal
12
+ }), toCalendarEvent = ({
13
+ _id,
14
+ title,
15
+ allday,
16
+ startMoment,
17
+ endMoment,
18
+ calendar
19
+ }) => ({
20
+ id: _id,
21
+ calendarId: calendar[0],
22
+ title,
23
+ startMoment,
24
+ endMoment,
25
+ allDay: allday
26
+ }), createAgendaService = (baseURL) => ({
27
+ /**
28
+ * Get the calendars the current user can see events from.
29
+ */
30
+ async getCalendars() {
31
+ return (await odeServices.http().get(`${baseURL}/calendar/calendars`)).map(toCalendar);
32
+ },
33
+ /**
34
+ * Get up to `nb` events for the given calendars, in no particular order.
35
+ */
36
+ async getEvents(calendarIds, nb) {
37
+ if (calendarIds.length === 0)
38
+ return [];
39
+ const query = new URLSearchParams();
40
+ return calendarIds.forEach((id) => query.append("calendarId", id)), query.set("nb", String(nb)), (await odeServices.http().get(`${baseURL}/calendar/events/widget?${query.toString()}`)).map(toCalendarEvent);
41
+ }
42
+ });
43
+ export {
44
+ createAgendaService
45
+ };
@@ -0,0 +1,4 @@
1
+ export declare const agendaService: {
2
+ getCalendars(): Promise<import('@edifice.io/client').Calendar[]>;
3
+ getEvents(calendarIds: string[], nb: number): Promise<import('@edifice.io/client').CalendarEvent[]>;
4
+ };
@@ -0,0 +1,5 @@
1
+ import { createAgendaService } from "./agendaService.js";
2
+ const agendaService = createAgendaService("");
3
+ export {
4
+ agendaService
5
+ };
@@ -0,0 +1,19 @@
1
+ import { Calendar, CalendarEvent } from '@edifice.io/client';
2
+ export declare const agendaQueryKeys: {
3
+ calendars: () => readonly ["agenda", "calendars"];
4
+ events: (calendarIds: string[], nb: number) => readonly ["agenda", "events", string[], number];
5
+ };
6
+ export declare const agendaQueryOptions: {
7
+ getCalendars(): import('@tanstack/react-query').OmitKeyof<import('@tanstack/react-query').UseQueryOptions<Calendar[], Error, Calendar[], import('@tanstack/react-query').QueryKey>, "queryFn"> & {
8
+ queryFn?: import('@tanstack/react-query').QueryFunction<Calendar[], import('@tanstack/react-query').QueryKey, never> | undefined;
9
+ } & {
10
+ queryKey: import('@tanstack/react-query').DataTag<import('@tanstack/react-query').QueryKey, Calendar[]>;
11
+ };
12
+ getEvents(calendarIds: string[], nb: number): import('@tanstack/react-query').OmitKeyof<import('@tanstack/react-query').UseQueryOptions<CalendarEvent[], Error, CalendarEvent[], import('@tanstack/react-query').QueryKey>, "queryFn"> & {
13
+ queryFn?: import('@tanstack/react-query').QueryFunction<CalendarEvent[], import('@tanstack/react-query').QueryKey, never> | undefined;
14
+ } & {
15
+ queryKey: import('@tanstack/react-query').DataTag<import('@tanstack/react-query').QueryKey, CalendarEvent[]>;
16
+ };
17
+ };
18
+ export declare const useCalendars: () => import('@tanstack/react-query').UseQueryResult<Calendar[], Error>;
19
+ export declare const useEvents: (calendarIds: string[], nb: number) => import('@tanstack/react-query').UseQueryResult<CalendarEvent[], Error>;
@@ -0,0 +1,26 @@
1
+ import { useQuery, queryOptions } from "@tanstack/react-query";
2
+ import { agendaService } from "../api/index.js";
3
+ const agendaQueryKeys = {
4
+ calendars: () => ["agenda", "calendars"],
5
+ events: (calendarIds, nb) => ["agenda", "events", calendarIds, nb]
6
+ }, agendaQueryOptions = {
7
+ getCalendars() {
8
+ return queryOptions({
9
+ queryKey: agendaQueryKeys.calendars(),
10
+ queryFn: async () => agendaService.getCalendars()
11
+ });
12
+ },
13
+ getEvents(calendarIds, nb) {
14
+ return queryOptions({
15
+ queryKey: agendaQueryKeys.events(calendarIds, nb),
16
+ queryFn: async () => agendaService.getEvents(calendarIds, nb),
17
+ enabled: calendarIds.length > 0
18
+ });
19
+ }
20
+ }, useCalendars = () => useQuery(agendaQueryOptions.getCalendars()), useEvents = (calendarIds, nb) => useQuery(agendaQueryOptions.getEvents(calendarIds, nb));
21
+ export {
22
+ agendaQueryKeys,
23
+ agendaQueryOptions,
24
+ useCalendars,
25
+ useEvents
26
+ };
@@ -24,8 +24,8 @@ const NARROW_CONTAINER_WIDTH = 800, MAX_ITEMS = 4, MAX_ITEMS_NARROW = 3, Communi
24
24
  height: 80
25
25
  } }),
26
26
  /* @__PURE__ */ jsxs("div", { className: "communities-empty-content", children: [
27
- /* @__PURE__ */ jsx("p", { className: "communities-empty-title", children: t("homepage.communities.subtitle", "Créez votre première communauté pour animer votre classe !") }),
28
- /* @__PURE__ */ jsx("p", { className: "communities-empty-description", children: t("homepage.communities.description", "Vous pouvez centraliser et organiser les documents et les ressources pour vos élèves.") })
27
+ /* @__PURE__ */ jsx("p", { className: "communities-empty-title", children: t("homepage.communities.subtitle") }),
28
+ /* @__PURE__ */ jsx("p", { className: "communities-empty-description", children: t("homepage.communities.description") })
29
29
  ] })
30
30
  ] }) }) })
31
31
  ] });
@@ -2,6 +2,8 @@ import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import clsx from "clsx";
3
3
  import { useTranslation } from "react-i18next";
4
4
  import { useId } from "react";
5
+ import { MyAppsPopoverBody, MyAppsPopoverFooter } from "./MyAppsPopover.js";
6
+ import { useSafeHoverPopover } from "./useSafeHoverPopover.js";
5
7
  import { Navbar } from "../../../../components/Layout/components/Navbar.js";
6
8
  import { NavItem } from "../../../../components/Layout/components/NavItem.js";
7
9
  import { NavLink } from "../../../../components/Layout/components/NavLink.js";
@@ -18,13 +20,13 @@ import useConversation from "../../../../hooks/useConversation/useConversation.j
18
20
  import useUser from "../../../../hooks/useUser/useUser.js";
19
21
  import useHasWorkflow from "../../../../hooks/useHasWorkflow/useHasWorkflow.js";
20
22
  import { useEdificeTheme } from "../../../../providers/EdificeThemeProvider/EdificeThemeProvider.hook.js";
21
- import useHover from "../../../../hooks/useHover/useHover.js";
23
+ import useBreakpoint from "../../../../hooks/useBreakpoint/useBreakpoint.js";
22
24
  import LogoBeta from "../../../../components/LogoBeta/LogoBeta.js";
23
25
  import Badge from "../../../../components/Badge/Badge.js";
24
26
  import VisuallyHidden from "../../../../components/VisuallyHidden/VisuallyHidden.js";
25
27
  import ButtonBeta from "../../../../components/ButtonBeta/ButtonBeta.js";
26
28
  import Avatar from "../../../../components/Avatar/Avatar.js";
27
- import { Popover, PopoverBody } from "../../../../components/Popover/Popover.js";
29
+ import { Popover, PopoverBody, PopoverFooter } from "../../../../components/Popover/Popover.js";
28
30
  const Header = ({
29
31
  src = "",
30
32
  onNotificationsClick,
@@ -41,13 +43,21 @@ const Header = ({
41
43
  userAvatar,
42
44
  userName,
43
45
  communitiesWorkflow,
44
- conversationWorflow
46
+ conversationWorflow,
47
+ bookmarkedApps
45
48
  } = useHeader({
46
49
  user,
47
50
  avatar
48
51
  }), {
49
52
  theme
50
- } = useEdificeTheme(), hasMessages = messages > 0, hasNotificationToday = useHasNotificationToday(), [userRef, isUserHovered] = useHover(), popoverUserId = useId(), handleNotificationsClick = () => {
53
+ } = useEdificeTheme(), hasMessages = messages > 0, hasNotificationToday = useHasNotificationToday(), popoverUserId = useId(), popoverAppsId = useId(), {
54
+ lg: isDesktop
55
+ } = useBreakpoint(), myApps = useSafeHoverPopover({
56
+ hoverEnabled: isDesktop,
57
+ clickEnabled: !isDesktop
58
+ }), userMenu = useSafeHoverPopover(), handleMyAppsClick = (event) => {
59
+ isDesktop || event.preventDefault();
60
+ }, handleNotificationsClick = () => {
51
61
  onNotificationsClick == null || onNotificationsClick();
52
62
  };
53
63
  return /* @__PURE__ */ jsx("header", { className: classes, "data-product": dataProduct, children: /* @__PURE__ */ jsxs(Navbar, { className: "px-24", children: [
@@ -66,7 +76,15 @@ const Header = ({
66
76
  /* @__PURE__ */ jsx(SvgIconMessagesBeta, {}),
67
77
  /* @__PURE__ */ jsx(VisuallyHidden, { children: t("conversation") })
68
78
  ] }) }),
69
- /* @__PURE__ */ jsx(NavItem, { children: /* @__PURE__ */ jsx(NavLink, { link: "/welcome", translate: t("navbar.applications"), "data-testid": "header-my-apps-button", children: /* @__PURE__ */ jsx(SvgIconMyAppsBeta, {}) }) }),
79
+ /* @__PURE__ */ jsxs(NavItem, { className: "position-relative", children: [
80
+ /* @__PURE__ */ jsx(NavLink, { ref: myApps.setReference, link: "/welcome", translate: t("navbar.applications"), id: popoverAppsId, "aria-haspopup": "true", "aria-expanded": myApps.open, "data-testid": "header-my-apps-button", ...myApps.getReferenceProps({
81
+ onClick: handleMyAppsClick
82
+ }), children: /* @__PURE__ */ jsx(SvgIconMyAppsBeta, {}) }),
83
+ /* @__PURE__ */ jsxs(Popover, { ref: myApps.setFloating, className: "my-apps-popover", id: popoverAppsId, isVisible: myApps.open, ...myApps.getFloatingProps(), children: [
84
+ /* @__PURE__ */ jsx(PopoverBody, { children: /* @__PURE__ */ jsx(MyAppsPopoverBody, { bookmarkedApps }) }),
85
+ /* @__PURE__ */ jsx(PopoverFooter, { className: "d-flex justify-content-center", children: /* @__PURE__ */ jsx(MyAppsPopoverFooter, {}) })
86
+ ] })
87
+ ] }),
70
88
  /* @__PURE__ */ jsxs(NavItem, { className: "position-relative", children: [
71
89
  /* @__PURE__ */ jsx(ButtonBeta, { leftIcon: /* @__PURE__ */ jsx(SvgIconNotificationBeta, {}), variant: "ghost", onClick: handleNotificationsClick }),
72
90
  hasNotificationToday && /* @__PURE__ */ jsx("span", { style: {
@@ -81,13 +99,11 @@ const Header = ({
81
99
  pointerEvents: "none",
82
100
  zIndex: 1,
83
101
  display: "block"
84
- }, "aria-label": t("homepage.notifications.new-badge", {
85
- defaultValue: "Nouvelle notification"
86
- }) })
102
+ }, "aria-label": t("homepage.notifications.new-badge") })
87
103
  ] }),
88
- /* @__PURE__ */ jsxs(NavItem, { className: "position-relative", ref: userRef, id: popoverUserId, "aria-haspopup": "true", "aria-expanded": isUserHovered, "data-testid": "header-user-menu-button", children: [
89
- /* @__PURE__ */ jsx(NavLink, { link: "/userbook/mon-compte", translate: t("navbar.myaccount"), "data-testid": "header-user-profile-button", children: /* @__PURE__ */ jsx(Avatar, { alt: userName, size: "sm", src: userAvatar, variant: "circle", className: "bg-white", width: "32", height: "32" }) }),
90
- /* @__PURE__ */ jsx(Popover, { align: "end", className: "widget", id: popoverUserId, isVisible: isUserHovered, children: /* @__PURE__ */ jsxs(PopoverBody, { children: [
104
+ /* @__PURE__ */ jsxs(NavItem, { className: "position-relative", children: [
105
+ /* @__PURE__ */ jsx(NavLink, { ref: userMenu.setReference, link: "/userbook/mon-compte", translate: t("navbar.myaccount"), id: popoverUserId, "aria-haspopup": "true", "aria-expanded": userMenu.open, "data-testid": "header-user-profile-button", ...userMenu.getReferenceProps(), children: /* @__PURE__ */ jsx(Avatar, { alt: userName, size: "sm", src: userAvatar, variant: "circle", className: "bg-white", width: "32", height: "32" }) }),
106
+ /* @__PURE__ */ jsx(Popover, { ref: userMenu.setFloating, align: "end", className: "widget", id: popoverUserId, isVisible: userMenu.open, ...userMenu.getFloatingProps(), children: /* @__PURE__ */ jsxs(PopoverBody, { children: [
91
107
  /* @__PURE__ */ jsxs("a", { href: "/timeline/customize?callback=" + encodeURIComponent(`${window.location.pathname}${window.location.search}`), className: "nav-link customize d-flex align-items-center gap-8", "data-testid": "header-customize-button", children: [
92
108
  /* @__PURE__ */ jsx(SvgIconSetBackground, { className: "icon" }),
93
109
  /* @__PURE__ */ jsx("span", { id: "cutomize-label", className: "nav-text", children: t("navbar.customize") })
@@ -0,0 +1,5 @@
1
+ import { IWebApp } from '@edifice.io/client';
2
+ export declare const MyAppsPopoverFooter: () => import("react/jsx-runtime").JSX.Element;
3
+ export declare const MyAppsPopoverBody: ({ bookmarkedApps, }: {
4
+ bookmarkedApps: IWebApp[] | undefined;
5
+ }) => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,28 @@
1
+ import { jsxs, jsx } from "react/jsx-runtime";
2
+ import { useTranslation } from "react-i18next";
3
+ import illuEmptyFavorite from "@edifice.io/bootstrap/dist/images/homepage/illu-empty-favorite.svg";
4
+ import Flex from "../../../../components/Flex/Flex.js";
5
+ import AppIcon from "../../../../components/AppIcon/AppIcon.js";
6
+ const MAX_VISIBLE_APPS = 10, appToOpenOnBlank = ["Administration"], MyAppsPopoverFooter = () => {
7
+ const {
8
+ t
9
+ } = useTranslation();
10
+ return /* @__PURE__ */ jsx("a", { href: "/welcome", className: "btn-beta btn-beta-tertiary btn-beta--ghost btn-beta--small", "data-testid": "header-my-apps-popover-more", children: /* @__PURE__ */ jsx("span", { children: t("homepage.header.myapps.seeMore") }) });
11
+ }, MyAppsPopoverBody = ({
12
+ bookmarkedApps
13
+ }) => {
14
+ const {
15
+ t
16
+ } = useTranslation(), getAppName = (app) => app.prefix && app.prefix.length > 1 ? t(app.prefix.substring(1)) : t(app.displayName) || "";
17
+ return bookmarkedApps != null && bookmarkedApps.length ? /* @__PURE__ */ jsx("div", { className: "my-apps-popover-grid", "data-testid": "header-my-apps-popover", children: bookmarkedApps.slice(0, MAX_VISIBLE_APPS).map((app, index) => {
18
+ const opensInNewTab = appToOpenOnBlank.includes(app.name) || app.isExternal || app.target === "_blank", appName = getAppName(app);
19
+ return /* @__PURE__ */ jsx("a", { href: app.address, title: appName, "aria-label": appName, className: "my-apps-popover-item", "data-testid": "header-my-apps-popover-item", target: opensInNewTab ? "_blank" : void 0, rel: opensInNewTab ? "noopener noreferrer" : void 0, children: /* @__PURE__ */ jsx(AppIcon, { app, size: "32" }) }, index);
20
+ }) }) : /* @__PURE__ */ jsxs(Flex, { direction: "column", align: "center", gap: "8", children: [
21
+ /* @__PURE__ */ jsx("img", { src: illuEmptyFavorite, alt: "", width: 50, height: 50 }),
22
+ /* @__PURE__ */ jsx("span", { className: "my-apps-popover-empty-text text-center", children: t("homepage.header.myapps.empty") })
23
+ ] });
24
+ };
25
+ export {
26
+ MyAppsPopoverBody,
27
+ MyAppsPopoverFooter
28
+ };
@@ -0,0 +1,32 @@
1
+ import { useState } from "react";
2
+ import { useFloating, useHover, safePolygon, useClick, useDismiss, useFocus, useInteractions } from "@floating-ui/react";
3
+ function useSafeHoverPopover({
4
+ hoverEnabled = !0,
5
+ clickEnabled = !1
6
+ } = {}) {
7
+ const [open, setOpen] = useState(!1), {
8
+ refs,
9
+ context
10
+ } = useFloating({
11
+ open,
12
+ onOpenChange: setOpen
13
+ }), hover = useHover(context, {
14
+ enabled: hoverEnabled,
15
+ handleClose: safePolygon()
16
+ }), click = useClick(context, {
17
+ enabled: clickEnabled
18
+ }), dismiss = useDismiss(context), focus = useFocus(context), {
19
+ getReferenceProps,
20
+ getFloatingProps
21
+ } = useInteractions([hover, click, dismiss, focus]);
22
+ return {
23
+ open,
24
+ setReference: refs.setReference,
25
+ setFloating: refs.setFloating,
26
+ getReferenceProps,
27
+ getFloatingProps
28
+ };
29
+ }
30
+ export {
31
+ useSafeHoverPopover
32
+ };
@@ -3,7 +3,11 @@ import { ButtonBeta } from '../../../../components';
3
3
  export interface HomeCardHeaderProps extends Omit<ComponentPropsWithoutRef<'div'>, 'title'> {
4
4
  /** Title displayed on the left side of the header. */
5
5
  title: ReactNode;
6
- /** Label of the action button displayed on the right side of the header. */
6
+ /**
7
+ * Label of the action button displayed on the right side of the header.
8
+ * Can be omitted for an icon-only button — pass `aria-label` via
9
+ * `actionProps` in that case.
10
+ */
7
11
  actionLabel?: ReactNode;
8
12
  /** Callback invoked when the action button is clicked. */
9
13
  onActionClick?: MouseEventHandler<HTMLButtonElement>;
@@ -12,7 +12,7 @@ const HomeCardHeader = ({
12
12
  className,
13
13
  ...rest
14
14
  }) => {
15
- const hasAction = !!actionLabel && !!onActionClick;
15
+ const hasAction = !!onActionClick && (!!actionLabel || !!actionLeftIcon || !!actionRightIcon);
16
16
  return /* @__PURE__ */ jsxs(Flex, { align: "center", justify: "between", gap: "8", className: clsx("home-card-header", className), ...rest, children: [
17
17
  /* @__PURE__ */ jsx("h3", { className: "home-card-header-title", children: title }),
18
18
  hasAction && /* @__PURE__ */ jsx(ButtonBeta, { color: "tertiary", size: "sm", variant: "ghost", onClick: onActionClick, leftIcon: actionLeftIcon, rightIcon: actionRightIcon, "data-testid": "home-card-header-action", ...actionProps, children: actionLabel })
@@ -47,7 +47,7 @@ const MessageFlash = ({
47
47
  }), title: t("close.message", {
48
48
  title: message.title
49
49
  }) }),
50
- /* @__PURE__ */ jsx("div", { className: "message-flash-icon", role: "img", "aria-label": message.color === "red" ? t("warning") : t("information"), children: message.color && message.color === "red" ? /* @__PURE__ */ jsx(SvgIconAlertTriangle, {}) : /* @__PURE__ */ jsx(SvgIconInfoCircle, {}) }),
50
+ /* @__PURE__ */ jsx("div", { className: "message-flash-icon", role: "img", "aria-label": message.color === "red" ? t("homepage.messageFlash.warning") : t("homepage.messageFlash.information"), children: message.color && message.color === "red" ? /* @__PURE__ */ jsx(SvgIconAlertTriangle, {}) : /* @__PURE__ */ jsx(SvgIconInfoCircle, {}) }),
51
51
  /* @__PURE__ */ jsxs(Flex, { direction: "column", className: "w-100 message-flash-body", children: [
52
52
  /* @__PURE__ */ jsx(Card.Title, { children: message.title }),
53
53
  /* @__PURE__ */ jsx("div", { ref: contentRef, className: classesContent, dangerouslySetInnerHTML: {
@@ -14,7 +14,7 @@ import Divider from "../../../../components/Divider/Divider.js";
14
14
  const NotificationList = ({
15
15
  notifications,
16
16
  notificationTypes,
17
- selectedTypes,
17
+ selectedTypes = [],
18
18
  onFilterChange,
19
19
  onCloseNotifications,
20
20
  onLoadNextPage,
@@ -25,19 +25,19 @@ const NotificationList = ({
25
25
  t
26
26
  } = useTranslation(), loadNextRef = useInfiniteScroll({
27
27
  callback: () => onLoadNextPage == null ? void 0 : onLoadNextPage()
28
- }), handleCloseClick = () => {
28
+ }), handleFilterChange = (types) => onFilterChange == null ? void 0 : onFilterChange(types), handleCloseClick = () => {
29
29
  onCloseNotifications && onCloseNotifications();
30
30
  };
31
31
  return /* @__PURE__ */ jsx("section", { role: "region", className: "notification-list", children: /* @__PURE__ */ jsxs(Flex, { direction: "column", children: [
32
32
  /* @__PURE__ */ jsxs(Flex, { justify: "between", align: "center", wrap: "nowrap", className: "py-16 ps-24 pe-8", children: [
33
33
  /* @__PURE__ */ jsxs(Flex, { align: "center", gap: "8", className: "notification-list-title", children: [
34
34
  /* @__PURE__ */ jsx("h4", { className: "text-truncate", children: t("homepage.notifications-list.title") }),
35
- notificationTypes && notificationTypes.length > 0 && /* @__PURE__ */ jsx(NotificationFilterMenu, { notificationTypes, selectedTypes: selectedTypes ?? notificationTypes, onFilterChange: (types) => onFilterChange == null ? void 0 : onFilterChange(types) })
35
+ notificationTypes && notificationTypes.length > 0 && /* @__PURE__ */ jsx(NotificationFilterMenu, { notificationTypes, selectedTypes, onFilterChange: handleFilterChange })
36
36
  ] }),
37
37
  onCloseNotifications && /* @__PURE__ */ jsx(ButtonBeta, { color: "tertiary", variant: "ghost", rightIcon: /* @__PURE__ */ jsx(SvgIconClose, {}), onClick: handleCloseClick, "aria-label": t("homepage.notifications-list.close"), title: t("homepage.notifications-list.close"), "data-testid": "notification-list-close-button" })
38
38
  ] }),
39
- (notifications == null ? void 0 : notifications.length) === 0 ? /* @__PURE__ */ jsx("div", { className: "mx-24", children: /* @__PURE__ */ jsx(EmptyScreen, { size: 135, imageSrc: illuEmptyNotification, imageAlt: t("homepage.notifications-list.empty.description"), text: t("homepage.notifications-list.empty.description") }) }) : /* @__PURE__ */ jsxs(Fragment, { children: [
40
- /* @__PURE__ */ jsx("ul", { children: notifications == null ? void 0 : notifications.map((notification, index) => /* @__PURE__ */ jsxs("li", { children: [
39
+ isLoading && !(notifications != null && notifications.length) ? /* @__PURE__ */ jsx(NotificationListSkeleton, {}) : !notifications || notifications.length === 0 ? /* @__PURE__ */ jsx("div", { className: "mx-24", children: /* @__PURE__ */ jsx(EmptyScreen, { size: 135, imageSrc: illuEmptyNotification, imageAlt: t("homepage.notifications-list.empty.description"), text: t("homepage.notifications-list.empty.description") }) }) : /* @__PURE__ */ jsxs(Fragment, { children: [
40
+ /* @__PURE__ */ jsx("ul", { children: notifications.map((notification, index) => /* @__PURE__ */ jsxs("li", { children: [
41
41
  /* @__PURE__ */ jsx(NotificationItem, { notification }),
42
42
  /* @__PURE__ */ jsx(Divider, { className: "border-grey-300 my-0" })
43
43
  ] }, "notification-list-" + notification._id + "-" + index)) }),
@@ -3,9 +3,9 @@ import React from "react";
3
3
  import Flex from "../../../../components/Flex/Flex.js";
4
4
  import Avatar from "../../../../components/Avatar/Avatar.js";
5
5
  import TextSkeleton from "../../../../components/Skeleton/TextSkeleton.js";
6
- const NotificationSkeleton = /* @__PURE__ */ React.forwardRef((_props, ref) => /* @__PURE__ */ jsx(Flex, { direction: "column", className: "notification", gap: "8", ref, children: /* @__PURE__ */ jsxs(Flex, { direction: "row", gap: "8", children: [
6
+ const NotificationSkeleton = /* @__PURE__ */ React.forwardRef((_props, ref) => /* @__PURE__ */ jsx(Flex, { direction: "column", className: "notification-item", gap: "8", ref, children: /* @__PURE__ */ jsxs(Flex, { direction: "row", gap: "8", children: [
7
7
  /* @__PURE__ */ jsx("div", { className: "notification-item-picture", children: /* @__PURE__ */ jsx(Avatar, { alt: "Notification Picture", variant: "circle", className: "notification-item-avatar" }) }),
8
- /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: "8", className: "notification-message", children: [
8
+ /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: "8", className: "notification-item-message", children: [
9
9
  /* @__PURE__ */ jsx(TextSkeleton, {}),
10
10
  /* @__PURE__ */ jsx(TextSkeleton, {}),
11
11
  /* @__PURE__ */ jsx("div", { className: "placeholder col-4", style: {
@@ -16,11 +16,7 @@ const NotificationFilterMenu = ({
16
16
  onFilterChange(types), setIsOpen(!1);
17
17
  };
18
18
  return /* @__PURE__ */ jsxs(Fragment, { children: [
19
- /* @__PURE__ */ jsx(ButtonBeta, { type: "button", color: "secondary", variant: "outline", size: "md", leftIcon: /* @__PURE__ */ jsx(SvgIconFilter, {}), rightIcon: /* @__PURE__ */ jsx(SvgIconRafterDown, {}), "aria-label": t("homepage.notifications.filter-modal.trigger", {
20
- defaultValue: "Filtrer les notifications"
21
- }), title: t("homepage.notifications.filter-modal.trigger", {
22
- defaultValue: "Filtrer les notifications"
23
- }), onClick: () => setIsOpen(!0), "data-testid": "notification-filter-button" }),
19
+ /* @__PURE__ */ jsx(ButtonBeta, { type: "button", color: "secondary", variant: "outline", size: "md", leftIcon: /* @__PURE__ */ jsx(SvgIconFilter, {}), rightIcon: /* @__PURE__ */ jsx(SvgIconRafterDown, {}), "aria-label": t("homepage.notifications.filter-modal.trigger"), title: t("homepage.notifications.filter-modal.trigger"), onClick: () => setIsOpen(!0), "data-testid": "notification-filter-button" }),
24
20
  isOpen && /* @__PURE__ */ jsx(NotificationFilterModal, { isOpen, allTypes: notificationTypes, appliedTypes: selectedTypes, onCancel: () => setIsOpen(!1), onApply: handleApply })
25
21
  ] });
26
22
  };
@@ -1,11 +1,12 @@
1
1
  import { jsxs, jsx } from "react/jsx-runtime";
2
2
  import clsx from "clsx";
3
- import { useState, useRef, useMemo, useEffect } from "react";
3
+ import { useState, useMemo } from "react";
4
4
  import { useTranslation } from "react-i18next";
5
5
  import SvgIconCheck from "../../../../icons/components/IconCheck.js";
6
6
  import { getAppCodeAndI18nKey } from "./notificationAdapter.js";
7
- import Modal from "../../../../../components/Modal/Modal.js";
7
+ import ModalBeta from "../../../../../components/ModalBeta/ModalBeta.js";
8
8
  import Flex from "../../../../../components/Flex/Flex.js";
9
+ import Checkbox from "../../../../../components/Checkbox/Checkbox.js";
9
10
  import AppIcon from "../../../../../components/AppIcon/AppIcon.js";
10
11
  import ButtonBeta from "../../../../../components/ButtonBeta/ButtonBeta.js";
11
12
  const NotificationFilterModal = ({
@@ -18,7 +19,7 @@ const NotificationFilterModal = ({
18
19
  const {
19
20
  t,
20
21
  i18n
21
- } = useTranslation(), [selected, setSelected] = useState(appliedTypes), refSelectAllCheckbox = useRef(null), allTypesSorted = useMemo(() => allTypes.map((type) => {
22
+ } = useTranslation(), [selected, setSelected] = useState(appliedTypes), allTypesSorted = useMemo(() => allTypes.map((type) => {
22
23
  const [appCode, appI18nKey] = getAppCodeAndI18nKey(type), label = t(appI18nKey, {
23
24
  defaultValue: appCode
24
25
  });
@@ -35,16 +36,13 @@ const NotificationFilterModal = ({
35
36
  }, toggleAll = () => {
36
37
  setSelected(selected.length === allTypes.length ? [] : allTypes);
37
38
  };
38
- return useEffect(() => {
39
- refSelectAllCheckbox.current && (refSelectAllCheckbox.current.indeterminate = selected.length > 0 && selected.length < allTypes.length);
40
- }, [selected.length, allTypes.length]), /* @__PURE__ */ jsxs(Modal, { id: "notification-filter-modal", size: "lg", scrollable: !0, isOpen, onModalClose: onCancel, children: [
41
- /* @__PURE__ */ jsx(Modal.Header, { onModalClose: onCancel, children: t("homepage.notifications.filter-modal.title") }),
42
- /* @__PURE__ */ jsx(Modal.Subtitle, { children: t("homepage.notifications.filter-modal.subtitle") }),
43
- /* @__PURE__ */ jsxs(Modal.Body, { children: [
39
+ return /* @__PURE__ */ jsxs(ModalBeta, { id: "notification-filter-modal", size: "l", isOpen, onModalClose: onCancel, children: [
40
+ /* @__PURE__ */ jsx(ModalBeta.Header, { subtitle: t("homepage.notifications.filter-modal.subtitle"), onModalClose: onCancel, children: t("homepage.notifications.filter-modal.title") }),
41
+ /* @__PURE__ */ jsxs(ModalBeta.Body, { children: [
44
42
  /* @__PURE__ */ jsxs(Flex, { align: "center", gap: "8", className: "mb-24", children: [
45
43
  /* @__PURE__ */ jsxs("label", { className: "notification-filter-select-all notification-filter-chip", children: [
46
44
  /* @__PURE__ */ jsx("span", { children: t("homepage.notifications.filter-modal.select-all") }),
47
- /* @__PURE__ */ jsx("input", { type: "checkbox", ref: refSelectAllCheckbox, checked: selected.length === allTypes.length, className: "notification-filter-checkbox", onChange: toggleAll })
45
+ /* @__PURE__ */ jsx(Checkbox, { checked: selected.length === allTypes.length, indeterminate: selected.length > 0 && selected.length < allTypes.length, onChange: toggleAll })
48
46
  ] }),
49
47
  /* @__PURE__ */ jsx("span", { className: "notification-filter-count", children: t("homepage.notifications.filter-modal.count", {
50
48
  selected: selected.length,
@@ -62,11 +60,11 @@ const NotificationFilterModal = ({
62
60
  }), children: [
63
61
  /* @__PURE__ */ jsx(AppIcon, { app: appCode, size: "24", iconFit: "contain" }),
64
62
  /* @__PURE__ */ jsx("span", { children: label }),
65
- /* @__PURE__ */ jsx("input", { type: "checkbox", className: "notification-filter-checkbox", checked, onChange: () => toggleType(type) })
63
+ /* @__PURE__ */ jsx(Checkbox, { checked, onChange: () => toggleType(type) })
66
64
  ] }, type);
67
65
  }) })
68
66
  ] }),
69
- /* @__PURE__ */ jsxs(Modal.Footer, { children: [
67
+ /* @__PURE__ */ jsxs(ModalBeta.Footer, { children: [
70
68
  /* @__PURE__ */ jsx(ButtonBeta, { color: "tertiary", onClick: onCancel, type: "button", variant: "ghost", children: t("cancel") }),
71
69
  /* @__PURE__ */ jsx(ButtonBeta, { color: "default", onClick: () => onApply(selected), type: "button", variant: "filled", rightIcon: /* @__PURE__ */ jsx(SvgIconCheck, {}), children: t("homepage.notifications.filter-modal.confirm") })
72
70
  ] })
@@ -20,16 +20,10 @@ const NotificationOptionsMenu = ({
20
20
  } = useDeleteNotification(), [isReportModalOpen, setIsReportModalOpen] = useState(!1);
21
21
  return /* @__PURE__ */ jsxs(Fragment, { children: [
22
22
  /* @__PURE__ */ jsx(Dropdown, { overflow: !1, children: (triggerProps) => /* @__PURE__ */ jsxs(Fragment, { children: [
23
- /* @__PURE__ */ jsx(IconButton, { ...triggerProps, type: "button", "aria-label": t("homepage.notifications.options.label", {
24
- defaultValue: "Options de la notification"
25
- }), color: "tertiary", variant: "ghost", icon: /* @__PURE__ */ jsx(SvgIconOptions, {}), "data-testid": "notification-item-options-button" }),
23
+ /* @__PURE__ */ jsx(IconButton, { ...triggerProps, type: "button", "aria-label": t("homepage.notifications.options.label"), color: "tertiary", variant: "ghost", icon: /* @__PURE__ */ jsx(SvgIconOptions, {}), "data-testid": "notification-item-options-button" }),
26
24
  /* @__PURE__ */ jsxs(Dropdown.Menu, { children: [
27
- /* @__PURE__ */ jsx(Dropdown.Item, { icon: /* @__PURE__ */ jsx(SvgIconAlertTriangle, {}), onClick: () => setIsReportModalOpen(!0), children: t("homepage.notifications.options.report", {
28
- defaultValue: "Signaler"
29
- }) }),
30
- /* @__PURE__ */ jsx(Dropdown.Item, { icon: /* @__PURE__ */ jsx(SvgIconDelete, {}), onClick: () => deleteNotification(notificationId), children: t("homepage.notifications.options.delete", {
31
- defaultValue: "Supprimer"
32
- }) })
25
+ /* @__PURE__ */ jsx(Dropdown.Item, { icon: /* @__PURE__ */ jsx(SvgIconAlertTriangle, {}), onClick: () => setIsReportModalOpen(!0), children: t("homepage.notifications.options.report") }),
26
+ /* @__PURE__ */ jsx(Dropdown.Item, { icon: /* @__PURE__ */ jsx(SvgIconDelete, {}), onClick: () => deleteNotification(notificationId), children: t("homepage.notifications.options.delete") })
33
27
  ] })
34
28
  ] }) }),
35
29
  isReportModalOpen && /* @__PURE__ */ jsx(ReportNotificationModal, { isOpen: isReportModalOpen, onCancel: () => setIsReportModalOpen(!1), onConfirm: () => {
@@ -8,34 +8,36 @@ const resolveNotificationResourceUri = (params, type, appCode, eventType) => {
8
8
  return value;
9
9
  }, parseNotificationMessage = (message) => message.replace(/<\/a>/gi, "</strong>").replace(/<a\b[^>]*>/gi, "<strong>"), getAppCodeAndI18nKey = (appCode) => {
10
10
  appCode = appCode.toLowerCase();
11
- let appI18nKey = appCode;
11
+ const appI18nKey = `homepage.notification.type.${appCode}`;
12
12
  switch (appCode) {
13
+ case "collaborativeeditor":
14
+ appCode = "pad";
15
+ break;
13
16
  case "collaborativewall":
14
17
  appCode = "collaborative-wall";
15
18
  break;
16
19
  case "formulaire":
17
- appI18nKey = "formulaire", appCode = "forms";
20
+ appCode = "forms";
18
21
  break;
19
22
  case "messagerie":
20
- appI18nKey = appCode = "conversation";
23
+ appCode = "conversation";
21
24
  break;
22
25
  case "news":
23
- appI18nKey = appCode = "actualites";
26
+ appCode = "actualites";
24
27
  break;
25
28
  case "homeworks":
26
29
  appCode = "cahier-de-texte";
27
30
  break;
28
31
  case "userbook_motto":
29
- appI18nKey = "userbook_motto", appCode = "userbook";
30
- break;
31
32
  case "userbook_mood":
32
- appI18nKey = "userbook_mood", appCode = "userbook";
33
- break;
34
33
  case "userbook_discovervisiblegroups":
35
- appI18nKey = "userbook_discovervisiblegroups", appCode = "userbook";
34
+ appCode = "userbook";
35
+ break;
36
+ case "viescolaire":
37
+ appCode = "vie-scolaire";
36
38
  break;
37
39
  default:
38
- appI18nKey = appCode = appCode.replace(/_/g, "-");
40
+ appCode = appCode.replace(/_/g, "-");
39
41
  }
40
42
  return [appCode, appI18nKey];
41
43
  }, notificationAdapter = (notification) => {
@@ -14,31 +14,30 @@ const useNotificationListContainer = () => {
14
14
  mutate: saveTimelinePreference
15
15
  } = useSaveTimelinePreference(), [selectedTypes, setSelectedTypesState] = useState();
16
16
  useEffect(() => {
17
- if (selectedTypes === void 0 && isFetchedTypes && isFetchedPreference) {
18
- const preferredTypes = preference == null ? void 0 : preference.type;
19
- setSelectedTypesState(preferredTypes && preferredTypes.length > 0 ? preferredTypes : notificationTypes ?? []);
20
- }
17
+ selectedTypes === void 0 && isFetchedTypes && isFetchedPreference && setSelectedTypesState((preference == null ? void 0 : preference.type) ?? notificationTypes ?? []);
21
18
  }, [selectedTypes, isFetchedTypes, isFetchedPreference, preference, notificationTypes]);
22
19
  const setSelectedTypes = (types) => {
23
20
  setSelectedTypesState(types), saveTimelinePreference({
24
21
  ...preference,
25
22
  type: types
26
23
  });
27
- }, {
28
- data: notifications,
24
+ }, hasNoTypeSelected = (selectedTypes == null ? void 0 : selectedTypes.length) === 0, {
25
+ data,
29
26
  hasNextPage,
30
27
  isLoading: isLoadingNotifications,
31
28
  error: errorNotifications,
32
29
  fetchNextPage
33
- } = useNotifications(selectedTypes ?? [], isFetchedTypes && !!selectedTypes);
30
+ } = useNotifications(selectedTypes ?? [], isFetchedTypes && !!selectedTypes && !hasNoTypeSelected);
34
31
  return {
35
- notifications,
32
+ notifications: hasNoTypeSelected ? [] : data,
36
33
  notificationTypes,
37
34
  selectedTypes,
38
35
  setSelectedTypes,
39
- hasNextPage,
36
+ hasNextPage: hasNoTypeSelected ? !1 : hasNextPage,
40
37
  loadNextPage: () => fetchNextPage(),
41
- isLoading: isLoadingTypes || isLoadingNotifications,
38
+ // The notification query stays disabled until the saved filter is
39
+ // read, so this wait must count as loading too.
40
+ isLoading: isLoadingTypes || selectedTypes === void 0 || isLoadingNotifications,
42
41
  error: errorTypes || errorNotifications
43
42
  };
44
43
  }, useHasNotificationToday = () => {
@@ -27,7 +27,7 @@ function UserSpace({
27
27
  /* @__PURE__ */ jsx("div", { "data-testid": "user-space-profile", className: "user-space--profile", children: t(profile) })
28
28
  ] })
29
29
  ] }),
30
- onCustomizeWidgetsClick && /* @__PURE__ */ jsx(IconButton, { className: "user-space--customize-widgets", icon: /* @__PURE__ */ jsx(SvgIconAddWidget, {}), variant: "ghost", color: "tertiary", "aria-label": t("homepage.userSpace.customizeWidgets", "Personnaliser mes widgets"), onClick: onCustomizeWidgetsClick })
30
+ onCustomizeWidgetsClick && /* @__PURE__ */ jsx(IconButton, { className: "user-space--customize-widgets", icon: /* @__PURE__ */ jsx(SvgIconAddWidget, {}), variant: "ghost", color: "tertiary", "aria-label": t("homepage.userSpace.customizeWidgets"), onClick: onCustomizeWidgetsClick })
31
31
  ] }),
32
32
  children && /* @__PURE__ */ jsx(HomeCard.Content, { children })
33
33
  ] });
@@ -1,3 +1,4 @@
1
+ export * from './Agenda';
1
2
  export * from './Communities';
2
3
  export * from './Favorites';
3
4
  export * from './Header';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@edifice.io/react",
3
- "version": "2.6.7-develop-b2school.20261009142401",
3
+ "version": "2.6.7-develop-b2school.20261009185039",
4
4
  "description": "Edifice React Library",
5
5
  "keywords": [
6
6
  "react",
@@ -140,9 +140,9 @@
140
140
  "swiper": "10.1.0",
141
141
  "ua-parser-js": "1.0.36",
142
142
  "zustand": "4.5.7",
143
- "@edifice.io/bootstrap": "2.6.7-develop-b2school.20261009142401",
144
- "@edifice.io/tiptap-extensions": "2.6.7-develop-b2school.20261009142401",
145
- "@edifice.io/utilities": "2.6.7-develop-b2school.20261009142401"
143
+ "@edifice.io/bootstrap": "2.6.7-develop-b2school.20261009185039",
144
+ "@edifice.io/tiptap-extensions": "2.6.7-develop-b2school.20261009185039",
145
+ "@edifice.io/utilities": "2.6.7-develop-b2school.20261009185039"
146
146
  },
147
147
  "devDependencies": {
148
148
  "@babel/plugin-transform-react-pure-annotations": "7.27.1",
@@ -173,8 +173,8 @@
173
173
  "vite": "5.4.14",
174
174
  "vite-plugin-dts": "4.5.4",
175
175
  "vite-tsconfig-paths": "5.1.4",
176
- "@edifice.io/client": "2.6.7-develop-b2school.20261009142401",
177
- "@edifice.io/config": "2.6.7-develop-b2school.20261009142401"
176
+ "@edifice.io/config": "2.6.7-develop-b2school.20261009185039",
177
+ "@edifice.io/client": "2.6.7-develop-b2school.20261009185039"
178
178
  },
179
179
  "peerDependencies": {
180
180
  "@react-spring/web": "9.7.5",