Skip to content

Commit

Permalink
feat: use only one save button
Browse files Browse the repository at this point in the history
- The code is not clean yet
- The tests are not updated
  • Loading branch information
ReidyT committed Dec 8, 2023
1 parent d7414e8 commit 86e564b
Show file tree
Hide file tree
Showing 6 changed files with 165 additions and 51 deletions.
7 changes: 6 additions & 1 deletion src/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -26,7 +27,11 @@ const App: FC = () => {
const renderContent = (): ReactElement => {
switch (context.context) {
case Context.Builder:
return <BuilderView />;
return (
<ObserverProvider>
<BuilderView />
</ObserverProvider>
);

// eslint-disable-next-line no-fallthrough
case Context.Analytics:
Expand Down
53 changes: 33 additions & 20 deletions src/components/common/settings/KeyWords.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -62,6 +63,8 @@ const HtmlTooltip = styled(({ className, ...props }: TooltipProps) => (
}));

const KeyWords: FC<Prop> = ({ textStudents, chatbotEnabled }) => {
const { subscribe, unsubscribe } = useObserver();

const defaultKeywordDef = { keyword: '', definition: '' };

const [keywordDef, setKeywordDef] =
Expand Down Expand Up @@ -101,20 +104,6 @@ const KeyWords: FC<Prop> = ({ 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
Expand All @@ -128,20 +117,44 @@ const KeyWords: FC<Prop> = ({ 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) => (
<ListItem
data-cy={KEYWORD_LIST_ITEM_CY}
Expand Down
58 changes: 32 additions & 26 deletions src/components/common/settings/SetText.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,12 @@ import { TextResourceSetting } from '../../../config/appSettingTypes';
import { DEFAULT_TEXT_RESOURCE_SETTING } from '../../../config/appSettings';
import { DEFAULT_MARGIN, FULL_WIDTH } from '../../../config/stylingConstants';
import { useAppSettingContext } from '../../context/AppSettingContext';
import GraaspButton from './GraaspButton';
import { useObserver } from '../../context/ObserverContext';

type Prop = {
resourceKey: string;
textFieldLabel: string;
textDataCy: string;
buttonDataCy: string;

multiline?: boolean;
minRows?: number;
Expand All @@ -23,11 +22,12 @@ const SetText: FC<Prop> = ({
resourceKey,
textFieldLabel,
textDataCy,
buttonDataCy,
multiline = false,
minRows = 1,
onTextChange,
}) => {
const { subscribe, unsubscribe } = useObserver();

const [resourceText, setResourceText] = useState(
DEFAULT_TEXT_RESOURCE_SETTING.text,
);
Expand Down Expand Up @@ -66,19 +66,36 @@ const SetText: FC<Prop> = ({
}
}, [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 (
<Box
Expand All @@ -98,17 +115,6 @@ const SetText: FC<Prop> = ({
value={resourceText}
minRows={minRows}
/>
<GraaspButton
buttonDataCy={buttonDataCy}
handleOnClick={handleClickSaveText}
sx={{ xs: { margin: '0px' }, sm: { margin: DEFAULT_MARGIN } }}
minHeight="55px"
disabled={
(textResourceSetting?.data || DEFAULT_TEXT_RESOURCE_SETTING).text ===
resourceText
}
text="Save"
/>
</Box>
);
};
Expand Down
65 changes: 65 additions & 0 deletions src/components/context/ObserverContext.tsx
Original file line number Diff line number Diff line change
@@ -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<ObserverContextProps | undefined>(
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<ObserverProviderProps> = ({
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 (
<ObserverContext.Provider value={contextValue}>
{children}
</ObserverContext.Provider>
);
};

export const useObserver = (): ObserverContextProps => {
const context = useContext(ObserverContext);

if (!context) {
throw new Error('useObserver must be used within an ObserverProvider');
}

return context;
};
31 changes: 27 additions & 4 deletions src/components/views/admin/BuilderView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
};
Expand All @@ -38,6 +44,8 @@ const BuilderView: FC = () => {
setTextStudents(text.toLowerCase());
};

const handleButtonClicked = (): void => notifySubscribers();

return (
<div data-cy={BUILDER_VIEW_CY}>
<PublicAlert />
Expand All @@ -52,13 +60,11 @@ const BuilderView: FC = () => {
</Typography>
<SetText
textDataCy={TITLE_INPUT_FIELD_CY}
buttonDataCy={SAVE_TITLE_BUTTON_CY}
resourceKey={LESSON_TITLE_SETTING_KEY}
textFieldLabel="Enter the lesson title"
/>
<SetText
textDataCy={TEXT_INPUT_FIELD_CY}
buttonDataCy={SAVE_TEXT_BUTTON_CY}
resourceKey={TEXT_RESOURCE_SETTING_KEY}
multiline
minRows={2}
Expand All @@ -85,14 +91,12 @@ const BuilderView: FC = () => {
<Box data-cy={CHATBOT_CONTAINER_CY}>
<SetText
textDataCy={INITIAL_PROMPT_INPUT_FIELD_CY}
buttonDataCy={INITIAL_PROMPT_BUTTON_CY}
resourceKey={INITIAL_PROMPT_SETTING_KEY}
multiline
textFieldLabel="Enter the intial prompt describing the conversation (as a template for {{keyword}})"
/>
<SetText
textDataCy={INITIAL_CHATBOT_PROMPT_INPUT_FIELD_CY}
buttonDataCy={INITIAL_CHATBOT_PROMPT_BUTTON_CY}
resourceKey={INITIAL_CHATBOT_PROMPT_SETTING_KEY}
multiline
textFieldLabel="Enter the chatbot's first line (as a template for {{keyword}})"
Expand All @@ -109,6 +113,25 @@ const BuilderView: FC = () => {
Keywords settings
</Typography>
<KeyWords textStudents={textStudents} chatbotEnabled={chatbotEnabled} />

<Box
component="span"
justifyContent="flex-end"
display="flex"
sx={{ margin: DEFAULT_MARGIN }}
>
<GraaspButton
buttonDataCy={SETTINGS_SAVE_BUTTON_CY}
handleOnClick={handleButtonClicked}
sx={{
xs: { margin: DEFAULT_MARGIN },
sm: { margin: DEFAULT_MARGIN },
}}
minHeight="55px"
disabled={false}
text="Save"
/>
</Box>
</div>
);
};
Expand Down
2 changes: 2 additions & 0 deletions src/config/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}]`;

Expand Down

0 comments on commit 86e564b

Please sign in to comment.