Skip to content

Commit

Permalink
Fix tags and schedule edit for parental control
Browse files Browse the repository at this point in the history
  • Loading branch information
gnattu committed Oct 20, 2024
1 parent 568b5f6 commit 956f9bf
Show file tree
Hide file tree
Showing 4 changed files with 68 additions and 79 deletions.
125 changes: 51 additions & 74 deletions src/apps/dashboard/routes/users/parentalcontrol.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,13 @@ const UserParentalControl = () => {
const [ blockedTags, setBlockedTags ] = useState<string[]>([]);
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<HTMLDivElement>(null);

const populateRatings = useCallback((allParentalRatings: ParentalRating[]) => {
Expand Down Expand Up @@ -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;

Expand All @@ -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 = '';
Expand All @@ -222,7 +187,7 @@ const UserParentalControl = () => {
}
setAccessSchedules(user.Policy?.AccessSchedules || []);
loading.hide();
}, [loadAllowedTags, loadBlockedTags, loadUnratedItems, populateRatings]);
}, [setAllowedTags, setBlockedTags, loadUnratedItems, populateRatings]);

Check warning on line 190 in src/apps/dashboard/routes/users/parentalcontrol.tsx

View workflow job for this annotation

GitHub Actions / Quality checks 👌🧪 / Run lint 🕵️‍♂️

React Hook useCallback has a missing dependency: 'libraryMenu'. Either include it or remove the dependency array

const loadData = useCallback(() => {
if (!userId) {
Expand Down Expand Up @@ -296,7 +261,7 @@ const UserParentalControl = () => {

if (tags.indexOf(value) == -1) {
tags.push(value);
loadAllowedTags(tags);
setAllowedTags(tags);
}
}).catch(() => {
// prompt closed
Expand All @@ -317,7 +282,7 @@ const UserParentalControl = () => {

if (tags.indexOf(value) == -1) {
tags.push(value);
loadBlockedTags(tags);
setBlockedTags(tags);
}
}).catch(() => {
// prompt closed
Expand Down Expand Up @@ -348,45 +313,39 @@ 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

Check warning on line 316 in src/apps/dashboard/routes/users/parentalcontrol.tsx

View workflow job for this annotation

GitHub Actions / Quality checks 👌🧪 / Run lint 🕵️‍♂️

Unexpected 'fixme' comment: 'FIXME: The following is still hacky and...'
if (accessSchedulesPopupCallback) {
(page.querySelector('#btnAddSchedule') as HTMLButtonElement).removeEventListener('click', accessSchedulesPopupCallback);
}
accessSchedulesPopupCallback = function () {

Check warning on line 320 in src/apps/dashboard/routes/users/parentalcontrol.tsx

View workflow job for this annotation

GitHub Actions / Quality checks 👌🧪 / Run lint 🕵️‍♂️

Assignments to the 'accessSchedulesPopupCallback' variable from inside React Hook useEffect will be lost after each render. To preserve the value over time, store it in a useRef Hook and keep the mutable value in the '.current' property. Otherwise, you can move this variable directly inside useEffect
showSchedulePopup({
Id: 0,
UserId: '',
DayOfWeek: DynamicDayOfWeek.Sunday,
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;

Check warning on line 334 in src/apps/dashboard/routes/users/parentalcontrol.tsx

View workflow job for this annotation

GitHub Actions / Quality checks 👌🧪 / Run lint 🕵️‍♂️

Assignments to the 'allowedTagsPopupCallback' variable from inside React Hook useEffect will be lost after each render. To preserve the value over time, store it in a useRef Hook and keep the mutable value in the '.current' property. Otherwise, you can move this variable directly inside useEffect
(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;

Check warning on line 340 in src/apps/dashboard/routes/users/parentalcontrol.tsx

View workflow job for this annotation

GitHub Actions / Quality checks 👌🧪 / Run lint 🕵️‍♂️

Assignments to the 'blockedTagsPopupCallback' variable from inside React Hook useEffect will be lost after each render. To preserve the value over time, store it in a useRef Hook and keep the mutable value in the '.current' property. Otherwise, you can move this variable directly inside useEffect
(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;

Check warning on line 346 in src/apps/dashboard/routes/users/parentalcontrol.tsx

View workflow job for this annotation

GitHub Actions / Quality checks 👌🧪 / Run lint 🕵️‍♂️

Assignments to the 'formSubmissionCallback' variable from inside React Hook useEffect will be lost after each render. To preserve the value over time, store it in a useRef Hook and keep the mutable value in the '.current' property. Otherwise, you can move this variable directly inside useEffect
(page.querySelector('.userParentalControlForm') as HTMLFormElement).addEventListener('submit', formSubmissionCallback);
}, [setAllowedTags, setBlockedTags, loadData, userId]);

const optionMaxParentalRating = () => {
let content = '';
Expand All @@ -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 (
<Page
id='userParentalControlPage'
Expand Down Expand Up @@ -461,6 +435,7 @@ const UserParentalControl = () => {
key={tag}
tag={tag}
tagType='allowedTag'
removeTagCallback={removeAllowedTagsCallback}
/>;
})}
</div>
Expand All @@ -485,6 +460,7 @@ const UserParentalControl = () => {
key={tag}
tag={tag}
tagType='blockedTag'
removeTagCallback={removeBlockedTagsTagsCallback}
/>;
})}
</div>
Expand All @@ -508,6 +484,7 @@ const UserParentalControl = () => {
DayOfWeek={accessSchedule.DayOfWeek}
StartHour={accessSchedule.StartHour}
EndHour={accessSchedule.EndHour}
removeScheduleCallback={removeScheduleCallback}
/>;
})}
</div>
Expand Down
9 changes: 7 additions & 2 deletions src/components/dashboard/users/AccessScheduleList.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -8,6 +8,7 @@ type AccessScheduleListProps = {
DayOfWeek?: string;
StartHour?: number ;
EndHour?: number;
removeScheduleCallback?: (index: number) => void;
};

function getDisplayTime(hours = 0) {
Expand All @@ -21,7 +22,10 @@ function getDisplayTime(hours = 0) {
return datetime.getDisplayTime(new Date(2000, 1, 1, hours, minutes, 0, 0));
}

const AccessScheduleList: FunctionComponent<AccessScheduleListProps> = ({ index, DayOfWeek, StartHour, EndHour }: AccessScheduleListProps) => {
const AccessScheduleList: FunctionComponent<AccessScheduleListProps> = ({ index, DayOfWeek, StartHour, EndHour, removeScheduleCallback }: AccessScheduleListProps) => {
const onClick = useCallback(() => {
index !== undefined && removeScheduleCallback !== undefined && removeScheduleCallback(index);
}, [index, removeScheduleCallback]);
return (
<div
className='liSchedule listItem'
Expand All @@ -43,6 +47,7 @@ const AccessScheduleList: FunctionComponent<AccessScheduleListProps> = ({ index,
title='Delete'
icon='delete'
dataIndex={index}
onClick={onClick}
/>
</div>
);
Expand Down
9 changes: 7 additions & 2 deletions src/components/dashboard/users/TagList.tsx
Original file line number Diff line number Diff line change
@@ -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<IProps> = ({ tag, tagType }: IProps) => {
const TagList: FunctionComponent<IProps> = ({ tag, tagType, removeTagCallback }: IProps) => {
const onClick = useCallback(() => {
tag !== undefined && removeTagCallback !== undefined && removeTagCallback(tag);
}, [tag, removeTagCallback]);
return (
<div className='paperList'>
<div className='listItem'>
Expand All @@ -21,6 +25,7 @@ const TagList: FunctionComponent<IProps> = ({ tag, tagType }: IProps) => {
title='Delete'
icon='delete'
dataTag={tag}
onClick={onClick}
/>
</div>
</div>
Expand Down
4 changes: 3 additions & 1 deletion src/elements/IconButtonElement.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) => ({
Expand All @@ -28,7 +29,7 @@ const createIconButtonElement = ({ is, id, className, title, icon, dataIndex, da
</button>`
});

const IconButtonElement: FunctionComponent<IProps> = ({ is, id, className, title, icon, dataIndex, dataTag, dataProfileid }: IProps) => {
const IconButtonElement: FunctionComponent<IProps> = ({ is, id, className, title, icon, dataIndex, dataTag, dataProfileid, onClick }: IProps) => {
return (
<div

Check failure on line 34 in src/elements/IconButtonElement.tsx

View workflow job for this annotation

GitHub Actions / Quality checks 👌🧪 / Run lint 🕵️‍♂️

Visible, non-interactive elements with click handlers must have at least one keyboard listener

Check failure on line 34 in src/elements/IconButtonElement.tsx

View workflow job for this annotation

GitHub Actions / Quality checks 👌🧪 / Run lint 🕵️‍♂️

Avoid non-native interactive elements. If using native HTML is not possible, add an appropriate role and support for tabbing, mouse, keyboard, and touch inputs to an interactive content element
dangerouslySetInnerHTML={createIconButtonElement({
Expand All @@ -41,6 +42,7 @@ const IconButtonElement: FunctionComponent<IProps> = ({ is, id, className, title
dataTag: dataTag ? `data-tag="${dataTag}"` : '',
dataProfileid: dataProfileid ? `data-profileid="${dataProfileid}"` : ''
})}
onClick={onClick}
/>
);
};
Expand Down

0 comments on commit 956f9bf

Please sign in to comment.