diff --git a/src/app/types.ts b/src/app/types.ts index fcb259a..197b95b 100644 --- a/src/app/types.ts +++ b/src/app/types.ts @@ -46,6 +46,8 @@ export type CorporateCatalog = { corporatePartner: number; }; +export type CorporateCatalogForm = Pick; + export interface PaginatedResponse { next: string | null; previous: string | null; diff --git a/src/catalogs/api.ts b/src/catalogs/api.ts index 459c0bb..e6ecca4 100644 --- a/src/catalogs/api.ts +++ b/src/catalogs/api.ts @@ -2,7 +2,7 @@ import { getConfig, camelCaseObject } from '@edx/frontend-platform'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { logError } from '@edx/frontend-platform/logging'; -import { CorporateCatalog, PaginatedResponse } from '../app/types'; +import { CorporateCatalog, CorporateCatalogForm, PaginatedResponse } from '../app/types'; export const getPartnerCatalogs = async ( partnerId: string, @@ -43,3 +43,18 @@ export const getCatalogDetails = async ( return null; } }; + +export const modifyCatalog = async ( + partnerId: string, + catalogId: string | number, + data: CorporateCatalogForm, +): Promise => { + try { + const url = `${getConfig().LMS_BASE_URL}/corporate_access/api/v1/partners/${partnerId}/catalogs/${catalogId}/`; + const response = await getAuthenticatedHttpClient().put(url, data); + return camelCaseObject(response.data); + } catch (error) { + logError(error); + return null; + } +}; diff --git a/src/catalogs/components/CatalogEditForm/index.tsx b/src/catalogs/components/CatalogEditForm/index.tsx index 1595dc8..c41f218 100644 --- a/src/catalogs/components/CatalogEditForm/index.tsx +++ b/src/catalogs/components/CatalogEditForm/index.tsx @@ -1,175 +1,236 @@ -import { FC } from 'react'; +import { useImperativeHandle, forwardRef } from 'react'; import { Controller, useForm } from 'react-hook-form'; -import { useParams } from 'wouter'; import { useIntl } from '@edx/frontend-platform/i18n'; import { Col, Form, Stack } from '@openedx/paragon'; -import { useCatalogDetails } from '@src/catalogs/hooks'; +import { useYupValidationResolver } from '@src/catalogs/hooks'; +import { CorporateCatalog, CorporateCatalogForm } from '@src/app/types'; +import { getCatalogSchema } from './validationSchema'; import { EMPTY_FORM_STATE } from './constants'; import messages from './messages'; interface CatalogEditFormProps { - selectedCatalog: string | number; + catalogDetails: CorporateCatalog; + onSubmit: (data: CorporateCatalogForm) => void; } -const CatalogEditForm: FC = ({ selectedCatalog }) => { - const intl = useIntl(); - const { partnerId } = useParams<{ partnerId: string }>(); - const { catalogDetails } = useCatalogDetails({ - partnerId, - selectedCatalog, - }); - - const { register, control } = useForm({ - defaultValues: catalogDetails ?? EMPTY_FORM_STATE, - mode: 'onChange', - }); - - return ( - - {/* General Information Section */} -

{intl.formatMessage(messages.formGeneralInformation)}

- - {/* Public Catalog Switch */} - - - {intl.formatMessage(messages.formIsPublic)} - - e.target.checked, - })} - floatLabelLeft - /> - - - {/* Catalog Name */} - - {intl.formatMessage(messages.formName)} - - - - {/* Alternative Link */} - - {intl.formatMessage(messages.formCatalogAlternativeLink)} - - - - {/* Support Email */} - - {intl.formatMessage(messages.formSupportEmail)} - - - - {/* Date Range Group */} - - ( - - {intl.formatMessage(messages.formAvailableStartDate)} - ) => onChange(new Date(e.target.value).toISOString())} - type="date" - /> - +export interface CatalogEditFormRef { + submitForm: () => void; +} + +const CatalogEditForm = forwardRef( + ({ catalogDetails, onSubmit }, ref) => { + const intl = useIntl(); + + const resolver = useYupValidationResolver(getCatalogSchema(intl)); + const { + register, control, handleSubmit, formState: { errors }, + } = useForm({ + defaultValues: catalogDetails ?? EMPTY_FORM_STATE, + mode: 'onChange', + resolver, + }); + + // Expose a form submit function to parent component + useImperativeHandle(ref, () => ({ + submitForm: handleSubmit(onSubmit), + })); + + return ( + + {/* General Information Section */} +

{intl.formatMessage(messages.formGeneralInformation)}

+ + {/* Public Catalog Switch */} + + + {intl.formatMessage(messages.formIsPublic)} + + e.target.checked, + })} + floatLabelLeft + /> + + + {/* Catalog Name */} + + {intl.formatMessage(messages.formName)} + + {errors.name && ( + + {errors.name?.message} + )} - /> + + + {/* Alternative Link */} + + {intl.formatMessage(messages.formCatalogAlternativeLink)} + + {errors.catalogAlternativeLink && ( + + {errors.catalogAlternativeLink?.message} + + )} + + + {/* Support Email */} + + {intl.formatMessage(messages.formSupportEmail)} + + {errors.supportEmail && ( + + {errors.supportEmail?.message} + + )} + + + {/* Date Range Group */} + + ( + + {intl.formatMessage(messages.formAvailableStartDate)} + ) => { + const dateValue = e.target.value; + if (dateValue) { + const date = new Date(dateValue); + // Check if the date is valid + if (!Number.isNaN(date.getTime())) { + onChange(date.toISOString()); + } + } else { + // Handle empty date input + onChange(null); + } + }} + type="date" + /> + {error && {error.message}} + + )} + /> + ( + + {intl.formatMessage(messages.formAvailableEndDate)} + ) => { + const dateValue = e.target.value; + if (dateValue) { + const date = new Date(dateValue); + // Check if the date is valid + if (!Number.isNaN(date.getTime())) { + // Set time to 23:59:59.999 for the selected day + date.setHours(23, 59, 59, 999); + onChange(date.toISOString()); + } + } else { + // Handle empty date input + onChange(null); + } + }} + type="date" + /> + {error && {error.message}} + + )} + /> + + + {/* Divider */} +
+ + {/* Enrollment Settings Section */} +

{intl.formatMessage(messages.formEnrollmentSettings)}

+ + {/* Enrollment Limits Group */} + + + {intl.formatMessage(messages.formCourseEnrollmentLimit)} + + {errors.courseEnrollmentLimit && ( + + {errors.courseEnrollmentLimit?.message} + + )} + + + {intl.formatMessage(messages.formUserLimit)} + + {errors.userLimit && ( + + {errors.userLimit?.message} + + )} + + + + {/* Divider */} +
+ + {/* Advanced Settings Section */} +

{intl.formatMessage(messages.formAdvancedSettings)}

+ + {/* Email Regexes */} ( - - {intl.formatMessage(messages.formAvailableEndDate)} + + {intl.formatMessage(messages.formEmailRegexes)} ) => { - const date = e.target.value; - // Set time to 23:59:59.999 for the selected day - const endDate = new Date(date); - endDate.setHours(23, 59, 59, 999); - onChange(endDate.toISOString()); - }} - type="date" + id="emailRegexes" + type="text" + value={Array.isArray(value) ? value.join(', ') : value || ''} + onChange={(e) => onChange(e.target.value.split(',').map((v) => v.trim()))} /> )} /> -
- - {/* Divider */} -
- {/* Enrollment Settings Section */} -

{intl.formatMessage(messages.formEnrollmentSettings)}

- - {/* Enrollment Limits Group */} - - - {intl.formatMessage(messages.formCourseEnrollmentLimit)} - + {/* Custom Courses Switch */} + + + {intl.formatMessage(messages.formCustomCourses)} + + e.target.checked, + })} + floatLabelLeft + /> - - {intl.formatMessage(messages.formUserLimit)} - + + {/* Self Enrollment Switch */} + + + {intl.formatMessage(messages.formIsSelfEnrollment)} + + e.target.checked, + })} + floatLabelLeft + /> - - - {/* Divider */} -
- - {/* Advanced Settings Section */} -

{intl.formatMessage(messages.formAdvancedSettings)}

- - {/* Email Regexes */} - ( - - {intl.formatMessage(messages.formEmailRegexes)} - onChange(e.target.value.split(',').map((v) => v.trim()))} - /> - - )} - /> - - {/* Custom Courses Switch */} - - - {intl.formatMessage(messages.formCustomCourses)} - - e.target.checked, - })} - floatLabelLeft - /> - - - {/* Self Enrollment Switch */} - - - {intl.formatMessage(messages.formIsSelfEnrollment)} - - e.target.checked, - })} - floatLabelLeft - /> - -
- ); -}; + + ); + }, +); + +CatalogEditForm.displayName = 'CatalogEditForm'; export default CatalogEditForm; diff --git a/src/catalogs/components/CatalogEditForm/messages.js b/src/catalogs/components/CatalogEditForm/messages.js index 24aa332..391d1bb 100644 --- a/src/catalogs/components/CatalogEditForm/messages.js +++ b/src/catalogs/components/CatalogEditForm/messages.js @@ -71,6 +71,81 @@ const messages = defineMessages({ defaultMessage: 'Self Enrollment', description: 'Label for the self enrollment switch', }, + formNameRequired: { + id: 'form.name.required', + defaultMessage: 'Catalog name is required', + description: 'Error message for required catalog name', + }, + formNameMin: { + id: 'form.name.min', + defaultMessage: 'Catalog name must be at least {min} characters', + description: 'Error message for minimum length of catalog name', + }, + formNameMax: { + id: 'form.name.max', + defaultMessage: 'Catalog name cannot exceed {max} characters', + description: 'Error message for maximum length of catalog name', + }, + formCatalogAlternativeLinkInvalid: { + id: 'form.catalog.alternative.link.invalid', + defaultMessage: 'Please enter a valid URL', + description: 'Error message for invalid alternative link URL', + }, + formSupportEmailRequired: { + id: 'form.support.email.required', + defaultMessage: 'Support email is required', + description: 'Error message for required support email', + }, + formSupportEmailInvalid: { + id: 'form.support.email.invalid', + defaultMessage: 'Please enter a valid email address', + description: 'Error message for invalid support email format', + }, + formAvailableStartDateRequired: { + id: 'form.available.start.date.required', + defaultMessage: 'Start date is required', + description: 'Error message for required start date', + }, + formAvailableStartDateInvalid: { + id: 'form.available.start.date.invalid', + defaultMessage: 'Please enter a valid start date', + description: 'Error message for invalid start date format', + }, + formAvailableEndDateRequired: { + id: 'form.available.end.date.required', + defaultMessage: 'End date is required', + description: 'Error message for required end date', + }, + formAvailableEndDateInvalid: { + id: 'form.available.end.date.invalid', + defaultMessage: 'Please enter a valid end date', + description: 'Error message for invalid end date format', + }, + formAvailableEndDateMin: { + id: 'form.available.end.date.min', + defaultMessage: 'End date must be after start date', + description: 'Error message when end date is before start date', + }, + formCourseEnrollmentLimitMin: { + id: 'form.course.enrollment.limit.min', + defaultMessage: 'Course enrollment must be at least {min} participants', + description: 'Error message for negative course enrollment limit', + }, + formCourseEnrollmentLimitInteger: { + id: 'form.course.enrollment.limit.integer', + defaultMessage: 'Course enrollment limit must be a whole number', + description: 'Error message for non-integer course enrollment limit', + }, + formUserLimitMin: { + id: 'form.user.limit.min', + defaultMessage: 'User limit cannot be negative', + description: 'Error message for negative user limit', + }, + formUserLimitInteger: { + id: 'form.user.limit.integer', + defaultMessage: 'User limit must be a whole number', + description: 'Error message for non-integer user limit', + }, }); export default messages; diff --git a/src/catalogs/components/CatalogEditForm/validationSchema.ts b/src/catalogs/components/CatalogEditForm/validationSchema.ts new file mode 100644 index 0000000..c07db9e --- /dev/null +++ b/src/catalogs/components/CatalogEditForm/validationSchema.ts @@ -0,0 +1,63 @@ +import * as yup from 'yup'; +import messages from './messages'; + +export const getCatalogSchema = (intl) => { + const CATALOG_NAME_MIN_LENGTH = 3; + const CATALOG_NAME_MAX_LENGTH = 100; + const COURSE_ENROLLMENT_LIMIT_MIN = 0; + const USER_LIMIT_MIN = 0; + + return yup.object({ + isPublic: yup.boolean(), + name: yup + .string() + .required(intl.formatMessage(messages.formNameRequired)) + .min(CATALOG_NAME_MIN_LENGTH, intl.formatMessage(messages.formNameMin, { min: CATALOG_NAME_MIN_LENGTH })) + .max(CATALOG_NAME_MAX_LENGTH, intl.formatMessage(messages.formNameMax, { max: CATALOG_NAME_MAX_LENGTH })), + catalogAlternativeLink: yup + .string() + .url(intl.formatMessage(messages.formCatalogAlternativeLinkInvalid)), + supportEmail: yup + .string() + .email(intl.formatMessage(messages.formSupportEmailInvalid)) + .required(intl.formatMessage(messages.formSupportEmailRequired)), + availableStartDate: yup + .date() + .required(intl.formatMessage(messages.formAvailableStartDateRequired)) + .typeError(intl.formatMessage(messages.formAvailableStartDateInvalid)), + availableEndDate: yup + .date() + .required(intl.formatMessage(messages.formAvailableEndDateRequired)) + .typeError(intl.formatMessage(messages.formAvailableEndDateInvalid)) + .test( + 'is-after-start-date', + intl.formatMessage(messages.formAvailableEndDateMin), + function validateEndDate(availableEndDate) { + const { availableStartDate } = this.parent; + if (!availableEndDate || !availableStartDate) { return true; } + return new Date(availableEndDate) > new Date(availableStartDate); + }, + ), + courseEnrollmentLimit: yup + .number() + .typeError(intl.formatMessage(messages.formCourseEnrollmentLimitInteger)) + .integer(intl.formatMessage(messages.formCourseEnrollmentLimitInteger)) + .min( + COURSE_ENROLLMENT_LIMIT_MIN, + intl.formatMessage(messages.formCourseEnrollmentLimitMin, { min: COURSE_ENROLLMENT_LIMIT_MIN }), + ), + userLimit: yup + .number() + .typeError(intl.formatMessage(messages.formUserLimitInteger)) + .integer(intl.formatMessage(messages.formUserLimitInteger)) + .min( + USER_LIMIT_MIN, + intl.formatMessage(messages.formUserLimitMin, { min: USER_LIMIT_MIN }), + ), + emailRegexes: yup + .array() + .of(yup.string().required()), + customCourses: yup.boolean(), + isSelfEnrollment: yup.boolean(), + }); +}; diff --git a/src/catalogs/components/CatalogsList.test.tsx b/src/catalogs/components/CatalogsList.test.tsx index 4aeade4..1362e6a 100644 --- a/src/catalogs/components/CatalogsList.test.tsx +++ b/src/catalogs/components/CatalogsList.test.tsx @@ -19,6 +19,7 @@ jest.mock('@src/hooks', () => ({ jest.mock('@src/catalogs/useCatalogEditionModal', () => ({ useCatalogEditionModal: () => ({ handleChangeSelectedCatalog: jest.fn(), + registerRefetchCallback: jest.fn(), }), })); diff --git a/src/catalogs/components/CatalogsList.tsx b/src/catalogs/components/CatalogsList.tsx index e95aa29..0b9f900 100644 --- a/src/catalogs/components/CatalogsList.tsx +++ b/src/catalogs/components/CatalogsList.tsx @@ -1,4 +1,4 @@ -import { FC } from 'react'; +import { FC, useEffect } from 'react'; import { useIntl } from '@edx/frontend-platform/i18n'; import { DataTable, TextFilter } from '@openedx/paragon'; @@ -26,16 +26,20 @@ const CatalogsList: FC = ({ partnerId }) => { const navigate = useNavigate(); const intl = useIntl(); - const { handleChangeSelectedCatalog } = useCatalogEditionModal(); - const { pageIndex, pageSize, onPaginationChange } = usePagination(); - const { partnerCatalogs, isLoadingCatalogs } = usePartnerCatalogs({ + const { handleChangeSelectedCatalog, registerRefetchCallback } = useCatalogEditionModal(); + const { partnerCatalogs, isLoadingCatalogs, refetchCatalogs } = usePartnerCatalogs({ partnerId, pageIndex: pageIndex + 1, pageSize, }); + useEffect(() => { + registerRefetchCallback(refetchCatalogs); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [refetchCatalogs]); + const tableActions = [{ type: 'view', onClick: (catalog: CorporateCatalog) => navigate(paths.courses.buildPath(partnerId, catalog.id)), diff --git a/src/catalogs/context/CatalogEditionModalContext.ts b/src/catalogs/context/CatalogEditionModalContext.ts index 2f097b3..94bca8e 100644 --- a/src/catalogs/context/CatalogEditionModalContext.ts +++ b/src/catalogs/context/CatalogEditionModalContext.ts @@ -3,13 +3,15 @@ import { createContext } from 'react'; import { CorporateCatalog } from '@src/app/types'; export interface TCatalogEditionModalContext { - isOpen: boolean; + isModalOpen: boolean; selectedCatalog: CorporateCatalog | null; handleChangeSelectedCatalog: (catalogId: number | string | null) => void; + registerRefetchCallback: (callback: () => void) => void; } export const CatalogEditionModalContext = createContext({ - isOpen: false, + isModalOpen: false, selectedCatalog: null, handleChangeSelectedCatalog: () => {}, + registerRefetchCallback: () => {}, }); diff --git a/src/catalogs/hooks.ts b/src/catalogs/hooks.ts index fac6c90..9af405b 100644 --- a/src/catalogs/hooks.ts +++ b/src/catalogs/hooks.ts @@ -1,22 +1,30 @@ -import { useQuery, useQueryClient, useSuspenseQuery } from '@tanstack/react-query'; -import { CorporateCatalog, PaginatedResponse } from '@src/app/types'; -import { getCatalogDetails, getPartnerCatalogs } from './api'; +import { useCallback } from 'react'; +import { FieldValues, Resolver } from 'react-hook-form'; +import { SchemaOf } from 'yup'; +import { + useMutation, useQuery, useQueryClient, useSuspenseQuery, +} from '@tanstack/react-query'; + +import { CorporateCatalog, CorporateCatalogForm, PaginatedResponse } from '@src/app/types'; +import { + getCatalogDetails, getPartnerCatalogs, modifyCatalog, +} from './api'; export const usePartnerCatalogs = ( { partnerId, pageIndex, pageSize }: { partnerId: string; pageIndex: number; pageSize: number; }, ) => { - const { data: partnerCatalogs, isLoading: isLoadingCatalogs } = useSuspenseQuery({ + const { data: partnerCatalogs, isLoading: isLoadingCatalogs, refetch: refetchCatalogs } = useSuspenseQuery({ queryKey: ['partnerCatalogs', partnerId, pageIndex, pageSize], queryFn: () => getPartnerCatalogs(partnerId, pageIndex, pageSize), }); - return { partnerCatalogs, isLoadingCatalogs }; + return { partnerCatalogs, isLoadingCatalogs, refetchCatalogs }; }; export const useCatalogDetails = ({ partnerId, selectedCatalog, -}: { partnerId: string; selectedCatalog: string | number; }) => { +}: { partnerId: string; selectedCatalog: string | number | null; }) => { const queryClient = useQueryClient(); const allCatalogs = queryClient @@ -25,14 +33,72 @@ export const useCatalogDetails = ({ const catalogFromCache = allCatalogs.find((c) => c.id === selectedCatalog); - const { data: catalogDetails, isLoading } = useQuery({ + const { data: catalogDetails, isLoading, refetch } = useQuery({ queryKey: ['catalogDetails', partnerId, selectedCatalog], - queryFn: () => getCatalogDetails(partnerId, selectedCatalog), + queryFn: () => getCatalogDetails(partnerId, selectedCatalog || 0), enabled: !catalogFromCache && !!partnerId && !!selectedCatalog, }); return { catalogDetails: catalogFromCache || catalogDetails || null, isLoadingCatalogDetails: isLoading, + refetchCatalogDetails: refetch, }; }; + +export const useModifyCatalog = () => { + const queryClient = useQueryClient(); + const { mutate } = useMutation({ + mutationFn: async ({ + partnerId, catalogId, data, + }: + { partnerId: string; catalogId: string; data: CorporateCatalogForm }) => modifyCatalog(partnerId, catalogId, data), + onSettled: (_data, _error, args) => { + queryClient.invalidateQueries({ + queryKey: ['catalogDetails', args.partnerId, args.catalogId], + exact: false, + }); + }, + }); + return mutate; +}; + +/** + * Creates a resolver function that validates form values using a Yup schema. + * The returned resolver function takes form values, validates them against the schema, + * and returns an object with either validated values or error messages. + * + * https://react-hook-form.com/advanced-usage#CustomHookwithResolver + * + * @param validationSchema - The Yup schema to validate against. + * @returns A resolver function that validates form values using the provided schema. + */ +export const useYupValidationResolver = ( + validationSchema: SchemaOf, +): Resolver => useCallback( + async (values) => { + try { + await validationSchema.validate(values, { abortEarly: false }); + + return { + values, + errors: {}, + }; + } catch (errors: any) { + return { + values: {}, + errors: errors.inner.reduce( + (allErrors: any, currentError: any) => ({ + ...allErrors, + [currentError.path]: { + type: currentError.type ?? 'validation', + message: currentError.message, + }, + }), + {}, + ), + }; + } + }, + [validationSchema], + ); diff --git a/src/catalogs/useCatalogEditionModal.tsx b/src/catalogs/useCatalogEditionModal.tsx index cbe96de..a7790bc 100644 --- a/src/catalogs/useCatalogEditionModal.tsx +++ b/src/catalogs/useCatalogEditionModal.tsx @@ -1,14 +1,17 @@ import { - FC, useContext, useEffect, useMemo, useState, + FC, useContext, useEffect, useMemo, useState, useRef, useCallback, } from 'react'; import { useIntl } from '@edx/frontend-platform/i18n'; import { Button, useToggle } from '@openedx/paragon'; import ModalLayout from '@src/app/ModalLayout'; -import CatalogEditForm from './components/CatalogEditForm'; +import { CorporateCatalogForm } from '@src/app/types'; +import { useParams } from 'wouter'; +import CatalogEditForm, { CatalogEditFormRef } from './components/CatalogEditForm'; import messages from './messages'; import { CatalogEditionModalContext } from './context/CatalogEditionModalContext'; +import { useCatalogDetails, useModifyCatalog } from './hooks'; interface CatalogEditionModalProviderProps { children: React.ReactNode | React.ReactNode[]; @@ -17,45 +20,78 @@ interface CatalogEditionModalProviderProps { export const CatalogEditionModalProvider: FC = ({ children }) => { const intl = useIntl(); - const [isOpen, open, close] = useToggle(false); - const [selectedCatalogId, setSelectedCatalogId] = useState(null); + const { partnerId } = useParams<{ partnerId: string }>(); + const formRef = useRef(null); - const handleChangeSelectedCatalog = (catalogId: number | string | null) => { + const [isModalOpen, openModal, closeModal] = useToggle(false); + const [selectedCatalogId, setSelectedCatalogId] = useState(null); + const [refetchCallback, setRefetchCallback] = useState<(() => void) | null>(null); + + const modifyCatalog = useModifyCatalog(); + const { catalogDetails, refetchCatalogDetails } = useCatalogDetails({ + partnerId, + selectedCatalog: selectedCatalogId, + }); + + const handleChangeSelectedCatalog = (catalogId: string | null) => { setSelectedCatalogId(catalogId); }; - const handleCloseModal = () => { - setSelectedCatalogId(null); - close(); + const registerRefetchCallback = useCallback((callback: () => void) => { + setRefetchCallback(() => callback); + }, []); + + const handleSaveData = () => { + if (formRef.current) { formRef.current.submitForm(); } + }; + + const handleFormSubmit = (data: CorporateCatalogForm) => { + if (data && selectedCatalogId) { + modifyCatalog({ partnerId, catalogId: selectedCatalogId, data }, { + onSuccess: () => { + if (refetchCallback) { refetchCallback(); } + refetchCatalogDetails(); + setSelectedCatalogId(null); + }, + }); + } }; useEffect(() => { - if (selectedCatalogId) { open(); } + if (selectedCatalogId) { openModal(); } + if (!selectedCatalogId) { closeModal(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedCatalogId]); const value = useMemo( () => ({ - isOpen, - selectedCatalog: null, + isModalOpen, + selectedCatalog: catalogDetails, handleChangeSelectedCatalog, + registerRefetchCallback, }), // eslint-disable-next-line react-hooks/exhaustive-deps - [isOpen], + [isModalOpen, catalogDetails, registerRefetchCallback], ); return ( setSelectedCatalogId(null)} actions={( - )} > - {selectedCatalogId && } + {catalogDetails && ( + + )} {children}