diff --git a/src/components/App.tsx b/src/components/App.tsx index e88d308e..6813f767 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -8,6 +8,7 @@ import i18n from '../config/i18n'; import { AppDataProvider } from './context/AppDataContext'; import { AppSettingProvider } from './context/AppSettingContext'; import { MembersProvider } from './context/MembersContext'; +import { ObserverProvider } from './context/ObserverContext'; import BuilderView from './views/admin/BuilderView'; import PlayerView from './views/read/PlayerView'; @@ -26,7 +27,11 @@ const App: FC = () => { const renderContent = (): ReactElement => { switch (context.context) { case Context.Builder: - return ; + return ( + + + + ); // eslint-disable-next-line no-fallthrough case Context.Analytics: diff --git a/src/components/common/settings/KeyWords.tsx b/src/components/common/settings/KeyWords.tsx index 421b6082..7fba763a 100644 --- a/src/components/common/settings/KeyWords.tsx +++ b/src/components/common/settings/KeyWords.tsx @@ -37,6 +37,7 @@ import { ICON_MARGIN, } from '../../../config/stylingConstants'; import { useAppSettingContext } from '../../context/AppSettingContext'; +import { useObserver } from '../../context/ObserverContext'; import GraaspButton from './GraaspButton'; type KeywordDefinition = { @@ -62,6 +63,8 @@ const HtmlTooltip = styled(({ className, ...props }: TooltipProps) => ( })); const KeyWords: FC = ({ textStudents, chatbotEnabled }) => { + const { subscribe, unsubscribe } = useObserver(); + const defaultKeywordDef = { keyword: '', definition: '' }; const [keywordDef, setKeywordDef] = @@ -101,20 +104,6 @@ const KeyWords: FC = ({ textStudents, chatbotEnabled }) => { setDictionary(keywords); }, [keywords]); - const saveKeywords = (newDictionary: keyword[]): void => { - if (keywordsResourceSetting) { - patchAppSetting({ - data: { keywords: newDictionary }, - id: keywordsResourceSetting.id, - }); - } else { - postAppSetting({ - data: { keywords: newDictionary }, - name: KEYWORDS_SETTING_KEY, - }); - } - }; - const handleClickAdd = (): void => { const wordToLowerCase = keywordDef.keyword.toLocaleLowerCase(); const definition = isDefinitionSet @@ -128,20 +117,44 @@ const KeyWords: FC = ({ textStudents, chatbotEnabled }) => { } if (wordToLowerCase !== '') { - // use new array to apply modifications because setState is not immediate - const newDictionary = [...dictionary, newKeyword]; - saveKeywords(newDictionary); - setDictionary(newDictionary); + setDictionary([...dictionary, newKeyword]); } setKeywordDef(defaultKeywordDef); }; const handleDelete = (id: string): void => { - const newDictionary = dictionary.filter((k) => k.word !== id); - saveKeywords(newDictionary); setDictionary(dictionary.filter((k) => k.word !== id)); }; + useEffect(() => { + const handleParentButtonClick = (): void => { + if (keywordsResourceSetting) { + patchAppSetting({ + data: { keywords: dictionary }, + id: keywordsResourceSetting.id, + }); + } else { + postAppSetting({ + data: { keywords: dictionary }, + name: KEYWORDS_SETTING_KEY, + }); + } + }; + + subscribe(handleParentButtonClick); + + return () => { + unsubscribe(handleParentButtonClick); + }; + }, [ + dictionary, + keywordsResourceSetting, + patchAppSetting, + postAppSetting, + subscribe, + unsubscribe, + ]); + const keyWordsItems = dictionary.map((k) => ( = ({ resourceKey, textFieldLabel, textDataCy, - buttonDataCy, multiline = false, minRows = 1, onTextChange, }) => { + const { subscribe, unsubscribe } = useObserver(); + const [resourceText, setResourceText] = useState( DEFAULT_TEXT_RESOURCE_SETTING.text, ); @@ -66,19 +66,36 @@ const SetText: FC = ({ } }, [isClean, notifyTextChanges, textResourceSetting]); - const handleClickSaveText = (): void => { - if (textResourceSetting) { - patchAppSetting({ - data: { text: resourceText }, - id: textResourceSetting.id, - }); - } else { - postAppSetting({ data: { text: resourceText }, name: resourceKey }); - } + useEffect(() => { + const handleParentButtonClick = (): void => { + if (textResourceSetting) { + patchAppSetting({ + data: { text: resourceText }, + id: textResourceSetting.id, + }); + } else { + postAppSetting({ data: { text: resourceText }, name: resourceKey }); + } - setIsClean(true); - notifyTextChanges(resourceText); - }; + setIsClean(true); + notifyTextChanges(resourceText); + }; + + subscribe(handleParentButtonClick); + + return () => { + unsubscribe(handleParentButtonClick); + }; + }, [ + notifyTextChanges, + patchAppSetting, + postAppSetting, + resourceKey, + resourceText, + subscribe, + textResourceSetting, + unsubscribe, + ]); return ( = ({ value={resourceText} minRows={minRows} /> - ); }; diff --git a/src/components/context/ObserverContext.tsx b/src/components/context/ObserverContext.tsx new file mode 100644 index 00000000..3f478239 --- /dev/null +++ b/src/components/context/ObserverContext.tsx @@ -0,0 +1,65 @@ +import React, { + ReactNode, + createContext, + useContext, + useMemo, + useState, +} from 'react'; + +interface ObserverContextProps { + subscribe: (callback: () => void) => void; + unsubscribe: (callback: () => void) => void; + notifySubscribers: () => void; +} + +const ObserverContext = createContext( + undefined, +); + +interface ObserverProviderProps { + children: ReactNode; +} + +/** + * This provider allow to a parent component to notify its children components. + * It is useful when a button is in the parent, but the children have to do something. + */ +export const ObserverProvider: React.FC = ({ + children, +}) => { + const [subscribers, setSubscribers] = useState<(() => void)[]>([]); + + const contextValue = useMemo(() => { + const subscribe: ObserverContextProps['subscribe'] = (callback) => { + setSubscribers((prevSubscribers) => [...prevSubscribers, callback]); + }; + + const unsubscribe: ObserverContextProps['unsubscribe'] = (callback) => { + setSubscribers((prevSubscribers) => + prevSubscribers.filter((subscriber) => subscriber !== callback), + ); + }; + + const notifySubscribers: ObserverContextProps['notifySubscribers'] = () => { + subscribers.forEach((subscriber) => subscriber()); + }; + + return { subscribe, unsubscribe, notifySubscribers }; + }, [subscribers]); + + return ( + + {children} + + ); +}; + +export const useObserver = (): ObserverContextProps => { + const context = useContext(ObserverContext); + + if (!context) { + throw new Error('useObserver must be used within an ObserverProvider'); + } + + return context; +}; diff --git a/src/components/views/admin/BuilderView.tsx b/src/components/views/admin/BuilderView.tsx index 5f2206dc..3f9bd512 100644 --- a/src/components/views/admin/BuilderView.tsx +++ b/src/components/views/admin/BuilderView.tsx @@ -17,19 +17,25 @@ import { INITIAL_PROMPT_INPUT_FIELD_CY, SAVE_TEXT_BUTTON_CY, SAVE_TITLE_BUTTON_CY, + SETTINGS_SAVE_BUTTON_CY, TEXT_INPUT_FIELD_CY, TITLE_INPUT_FIELD_CY, } from '../../../config/selectors'; +import { DEFAULT_MARGIN } from '../../../config/stylingConstants'; import PublicAlert from '../../common/PublicAlert'; +import GraaspButton from '../../common/settings/GraaspButton'; import KeyWords from '../../common/settings/KeyWords'; import SetText from '../../common/settings/SetText'; import SwitchModes from '../../common/settings/SwitchModes'; +import { useObserver } from '../../context/ObserverContext'; // eslint-disable-next-line arrow-body-style const BuilderView: FC = () => { const [chatbotEnabled, setChatbotEnabled] = useState(false); const [textStudents, setTextStudents] = useState(''); + const { notifySubscribers } = useObserver(); + const updateEnableChatbot = (enable: boolean): void => { setChatbotEnabled(enable); }; @@ -38,6 +44,8 @@ const BuilderView: FC = () => { setTextStudents(text.toLowerCase()); }; + const handleButtonClicked = (): void => notifySubscribers(); + return (
@@ -52,13 +60,11 @@ const BuilderView: FC = () => { { { Keywords settings + + + +
); }; diff --git a/src/config/selectors.ts b/src/config/selectors.ts index 0298dc18..878e3d8c 100644 --- a/src/config/selectors.ts +++ b/src/config/selectors.ts @@ -37,6 +37,8 @@ export const CHAT_WINDOW_CY = 'chat_window'; export const DICTIONNARY_MODE_CY = 'dictionnary_mode'; export const CHATBOT_MODE_CY = 'chatbot_mode_cy'; +export const SETTINGS_SAVE_BUTTON_CY = 'settings_save_button'; + export const buildDataCy = (selector: string): string => `[data-cy=${selector}]`;