diff --git a/src/apps/dashboard/routes/users/parentalcontrol.tsx b/src/apps/dashboard/routes/users/parentalcontrol.tsx index da7262af1b..70c299bf43 100644 --- a/src/apps/dashboard/routes/users/parentalcontrol.tsx +++ b/src/apps/dashboard/routes/users/parentalcontrol.tsx @@ -70,6 +70,13 @@ const UserParentalControl = () => { const [ blockedTags, setBlockedTags ] = useState([]); const libraryMenu = useMemo(async () => ((await import('../../../../scripts/libraryMenu')).default), []); + // The following are meant to be reset on each render. + // These are to prevent multiple callbacks to be added to a single element in one render as useEffect may be executed multiple times in each render. + let allowedTagsPopupCallback: (() => void) | null = null; + let blockedTagsPopupCallback: (() => void) | null = null; + let accessSchedulesPopupCallback: (() => void) | null = null; + let formSubmissionCallback: ((e: Event) => void) | null = null; + const element = useRef(null); const populateRatings = useCallback((allParentalRatings: ParentalRating[]) => { @@ -146,48 +153,6 @@ const UserParentalControl = () => { blockUnratedItems.dispatchEvent(new CustomEvent('create')); }, []); - const loadAllowedTags = useCallback((tags: string[]) => { - const page = element.current; - - if (!page) { - console.error('[userparentalcontrol] Unexpected null page reference'); - return; - } - - setAllowedTags(tags); - - const allowedTagsElem = page.querySelector('.allowedTags') as HTMLDivElement; - - for (const btnDeleteTag of allowedTagsElem.querySelectorAll('.btnDeleteTag')) { - btnDeleteTag.addEventListener('click', function () { - const tag = btnDeleteTag.getAttribute('data-tag'); - const newTags = tags.filter(t => t !== tag); - loadAllowedTags(newTags); - }); - } - }, []); - - const loadBlockedTags = useCallback((tags: string[]) => { - const page = element.current; - - if (!page) { - console.error('[userparentalcontrol] Unexpected null page reference'); - return; - } - - setBlockedTags(tags); - - const blockedTagsElem = page.querySelector('.blockedTags') as HTMLDivElement; - - for (const btnDeleteTag of blockedTagsElem.querySelectorAll('.btnDeleteTag')) { - btnDeleteTag.addEventListener('click', function () { - const tag = btnDeleteTag.getAttribute('data-tag'); - const newTags = tags.filter(t => t !== tag); - loadBlockedTags(newTags); - }); - } - }, []); - const loadUser = useCallback((user: UserDto, allParentalRatings: ParentalRating[]) => { const page = element.current; @@ -200,8 +165,8 @@ const UserParentalControl = () => { void libraryMenu.then(menu => menu.setTitle(user.Name)); loadUnratedItems(user); - loadAllowedTags(user.Policy?.AllowedTags || []); - loadBlockedTags(user.Policy?.BlockedTags || []); + setAllowedTags(user.Policy?.AllowedTags || []); + setBlockedTags(user.Policy?.BlockedTags || []); populateRatings(allParentalRatings); let ratingValue = ''; @@ -222,7 +187,7 @@ const UserParentalControl = () => { } setAccessSchedules(user.Policy?.AccessSchedules || []); loading.hide(); - }, [loadAllowedTags, loadBlockedTags, loadUnratedItems, populateRatings]); + }, [setAllowedTags, setBlockedTags, loadUnratedItems, populateRatings]); const loadData = useCallback(() => { if (!userId) { @@ -296,7 +261,7 @@ const UserParentalControl = () => { if (tags.indexOf(value) == -1) { tags.push(value); - loadAllowedTags(tags); + setAllowedTags(tags); } }).catch(() => { // prompt closed @@ -317,7 +282,7 @@ const UserParentalControl = () => { if (tags.indexOf(value) == -1) { tags.push(value); - loadBlockedTags(tags); + setBlockedTags(tags); } }).catch(() => { // prompt closed @@ -348,7 +313,11 @@ const UserParentalControl = () => { return false; }; - (page.querySelector('#btnAddSchedule') as HTMLButtonElement).addEventListener('click', function () { + // FIXME: The following is still hacky and should migrate to pure react implementation for callbacks in the future + if (accessSchedulesPopupCallback) { + (page.querySelector('#btnAddSchedule') as HTMLButtonElement).removeEventListener('click', accessSchedulesPopupCallback); + } + accessSchedulesPopupCallback = function () { showSchedulePopup({ Id: 0, UserId: '', @@ -356,37 +325,27 @@ const UserParentalControl = () => { StartHour: 0, EndHour: 0 }, -1); - }); + }; + (page.querySelector('#btnAddSchedule') as HTMLButtonElement).addEventListener('click', accessSchedulesPopupCallback); - (page.querySelector('#btnAddAllowedTag') as HTMLButtonElement).addEventListener('click', function () { - showAllowedTagPopup(); - }); - - (page.querySelector('#btnAddBlockedTag') as HTMLButtonElement).addEventListener('click', function () { - showBlockedTagPopup(); - }); - - (page.querySelector('.userParentalControlForm') as HTMLFormElement).addEventListener('submit', onSubmit); - }, [loadAllowedTags, loadBlockedTags, loadData, userId]); - - useEffect(() => { - const page = element.current; - - if (!page) { - console.error('[userparentalcontrol] Unexpected null page reference'); - return; + if (allowedTagsPopupCallback) { + (page.querySelector('#btnAddAllowedTag') as HTMLButtonElement).removeEventListener('click', allowedTagsPopupCallback); } + allowedTagsPopupCallback = showAllowedTagPopup; + (page.querySelector('#btnAddAllowedTag') as HTMLButtonElement).addEventListener('click', allowedTagsPopupCallback); - const accessScheduleList = page.querySelector('.accessScheduleList') as HTMLDivElement; - - for (const btnDelete of accessScheduleList.querySelectorAll('.btnDelete')) { - btnDelete.addEventListener('click', function () { - const index = parseInt(btnDelete.getAttribute('data-index') ?? '0', 10); - const newindex = accessSchedules.filter((_e, i) => i != index); - setAccessSchedules(newindex); - }); + if (blockedTagsPopupCallback) { + (page.querySelector('#btnAddBlockedTag') as HTMLButtonElement).removeEventListener('click', blockedTagsPopupCallback); } - }, [accessSchedules]); + blockedTagsPopupCallback = showBlockedTagPopup; + (page.querySelector('#btnAddBlockedTag') as HTMLButtonElement).addEventListener('click', blockedTagsPopupCallback); + + if (formSubmissionCallback) { + (page.querySelector('.userParentalControlForm') as HTMLFormElement).removeEventListener('submit', formSubmissionCallback); + } + formSubmissionCallback = onSubmit; + (page.querySelector('.userParentalControlForm') as HTMLFormElement).addEventListener('submit', formSubmissionCallback); + }, [setAllowedTags, setBlockedTags, loadData, userId]); const optionMaxParentalRating = () => { let content = ''; @@ -397,6 +356,21 @@ const UserParentalControl = () => { return content; }; + const removeAllowedTagsCallback = useCallback((tag: string) => { + const newTags = allowedTags.filter(t => t !== tag); + setAllowedTags(newTags); + }, [allowedTags, setAllowedTags]); + + const removeBlockedTagsTagsCallback = useCallback((tag: string) => { + const newTags = blockedTags.filter(t => t !== tag); + setBlockedTags(newTags); + }, [blockedTags, setBlockedTags]); + + const removeScheduleCallback = useCallback((index: number) => { + const newSchedules = accessSchedules.filter((_e, i) => i != index); + setAccessSchedules(newSchedules); + }, [accessSchedules, setAccessSchedules]); + return ( { key={tag} tag={tag} tagType='allowedTag' + removeTagCallback={removeAllowedTagsCallback} />; })} @@ -485,6 +460,7 @@ const UserParentalControl = () => { key={tag} tag={tag} tagType='blockedTag' + removeTagCallback={removeBlockedTagsTagsCallback} />; })} @@ -508,6 +484,7 @@ const UserParentalControl = () => { DayOfWeek={accessSchedule.DayOfWeek} StartHour={accessSchedule.StartHour} EndHour={accessSchedule.EndHour} + removeScheduleCallback={removeScheduleCallback} />; })} diff --git a/src/components/dashboard/users/AccessScheduleList.tsx b/src/components/dashboard/users/AccessScheduleList.tsx index 7303ec7e50..91a789017f 100644 --- a/src/components/dashboard/users/AccessScheduleList.tsx +++ b/src/components/dashboard/users/AccessScheduleList.tsx @@ -1,4 +1,4 @@ -import React, { FunctionComponent } from 'react'; +import React, { FunctionComponent, useCallback } from 'react'; import datetime from '../../../scripts/datetime'; import globalize from '../../../lib/globalize'; import IconButtonElement from '../../../elements/IconButtonElement'; @@ -8,6 +8,7 @@ type AccessScheduleListProps = { DayOfWeek?: string; StartHour?: number ; EndHour?: number; + removeScheduleCallback?: (index: number) => void; }; function getDisplayTime(hours = 0) { @@ -21,7 +22,10 @@ function getDisplayTime(hours = 0) { return datetime.getDisplayTime(new Date(2000, 1, 1, hours, minutes, 0, 0)); } -const AccessScheduleList: FunctionComponent = ({ index, DayOfWeek, StartHour, EndHour }: AccessScheduleListProps) => { +const AccessScheduleList: FunctionComponent = ({ index, DayOfWeek, StartHour, EndHour, removeScheduleCallback }: AccessScheduleListProps) => { + const onClick = useCallback(() => { + index !== undefined && removeScheduleCallback !== undefined && removeScheduleCallback(index); + }, [index, removeScheduleCallback]); return (
= ({ index, title='Delete' icon='delete' dataIndex={index} + onClick={onClick} />
); diff --git a/src/components/dashboard/users/TagList.tsx b/src/components/dashboard/users/TagList.tsx index 531ee2f6e6..172ee0196e 100644 --- a/src/components/dashboard/users/TagList.tsx +++ b/src/components/dashboard/users/TagList.tsx @@ -1,12 +1,16 @@ -import React, { FunctionComponent } from 'react'; +import React, { FunctionComponent, useCallback } from 'react'; import IconButtonElement from '../../../elements/IconButtonElement'; type IProps = { tag?: string, tagType?: string; + removeTagCallback?: (tag: string) => void; }; -const TagList: FunctionComponent = ({ tag, tagType }: IProps) => { +const TagList: FunctionComponent = ({ tag, tagType, removeTagCallback }: IProps) => { + const onClick = useCallback(() => { + tag !== undefined && removeTagCallback !== undefined && removeTagCallback(tag); + }, [tag, removeTagCallback]); return (
@@ -21,6 +25,7 @@ const TagList: FunctionComponent = ({ tag, tagType }: IProps) => { title='Delete' icon='delete' dataTag={tag} + onClick={onClick} />
diff --git a/src/elements/IconButtonElement.tsx b/src/elements/IconButtonElement.tsx index 93e3fd2b87..3970d72392 100644 --- a/src/elements/IconButtonElement.tsx +++ b/src/elements/IconButtonElement.tsx @@ -11,6 +11,7 @@ type IProps = { dataIndex?: string | number; dataTag?: string | number; dataProfileid?: string | number; + onClick?: () => void; }; const createIconButtonElement = ({ is, id, className, title, icon, dataIndex, dataTag, dataProfileid }: IProps) => ({ @@ -28,7 +29,7 @@ const createIconButtonElement = ({ is, id, className, title, icon, dataIndex, da ` }); -const IconButtonElement: FunctionComponent = ({ is, id, className, title, icon, dataIndex, dataTag, dataProfileid }: IProps) => { +const IconButtonElement: FunctionComponent = ({ is, id, className, title, icon, dataIndex, dataTag, dataProfileid, onClick }: IProps) => { return (
= ({ is, id, className, title dataTag: dataTag ? `data-tag="${dataTag}"` : '', dataProfileid: dataProfileid ? `data-profileid="${dataProfileid}"` : '' })} + onClick={onClick} /> ); };