From 6138a6891e37314501cf564ad422ac5d879c4a28 Mon Sep 17 00:00:00 2001 From: jcbe-ode Date: Fri, 20 Mar 2026 16:37:50 +0100 Subject: [PATCH 1/9] Base implementation --- .../SchoolWidget/SchoolWidget.stories.tsx | 105 ++++++++++++++++++ .../components/SchoolWidget/SchoolWidget.tsx | 52 +++++++++ .../components/SchoolWidget/useUserSchools.ts | 22 ++++ 3 files changed, 179 insertions(+) create mode 100644 packages/react/src/modules/homepage/components/SchoolWidget/SchoolWidget.stories.tsx create mode 100644 packages/react/src/modules/homepage/components/SchoolWidget/SchoolWidget.tsx create mode 100644 packages/react/src/modules/homepage/components/SchoolWidget/useUserSchools.ts diff --git a/packages/react/src/modules/homepage/components/SchoolWidget/SchoolWidget.stories.tsx b/packages/react/src/modules/homepage/components/SchoolWidget/SchoolWidget.stories.tsx new file mode 100644 index 000000000..c9addbe14 --- /dev/null +++ b/packages/react/src/modules/homepage/components/SchoolWidget/SchoolWidget.stories.tsx @@ -0,0 +1,105 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import SchoolWidget, { SchoolWidgetProps } from './SchoolWidget'; + +const meta: Meta = { + title: 'Modules/Homepage/SchoolWidget', + component: SchoolWidget, + decorators: [ + (Story) => ( +
+
+ +
+ ), + ], + parameters: { + docs: { + description: { + component: 'Description 1', + }, + }, + }, +}; + +export default meta; +type Story = StoryObj; + +const renderWithProps = (props: SchoolWidgetProps) => () => ( +
+ +
+); + +export const MultipleSchools: Story = { + render: renderWithProps({ + schools: [ + { + id: 'school-1', + name: 'Collège Jean Moulin', + UAI: '0012345A', + classes: [], + exports: [], + }, + { + id: 'school-2', + name: 'Lycée Louise Michel', + UAI: '0098765Z', + classes: [], + exports: [], + }, + ], + selectedSchool: { + id: 'school-2', + name: 'Lycée Louise Michel', + UAI: '0098765Z', + classes: [], + exports: [], + }, + }), + parameters: { + docs: { + description: { + story: 'Description for many schools', + }, + }, + }, +}; + +export const SingleSchool: Story = { + render: renderWithProps({ + schools: [ + { + id: 'school-1', + name: 'Collège Jean Moulin', + UAI: '0012345A', + classes: [], + exports: [], + }, + ], + selectedSchool: { + id: 'school-1', + name: 'Collège Jean Moulin', + UAI: '0012345A', + classes: [], + exports: [], + }, + }), + parameters: { + docs: { + description: { + story: 'Description for 1 school', + }, + }, + }, +}; + +export const Empty: Story = { + render: renderWithProps({ selectedSchool: undefined }), + parameters: { + docs: { + description: { + story: 'Description for no school', + }, + }, + }, +}; diff --git a/packages/react/src/modules/homepage/components/SchoolWidget/SchoolWidget.tsx b/packages/react/src/modules/homepage/components/SchoolWidget/SchoolWidget.tsx new file mode 100644 index 000000000..877eea8de --- /dev/null +++ b/packages/react/src/modules/homepage/components/SchoolWidget/SchoolWidget.tsx @@ -0,0 +1,52 @@ +import { School } from '@edifice.io/client'; + +export interface SchoolWidgetProps { + selectedSchool: School | undefined; + onSelectedSchoolChange?: (schoolIndex: number) => void; + schools?: School[]; +} + +const SchoolWidget = ({ + schools, + selectedSchool, + onSelectedSchoolChange, +}: SchoolWidgetProps) => { + const hasManySchools = schools && schools.length > 1; + + return ( +
+ {!selectedSchool ? ( +

Aucun établissement trouvé.

+ ) : ( +
+ {selectedSchool.name} + {selectedSchool.UAI && UAI : {selectedSchool.UAI}} +
+ + /* + hasManySchools ? ( +
    + {schools.map((school) => ( +
  • +
    + {school.name} + {school.UAI && UAI : {school.UAI}} +
    +
  • + ))} +
+ ) : ( +
+ {selectedSchool.name} + {selectedSchool.UAI && UAI : {selectedSchool.UAI}} +
+ ) + */ + )} +
+ ); +}; + +SchoolWidget.displayName = 'SchoolWidget'; + +export default SchoolWidget; diff --git a/packages/react/src/modules/homepage/components/SchoolWidget/useUserSchools.ts b/packages/react/src/modules/homepage/components/SchoolWidget/useUserSchools.ts new file mode 100644 index 000000000..d7deda3e4 --- /dev/null +++ b/packages/react/src/modules/homepage/components/SchoolWidget/useUserSchools.ts @@ -0,0 +1,22 @@ +import { School } from '@edifice.io/client'; +import { useEffect, useState } from 'react'; +import { useEdificeClient } from 'src/providers'; + +export function useUserSchools() { + const { userDescription } = useEdificeClient(); + const [selectedSchool, setSelectedSchool] = useState(); + + const schools = userDescription?.schools; + + useEffect(() => { + setSelectedSchool(schools?.[0]); + }, []); + + return { + schools: schools ?? [], + selectedSchool, + handleSelectedSchoolChange: (school: School) => { + setSelectedSchool(school); + }, + }; +} From d9f8789c325aee6d8809cc9f8824c91d7eb4d3f4 Mon Sep 17 00:00:00 2001 From: jcbe-ode Date: Fri, 20 Mar 2026 18:10:30 +0100 Subject: [PATCH 2/9] css begin --- .../components/SchoolWidget/SchoolWidget.css | 26 +++++++++++++++++++ .../SchoolWidget/SchoolWidget.stories.tsx | 4 +-- .../components/SchoolWidget/SchoolWidget.tsx | 15 ++++------- .../SchoolWidget/SchoolWidgetBackground.svg | 3 +++ 4 files changed, 36 insertions(+), 12 deletions(-) create mode 100644 packages/react/src/modules/homepage/components/SchoolWidget/SchoolWidget.css create mode 100644 packages/react/src/modules/homepage/components/SchoolWidget/SchoolWidgetBackground.svg diff --git a/packages/react/src/modules/homepage/components/SchoolWidget/SchoolWidget.css b/packages/react/src/modules/homepage/components/SchoolWidget/SchoolWidget.css new file mode 100644 index 000000000..5046ed513 --- /dev/null +++ b/packages/react/src/modules/homepage/components/SchoolWidget/SchoolWidget.css @@ -0,0 +1,26 @@ +:root { + --school-widget-border-color: #ffe5a3; + --school-widget-bg-color: #fefaec; + --school-widget-selected-color: #383838; +} + +.school-widget { + padding: 1.4rem 0.4rem; + border: 1px solid var(--school-widget-border-color); + border-radius: 2.4rem; + + background-color: var(--school-widget-bg-color); + background-image: url('./SchoolWidgetBackground.svg'); + background-repeat: no-repeat; + background-position: bottom right; +} + +.school-widget__selected { + width: 100%; + text-align: center; + font-weight: 700; + color: var(--school-widget-selected-color); + font-size: 1.6rem; + line-height: 2.2rem; + margin: 0.8rem; +} diff --git a/packages/react/src/modules/homepage/components/SchoolWidget/SchoolWidget.stories.tsx b/packages/react/src/modules/homepage/components/SchoolWidget/SchoolWidget.stories.tsx index c9addbe14..37576b6e4 100644 --- a/packages/react/src/modules/homepage/components/SchoolWidget/SchoolWidget.stories.tsx +++ b/packages/react/src/modules/homepage/components/SchoolWidget/SchoolWidget.stories.tsx @@ -42,7 +42,7 @@ export const MultipleSchools: Story = { }, { id: 'school-2', - name: 'Lycée Louise Michel', + name: 'Lycée Jeanne Ferry de Loisette en Royan', UAI: '0098765Z', classes: [], exports: [], @@ -50,7 +50,7 @@ export const MultipleSchools: Story = { ], selectedSchool: { id: 'school-2', - name: 'Lycée Louise Michel', + name: 'Lycée Jeanne Ferry de Loisette en Royan', UAI: '0098765Z', classes: [], exports: [], diff --git a/packages/react/src/modules/homepage/components/SchoolWidget/SchoolWidget.tsx b/packages/react/src/modules/homepage/components/SchoolWidget/SchoolWidget.tsx index 877eea8de..fd32a6b9d 100644 --- a/packages/react/src/modules/homepage/components/SchoolWidget/SchoolWidget.tsx +++ b/packages/react/src/modules/homepage/components/SchoolWidget/SchoolWidget.tsx @@ -1,4 +1,5 @@ import { School } from '@edifice.io/client'; +import './SchoolWidget.css'; export interface SchoolWidgetProps { selectedSchool: School | undefined; @@ -15,15 +16,9 @@ const SchoolWidget = ({ return (
- {!selectedSchool ? ( -

Aucun établissement trouvé.

- ) : ( -
- {selectedSchool.name} - {selectedSchool.UAI && UAI : {selectedSchool.UAI}} -
- - /* + {selectedSchool ? ( +
{selectedSchool.name}
+ ) : /* hasManySchools ? (
    {schools.map((school) => ( @@ -42,7 +37,7 @@ const SchoolWidget = ({
) */ - )} + null}
); }; diff --git a/packages/react/src/modules/homepage/components/SchoolWidget/SchoolWidgetBackground.svg b/packages/react/src/modules/homepage/components/SchoolWidget/SchoolWidgetBackground.svg new file mode 100644 index 000000000..278b25d24 --- /dev/null +++ b/packages/react/src/modules/homepage/components/SchoolWidget/SchoolWidgetBackground.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file From 7b7ad1dd821f4c22cb8de85edcecc82815815b04 Mon Sep 17 00:00:00 2001 From: jcbe-ode Date: Fri, 20 Mar 2026 18:11:32 +0100 Subject: [PATCH 3/9] fix client interface --- packages/client/src/ts/session/interfaces.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client/src/ts/session/interfaces.ts b/packages/client/src/ts/session/interfaces.ts index 7528d900a..1d66dde92 100644 --- a/packages/client/src/ts/session/interfaces.ts +++ b/packages/client/src/ts/session/interfaces.ts @@ -117,7 +117,7 @@ export type School = { id: string; // "09772a06-1362-4802-a475-66a87d9cb679" name: string; // "MY DEV SCHOOL" UAI: string; // "1111888G" - exports: string[]; // ["GAR-P0"] + exports: string[] | null; // ["GAR-P0"] }; export type UserProfile = Array< 'Student' | 'Teacher' | 'Relative' | 'Personnel' | 'Guest' From 36efe292eadaed32d773c3961980adf5b7761a48 Mon Sep 17 00:00:00 2001 From: jcbe-ode Date: Mon, 23 Mar 2026 14:48:03 +0100 Subject: [PATCH 4/9] multiple schools support --- .../components/SchoolWidget/SchoolWidget.css | 11 --- .../SchoolWidget/SchoolWidget.stories.tsx | 73 ++++++++------- .../components/SchoolWidget/SchoolWidget.tsx | 88 ++++++++++++++----- 3 files changed, 101 insertions(+), 71 deletions(-) diff --git a/packages/react/src/modules/homepage/components/SchoolWidget/SchoolWidget.css b/packages/react/src/modules/homepage/components/SchoolWidget/SchoolWidget.css index 5046ed513..dc0a17e6a 100644 --- a/packages/react/src/modules/homepage/components/SchoolWidget/SchoolWidget.css +++ b/packages/react/src/modules/homepage/components/SchoolWidget/SchoolWidget.css @@ -5,7 +5,6 @@ } .school-widget { - padding: 1.4rem 0.4rem; border: 1px solid var(--school-widget-border-color); border-radius: 2.4rem; @@ -14,13 +13,3 @@ background-repeat: no-repeat; background-position: bottom right; } - -.school-widget__selected { - width: 100%; - text-align: center; - font-weight: 700; - color: var(--school-widget-selected-color); - font-size: 1.6rem; - line-height: 2.2rem; - margin: 0.8rem; -} diff --git a/packages/react/src/modules/homepage/components/SchoolWidget/SchoolWidget.stories.tsx b/packages/react/src/modules/homepage/components/SchoolWidget/SchoolWidget.stories.tsx index 37576b6e4..511768997 100644 --- a/packages/react/src/modules/homepage/components/SchoolWidget/SchoolWidget.stories.tsx +++ b/packages/react/src/modules/homepage/components/SchoolWidget/SchoolWidget.stories.tsx @@ -15,7 +15,8 @@ const meta: Meta = { parameters: { docs: { description: { - component: 'Description 1', + component: + "Ce storybook documente le composant SchoolWidget, un widget de sélection d'école avec plusieurs variantes possibles.", }, }, }, @@ -25,29 +26,31 @@ export default meta; type Story = StoryObj; const renderWithProps = (props: SchoolWidgetProps) => () => ( -
+
); +const schools = [ + { + id: 'school-1', + name: 'Collège Jean Moulin', + UAI: '0012345A', + classes: [], + exports: [], + }, + { + id: 'school-2', + name: 'Lycée Jeanne Ferry de Loisette en Royan', + UAI: '0098765Z', + classes: [], + exports: [], + }, +]; + export const MultipleSchools: Story = { render: renderWithProps({ - schools: [ - { - id: 'school-1', - name: 'Collège Jean Moulin', - UAI: '0012345A', - classes: [], - exports: [], - }, - { - id: 'school-2', - name: 'Lycée Jeanne Ferry de Loisette en Royan', - UAI: '0098765Z', - classes: [], - exports: [], - }, - ], + schools, selectedSchool: { id: 'school-2', name: 'Lycée Jeanne Ferry de Loisette en Royan', @@ -55,11 +58,21 @@ export const MultipleSchools: Story = { classes: [], exports: [], }, + onSelectedSchoolChange: (idx) => + alert( + `School id=${schools[idx].id} UAI=${schools[idx].UAI} is selected.`, + ), }), parameters: { docs: { description: { - story: 'Description for many schools', + story: ` +Affiche une liste de plusieurs écoles avec sélection active. +
    +
  • 2 écoles disponibles (Collège Jean Moulin, Lycée Jeanne Ferry de Loisette en Royan)
  • +
  • École sélectionnée : Lycée Jeanne Ferry
  • +
  • Callback au changement de sélection
  • +
`, }, }, }, @@ -67,27 +80,13 @@ export const MultipleSchools: Story = { export const SingleSchool: Story = { render: renderWithProps({ - schools: [ - { - id: 'school-1', - name: 'Collège Jean Moulin', - UAI: '0012345A', - classes: [], - exports: [], - }, - ], - selectedSchool: { - id: 'school-1', - name: 'Collège Jean Moulin', - UAI: '0012345A', - classes: [], - exports: [], - }, + schools: [schools[0]], + selectedSchool: schools[0], }), parameters: { docs: { description: { - story: 'Description for 1 school', + story: `Affiche une seule école`, }, }, }, @@ -98,7 +97,7 @@ export const Empty: Story = { parameters: { docs: { description: { - story: 'Description for no school', + story: `État vide (aucune école)`, }, }, }, diff --git a/packages/react/src/modules/homepage/components/SchoolWidget/SchoolWidget.tsx b/packages/react/src/modules/homepage/components/SchoolWidget/SchoolWidget.tsx index fd32a6b9d..edceabb8a 100644 --- a/packages/react/src/modules/homepage/components/SchoolWidget/SchoolWidget.tsx +++ b/packages/react/src/modules/homepage/components/SchoolWidget/SchoolWidget.tsx @@ -1,4 +1,8 @@ import { School } from '@edifice.io/client'; +import { Dropdown, Flex, IconButton, useToggle } from '@edifice.io/react'; +import { IconRafterDown } from '@edifice.io/react/icons'; +import { RefAttributes } from 'react'; +import { useTranslation } from 'react-i18next'; import './SchoolWidget.css'; export interface SchoolWidgetProps { @@ -12,32 +16,70 @@ const SchoolWidget = ({ selectedSchool, onSelectedSchoolChange, }: SchoolWidgetProps) => { + const [isExpanded, toggleExpanded] = useToggle(false); + const { t } = useTranslation(); + const hasManySchools = schools && schools.length > 1; + const widgetStyle = { padding: '1.4rem 0.4rem' }; + const containerStyle = { padding: '0.8rem' }; + const selectedSchoolStyle = { + 'padding': '.4rem 2.9rem', + 'font-size': '1.6rem', + 'line-height': '2.2rem', + 'color': 'var(--school-widget-selected-color)', + }; + + if (!selectedSchool) return null; + return ( -
- {selectedSchool ? ( -
{selectedSchool.name}
- ) : /* - hasManySchools ? ( -
    - {schools.map((school) => ( -
  • -
    - {school.name} - {school.UAI && UAI : {school.UAI}} -
    -
  • - ))} -
- ) : ( -
- {selectedSchool.name} - {selectedSchool.UAI && UAI : {selectedSchool.UAI}} -
- ) - */ - null} +
+
+ + {selectedSchool.name} + {hasManySchools && ( + + {(triggerProps: RefAttributes) => ( + <> + + } + /> + + {schools.map((school, index) => ( + onSelectedSchoolChange?.(index)} + > + +

{school.name}

+ {school.UAI &&

UAI : {school.UAI}

} +
+
+ ))} +
+ + )} +
+ )} +
+
); }; From 5d6fcf40977e5e7bed2d6fa35d33724847406fd5 Mon Sep 17 00:00:00 2001 From: jcbe-ode Date: Mon, 23 Mar 2026 16:57:36 +0100 Subject: [PATCH 5/9] add widgets preferences support --- .../components/SchoolWidget/useUserSchools.ts | 27 ++++++++++++++++++- .../homepage/hooks/useWidgetPreferences.ts | 22 +++++++++++++++ .../modules/homepage/types/WidgetUserPref.ts | 17 ++++++++++++ .../EdificeClientProvider.tsx | 8 ------ 4 files changed, 65 insertions(+), 9 deletions(-) create mode 100644 packages/react/src/modules/homepage/hooks/useWidgetPreferences.ts create mode 100644 packages/react/src/modules/homepage/types/WidgetUserPref.ts diff --git a/packages/react/src/modules/homepage/components/SchoolWidget/useUserSchools.ts b/packages/react/src/modules/homepage/components/SchoolWidget/useUserSchools.ts index d7deda3e4..4b7b14104 100644 --- a/packages/react/src/modules/homepage/components/SchoolWidget/useUserSchools.ts +++ b/packages/react/src/modules/homepage/components/SchoolWidget/useUserSchools.ts @@ -1,9 +1,12 @@ -import { School } from '@edifice.io/client'; +import { School, WIDGET_NAME, WidgetUserPref } from '@edifice.io/client'; import { useEffect, useState } from 'react'; import { useEdificeClient } from 'src/providers'; +import useWidgetPreferences from '../../hooks/useWidgetPreferences'; export function useUserSchools() { const { userDescription } = useEdificeClient(); + const { lookup, saveUserPreferences } = useWidgetPreferences(); + const [userPreferences, setUserPreferences] = useState(); const [selectedSchool, setSelectedSchool] = useState(); const schools = userDescription?.schools; @@ -12,11 +15,33 @@ export function useUserSchools() { setSelectedSchool(schools?.[0]); }, []); + useEffect(() => { + const userPref = lookup?.(WIDGET_NAME.SCHOOL)?.userPref; + setUserPreferences(userPref); + }, [lookup]); + + useEffect(() => { + if (userPreferences?.schoolId && Array.isArray(schools)) { + const index = schools.findIndex( + (school) => school.id === userPreferences?.schoolId, + ); + setSelectedSchool( + index < 0 || index >= schools.length ? schools[0] : schools[index], + ); + } else { + setSelectedSchool(undefined); + } + }, [userPreferences]); + return { schools: schools ?? [], selectedSchool, handleSelectedSchoolChange: (school: School) => { setSelectedSchool(school); + if (userPreferences) { + userPreferences.schoolId = school.id; + saveUserPreferences(); + } }, }; } diff --git a/packages/react/src/modules/homepage/hooks/useWidgetPreferences.ts b/packages/react/src/modules/homepage/hooks/useWidgetPreferences.ts new file mode 100644 index 000000000..86906d589 --- /dev/null +++ b/packages/react/src/modules/homepage/hooks/useWidgetPreferences.ts @@ -0,0 +1,22 @@ +import { IWidgetFramework, WidgetFrameworkFactory } from '@edifice.io/client'; +import { useMutation } from '@tanstack/react-query'; +import { useEffect, useState } from 'react'; + +export default function useWidgetPreferences() { + const [svc, setSvc] = useState(); + + const saveMutation = useMutation({ + mutationFn: svc?.saveUserPrefs, + }); + + useEffect(() => { + const widgetService = WidgetFrameworkFactory.instance(); + widgetService.initialize(null, null).then(() => setSvc(widgetService)); + }, []); + + return { + list: svc?.list, + lookup: svc?.lookup, + saveUserPreferences: saveMutation.mutateAsync, + }; +} diff --git a/packages/react/src/modules/homepage/types/WidgetUserPref.ts b/packages/react/src/modules/homepage/types/WidgetUserPref.ts new file mode 100644 index 000000000..47e086e69 --- /dev/null +++ b/packages/react/src/modules/homepage/types/WidgetUserPref.ts @@ -0,0 +1,17 @@ +export type WidgetUserPref = { + //-------------------------------------------------------------------------- + // General user's preferences, available for every widget. + //-------------------------------------------------------------------------- + /** Boolean indicating wether the user wants to see this widget, or not. */ + show: boolean; + /** Integer defining the sort order of this widget. */ + index: number; + /** Prefered column on-screen. */ + position?: 'left' | 'right'; +} & { + //-------------------------------------------------------------------------- + // Specific user's preferences, available to some widgets only. + //-------------------------------------------------------------------------- + /** Lastest selected school ID, in SchoolWidget */ + schoolId?: string; +}; diff --git a/packages/react/src/providers/EdificeClientProvider/EdificeClientProvider.tsx b/packages/react/src/providers/EdificeClientProvider/EdificeClientProvider.tsx index a73177a2a..9b4d0a854 100644 --- a/packages/react/src/providers/EdificeClientProvider/EdificeClientProvider.tsx +++ b/packages/react/src/providers/EdificeClientProvider/EdificeClientProvider.tsx @@ -1,6 +1,5 @@ import { useEffect, useMemo } from 'react'; -import { App } from '@edifice.io/client'; import { useTranslation } from 'react-i18next'; import { useConf } from '../../hooks/useConf'; import { useSession } from '../../hooks/useSession'; @@ -9,13 +8,6 @@ import { EdificeClientProviderProps, } from './EdificeClientProvider.context'; -export interface OdeProviderParams { - alternativeApp?: boolean; - app: App; - cdnDomain?: string | null; - version?: string | null; -} - export function EdificeClientProvider({ children, params, From 141f1050612dd6c2a76307509c995b3f36cbdadf Mon Sep 17 00:00:00 2001 From: jcbe-ode Date: Mon, 23 Mar 2026 16:58:32 +0100 Subject: [PATCH 6/9] remove unused file --- .../modules/homepage/types/WidgetUserPref.ts | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 packages/react/src/modules/homepage/types/WidgetUserPref.ts diff --git a/packages/react/src/modules/homepage/types/WidgetUserPref.ts b/packages/react/src/modules/homepage/types/WidgetUserPref.ts deleted file mode 100644 index 47e086e69..000000000 --- a/packages/react/src/modules/homepage/types/WidgetUserPref.ts +++ /dev/null @@ -1,17 +0,0 @@ -export type WidgetUserPref = { - //-------------------------------------------------------------------------- - // General user's preferences, available for every widget. - //-------------------------------------------------------------------------- - /** Boolean indicating wether the user wants to see this widget, or not. */ - show: boolean; - /** Integer defining the sort order of this widget. */ - index: number; - /** Prefered column on-screen. */ - position?: 'left' | 'right'; -} & { - //-------------------------------------------------------------------------- - // Specific user's preferences, available to some widgets only. - //-------------------------------------------------------------------------- - /** Lastest selected school ID, in SchoolWidget */ - schoolId?: string; -}; From ad4540ebc44055bdd3c09e42f7e8efeabbb1e313 Mon Sep 17 00:00:00 2001 From: jcbe-ode Date: Tue, 24 Mar 2026 09:55:42 +0100 Subject: [PATCH 7/9] feedback from copilot --- .../homepage/components/SchoolWidget/SchoolWidget.css | 2 +- .../homepage/components/SchoolWidget/SchoolWidget.tsx | 5 +++-- .../homepage/components/SchoolWidget/useUserSchools.ts | 4 ++++ 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/react/src/modules/homepage/components/SchoolWidget/SchoolWidget.css b/packages/react/src/modules/homepage/components/SchoolWidget/SchoolWidget.css index dc0a17e6a..71b5afa65 100644 --- a/packages/react/src/modules/homepage/components/SchoolWidget/SchoolWidget.css +++ b/packages/react/src/modules/homepage/components/SchoolWidget/SchoolWidget.css @@ -1,4 +1,4 @@ -:root { +.school-widget { --school-widget-border-color: #ffe5a3; --school-widget-bg-color: #fefaec; --school-widget-selected-color: #383838; diff --git a/packages/react/src/modules/homepage/components/SchoolWidget/SchoolWidget.tsx b/packages/react/src/modules/homepage/components/SchoolWidget/SchoolWidget.tsx index edceabb8a..867541c8a 100644 --- a/packages/react/src/modules/homepage/components/SchoolWidget/SchoolWidget.tsx +++ b/packages/react/src/modules/homepage/components/SchoolWidget/SchoolWidget.tsx @@ -1,7 +1,6 @@ import { School } from '@edifice.io/client'; import { Dropdown, Flex, IconButton, useToggle } from '@edifice.io/react'; import { IconRafterDown } from '@edifice.io/react/icons'; -import { RefAttributes } from 'react'; import { useTranslation } from 'react-i18next'; import './SchoolWidget.css'; @@ -44,7 +43,9 @@ const SchoolWidget = ({ {selectedSchool.name} {hasManySchools && ( - {(triggerProps: RefAttributes) => ( + {( + triggerProps: React.ComponentPropsWithRef, + ) => ( <> { setSelectedSchool(schools?.[0]); }, []); + // Memoize user's preferences for this widget, depending on the 'lookup' function availability. useEffect(() => { const userPref = lookup?.(WIDGET_NAME.SCHOOL)?.userPref; setUserPreferences(userPref); }, [lookup]); + // Select the user's prefered school useEffect(() => { if (userPreferences?.schoolId && Array.isArray(schools)) { const index = schools.findIndex( @@ -39,6 +42,7 @@ export function useUserSchools() { handleSelectedSchoolChange: (school: School) => { setSelectedSchool(school); if (userPreferences) { + // Update user's preferences and save them userPreferences.schoolId = school.id; saveUserPreferences(); } From e61bf714c89f70edc2dd7e7e8ad319fa6803fb6a Mon Sep 17 00:00:00 2001 From: jcbe-ode Date: Tue, 24 Mar 2026 11:08:44 +0100 Subject: [PATCH 8/9] feedback from copilot --- .../components/SchoolWidget/SchoolWidget.tsx | 12 ++++++------ .../components/SchoolWidget/useUserSchools.ts | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/react/src/modules/homepage/components/SchoolWidget/SchoolWidget.tsx b/packages/react/src/modules/homepage/components/SchoolWidget/SchoolWidget.tsx index 867541c8a..f32d7fb3d 100644 --- a/packages/react/src/modules/homepage/components/SchoolWidget/SchoolWidget.tsx +++ b/packages/react/src/modules/homepage/components/SchoolWidget/SchoolWidget.tsx @@ -1,7 +1,7 @@ import { School } from '@edifice.io/client'; -import { Dropdown, Flex, IconButton, useToggle } from '@edifice.io/react'; -import { IconRafterDown } from '@edifice.io/react/icons'; import { useTranslation } from 'react-i18next'; +import { Dropdown, Flex, IconButton, useToggle } from '../../../..'; +import { IconRafterDown } from '../../../../modules/icons/components'; import './SchoolWidget.css'; export interface SchoolWidgetProps { @@ -23,10 +23,10 @@ const SchoolWidget = ({ const widgetStyle = { padding: '1.4rem 0.4rem' }; const containerStyle = { padding: '0.8rem' }; const selectedSchoolStyle = { - 'padding': '.4rem 2.9rem', - 'font-size': '1.6rem', - 'line-height': '2.2rem', - 'color': 'var(--school-widget-selected-color)', + padding: '.4rem 2.9rem', + fontSize: '1.6rem', + lineHeight: '2.2rem', + color: 'var(--school-widget-selected-color)', }; if (!selectedSchool) return null; diff --git a/packages/react/src/modules/homepage/components/SchoolWidget/useUserSchools.ts b/packages/react/src/modules/homepage/components/SchoolWidget/useUserSchools.ts index 7b628dd7c..7b0664eb0 100644 --- a/packages/react/src/modules/homepage/components/SchoolWidget/useUserSchools.ts +++ b/packages/react/src/modules/homepage/components/SchoolWidget/useUserSchools.ts @@ -34,7 +34,7 @@ export function useUserSchools() { } else { setSelectedSchool(undefined); } - }, [userPreferences]); + }, [userPreferences, schools]); return { schools: schools ?? [], From ec88d15cf4d8d4a4bc75eb060d3a035adde78b78 Mon Sep 17 00:00:00 2001 From: jcbe-ode Date: Tue, 24 Mar 2026 12:03:56 +0100 Subject: [PATCH 9/9] add tests --- .../SchoolWidget/useUserSchools.spec.ts | 122 ++++++++++++++++++ packages/react/vite.config.ts | 2 +- 2 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 packages/react/src/modules/homepage/components/SchoolWidget/useUserSchools.spec.ts diff --git a/packages/react/src/modules/homepage/components/SchoolWidget/useUserSchools.spec.ts b/packages/react/src/modules/homepage/components/SchoolWidget/useUserSchools.spec.ts new file mode 100644 index 000000000..08eedb0cf --- /dev/null +++ b/packages/react/src/modules/homepage/components/SchoolWidget/useUserSchools.spec.ts @@ -0,0 +1,122 @@ +// packages/react/src/modules/homepage/components/SchoolWidget/useUserSchools.test.tsx +import { act, renderHook, waitFor } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { useUserSchools } from './useUserSchools'; + +const mocks = vi.hoisted(() => ({ + useEdificeClient: vi.fn(), + useWidgetPreferences: vi.fn(), + lookup: vi.fn(), + saveUserPreferences: vi.fn(), +})); + +vi.mock('src/providers', () => ({ + useEdificeClient: mocks.useEdificeClient, +})); + +vi.mock('../../hooks/useWidgetPreferences', () => ({ + default: mocks.useWidgetPreferences, +})); + +vi.mock('@edifice.io/client', () => ({ + WIDGET_NAME: { SCHOOL: 'SCHOOL' }, +})); + +describe('useUserSchools', () => { + const schools = [ + { id: 's1', name: 'School 1' }, + { id: 's2', name: 'School 2' }, + ] as any[]; + + beforeEach(() => { + vi.clearAllMocks(); + + mocks.useEdificeClient.mockReturnValue({ + userDescription: { schools }, + }); + + mocks.lookup.mockReturnValue(undefined); + + mocks.useWidgetPreferences.mockReturnValue({ + lookup: mocks.lookup, + saveUserPreferences: mocks.saveUserPreferences, + }); + }); + + it('returns empty schools when user has no schools', () => { + mocks.useEdificeClient.mockReturnValue({ + userDescription: { schools: null }, + }); + + const { result } = renderHook(() => useUserSchools()); + + expect(result.current.schools).toEqual([]); + expect(result.current.selectedSchool).toBeUndefined(); + }); + + it('selects preferred school when preference exists', async () => { + const userPref = { schoolId: 's2' }; + mocks.lookup.mockReturnValue({ userPref }); + + const { result } = renderHook(() => useUserSchools()); + + await waitFor(() => { + expect(result.current.selectedSchool?.id).toBe('s2'); + }); + + expect(mocks.lookup).toHaveBeenCalledWith('SCHOOL'); + }); + + it('falls back to first school when preferred school is missing', async () => { + const userPref = { schoolId: 'missing-school' }; + mocks.lookup.mockReturnValue({ userPref }); + + const { result } = renderHook(() => useUserSchools()); + + await waitFor(() => { + expect(result.current.selectedSchool?.id).toBe('s1'); + }); + }); + + it('keeps selectedSchool undefined when no user preference exists', async () => { + mocks.lookup.mockReturnValue(undefined); + + const { result } = renderHook(() => useUserSchools()); + + await waitFor(() => { + expect(result.current.selectedSchool).toBeUndefined(); + }); + }); + + it('updates selected school and saves preference on change when userPref exists', async () => { + const userPref = { schoolId: 's1' }; + mocks.lookup.mockReturnValue({ userPref }); + + const { result } = renderHook(() => useUserSchools()); + + await waitFor(() => { + expect(result.current.selectedSchool?.id).toBe('s1'); + }); + + act(() => { + result.current.handleSelectedSchoolChange(schools[1] as any); + }); + + expect(result.current.selectedSchool?.id).toBe('s2'); + expect(userPref.schoolId).toBe('s2'); + expect(mocks.saveUserPreferences).toHaveBeenCalledTimes(1); + }); + + it('updates selected school but does not save when no userPref exists', () => { + mocks.lookup.mockReturnValue(undefined); + + const { result } = renderHook(() => useUserSchools()); + + act(() => { + result.current.handleSelectedSchoolChange(schools[0] as any); + }); + + expect(result.current.selectedSchool?.id).toBe('s1'); + expect(mocks.saveUserPreferences).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/react/vite.config.ts b/packages/react/vite.config.ts index 46308fbbb..c249e4681 100644 --- a/packages/react/vite.config.ts +++ b/packages/react/vite.config.ts @@ -91,7 +91,7 @@ export default defineConfig({ watch: false, globals: true, environment: 'jsdom', - include: ['src/**/*.spec.tsx'], + include: ['src/**/*.spec.tsx', 'src/**/*.spec.ts'], setupFiles: ['./vitest.setup.ts'], reporters: ['default'], },