From 956f9bf7e47d1156cd2c852ec6aadfc59e799b30 Mon Sep 17 00:00:00 2001 From: gnattu Date: Sun, 20 Oct 2024 21:40:55 +0800 Subject: [PATCH 01/12] Fix tags and schedule edit for parental control --- .../routes/users/parentalcontrol.tsx | 125 +++++++----------- .../dashboard/users/AccessScheduleList.tsx | 9 +- src/components/dashboard/users/TagList.tsx | 9 +- src/elements/IconButtonElement.tsx | 4 +- 4 files changed, 68 insertions(+), 79 deletions(-) diff --git a/src/apps/dashboard/routes/users/parentalcontrol.tsx b/src/apps/dashboard/routes/users/parentalcontrol.tsx index da7262af1b1..70c299bf43f 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('#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; + }; + (page.querySelector('#btnAddSchedule') as HTMLButtonElement).addEventListener('click', accessSchedulesPopupCallback); - 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; + if (blockedTagsPopupCallback) { + (page.querySelector('#btnAddBlockedTag') as HTMLButtonElement).removeEventListener('click', blockedTagsPopupCallback); + } + blockedTagsPopupCallback = showBlockedTagPopup; + (page.querySelector('#btnAddBlockedTag') as HTMLButtonElement).addEventListener('click', blockedTagsPopupCallback); - 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 (formSubmissionCallback) { + (page.querySelector('.userParentalControlForm') as HTMLFormElement).removeEventListener('submit', formSubmissionCallback); } - }, [accessSchedules]); + 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 7303ec7e50a..91a789017f8 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 531ee2f6e66..172ee0196e8 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 93e3fd2b87c..3970d72392e 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} /> ); }; From 88f9c3d31b81f3f88c12ebca1ca358c72adad234 Mon Sep 17 00:00:00 2001 From: gnattu Date: Sun, 20 Oct 2024 21:54:49 +0800 Subject: [PATCH 02/12] =?UTF-8?q?Don=E2=80=99t=20use=20FIXME?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apps/dashboard/routes/users/parentalcontrol.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/apps/dashboard/routes/users/parentalcontrol.tsx b/src/apps/dashboard/routes/users/parentalcontrol.tsx index 70c299bf43f..e54a78eb025 100644 --- a/src/apps/dashboard/routes/users/parentalcontrol.tsx +++ b/src/apps/dashboard/routes/users/parentalcontrol.tsx @@ -313,7 +313,7 @@ const UserParentalControl = () => { return false; }; - // FIXME: The following is still hacky and should migrate to pure react implementation for callbacks in the future + // 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); } From 7eda53f795e72e23f24715072d236002d4effb2b Mon Sep 17 00:00:00 2001 From: gnattu Date: Sun, 20 Oct 2024 22:15:26 +0800 Subject: [PATCH 03/12] Use anchor for onClick --- src/elements/IconButtonElement.tsx | 32 ++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/src/elements/IconButtonElement.tsx b/src/elements/IconButtonElement.tsx index 3970d72392e..552c407feae 100644 --- a/src/elements/IconButtonElement.tsx +++ b/src/elements/IconButtonElement.tsx @@ -30,19 +30,29 @@ const createIconButtonElement = ({ is, id, className, title, icon, dataIndex, da }); const IconButtonElement: FunctionComponent = ({ is, id, className, title, icon, dataIndex, dataTag, dataProfileid, onClick }: IProps) => { + const button = createIconButtonElement({ + is: is, + id: id ? `id="${id}"` : '', + className: className, + title: title ? `title="${globalize.translate(title)}"` : '', + icon: icon, + dataIndex: (dataIndex || dataIndex === 0) ? `data-index="${dataIndex}"` : '', + dataTag: dataTag ? `data-tag="${dataTag}"` : '', + dataProfileid: dataProfileid ? `data-profileid="${dataProfileid}"` : '' + }); + + if (onClick !== undefined) { + return ( + + ) + } + return (
); }; From 84f7cf1997b433abb3911afa26e1c51fbe78e085 Mon Sep 17 00:00:00 2001 From: gnattu Date: Sun, 20 Oct 2024 22:17:50 +0800 Subject: [PATCH 04/12] Use button --- src/elements/IconButtonElement.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/elements/IconButtonElement.tsx b/src/elements/IconButtonElement.tsx index 552c407feae..e72cf9bb51f 100644 --- a/src/elements/IconButtonElement.tsx +++ b/src/elements/IconButtonElement.tsx @@ -43,7 +43,7 @@ const IconButtonElement: FunctionComponent = ({ is, id, className, title if (onClick !== undefined) { return ( - From d9786d4d245431edaeb298c8109c820fadb8deb1 Mon Sep 17 00:00:00 2001 From: gnattu Date: Sun, 20 Oct 2024 22:20:59 +0800 Subject: [PATCH 05/12] Unset button style --- src/elements/IconButtonElement.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/elements/IconButtonElement.tsx b/src/elements/IconButtonElement.tsx index e72cf9bb51f..258a062baf7 100644 --- a/src/elements/IconButtonElement.tsx +++ b/src/elements/IconButtonElement.tsx @@ -44,6 +44,7 @@ const IconButtonElement: FunctionComponent = ({ is, id, className, title if (onClick !== undefined) { return (