From e65612f170ae59225419d5a9951419c4ee35646a Mon Sep 17 00:00:00 2001 From: faisalsiddique4400 Date: Fri, 26 Jun 2026 16:21:26 +0500 Subject: [PATCH] fix(admin-ui): enable SSA Apply button when 'Is Expirable' is enabled (#2909) Signed-off-by: faisalsiddique4400 --- .../__tests__/CustomInput.test.tsx | 6 +- .../GluuDatePicker/GluuDatePicker.tsx | 4 +- .../__tests__/GluuDatePicker.test.tsx | 23 +++++-- .../app/components/GluuDatePicker/types.ts | 1 + .../__tests__/GluuDropdown.test.tsx | 14 ++-- .../__tests__/GluuDynamicList.test.tsx | 4 +- .../__tests__/GluuSearchToolbar.test.tsx | 5 +- .../Sidebar/__tests__/Sidebar.test.tsx | 7 +- .../Gluu/__tests__/GluuInputEditor.test.tsx | 3 +- .../__tests__/GluuThemeFormFooter.test.tsx | 4 +- .../Ssa/__tests__/components/SsaForm.test.tsx | 18 +++-- .../components/Ssa/components/SsaForm.tsx | 65 +++++++++++-------- .../Ssa/components/styles/SsaForm.style.ts | 38 ++++++++++- .../components/Ssa/helper/validations.ts | 2 +- .../Ssa/hooks/useSsaValidationState.ts | 7 ++ 15 files changed, 139 insertions(+), 62 deletions(-) diff --git a/admin-ui/app/components/CustomInput/__tests__/CustomInput.test.tsx b/admin-ui/app/components/CustomInput/__tests__/CustomInput.test.tsx index 437883ebc6..f10fbe1508 100644 --- a/admin-ui/app/components/CustomInput/__tests__/CustomInput.test.tsx +++ b/admin-ui/app/components/CustomInput/__tests__/CustomInput.test.tsx @@ -35,9 +35,9 @@ describe('CustomInput', () => { it('forwards name, placeholder and disabled props to the input', () => { renderInput({ - name: 'email', - type: 'text', - disabled: true, + 'name': 'email', + 'type': 'text', + 'disabled': true, 'data-testid': 'email', }) const input = screen.getByTestId('email') as HTMLInputElement diff --git a/admin-ui/app/components/GluuDatePicker/GluuDatePicker.tsx b/admin-ui/app/components/GluuDatePicker/GluuDatePicker.tsx index 4b61913115..ee09f80a62 100644 --- a/admin-ui/app/components/GluuDatePicker/GluuDatePicker.tsx +++ b/admin-ui/app/components/GluuDatePicker/GluuDatePicker.tsx @@ -95,6 +95,7 @@ const GluuDatePicker = memo( onAccept={props.onAccept} minDate={props.minDate} maxDate={props.maxDate} + disabled={props.disabled ?? false} slotProps={effectiveSlotProps} sx={datePickerSx} {...(props.showTime ? { closeOnSelect: false } : {})} @@ -123,7 +124,8 @@ const GluuDatePicker = memo( prev.textColor === next.textColor && prev.backgroundColor === next.backgroundColor && prev.minDate === next.minDate && - prev.maxDate === next.maxDate + prev.maxDate === next.maxDate && + prev.disabled === next.disabled ) }, ) diff --git a/admin-ui/app/components/GluuDatePicker/__tests__/GluuDatePicker.test.tsx b/admin-ui/app/components/GluuDatePicker/__tests__/GluuDatePicker.test.tsx index 858ec7bf91..086311e32a 100644 --- a/admin-ui/app/components/GluuDatePicker/__tests__/GluuDatePicker.test.tsx +++ b/admin-ui/app/components/GluuDatePicker/__tests__/GluuDatePicker.test.tsx @@ -39,16 +39,24 @@ describe('GluuDatePicker (single mode)', () => { it('displays the passed value formatted with the default US format (MM/DD/YYYY)', () => { const { container } = render( - , + , { wrapper: Wrapper }, ) expect(getInputs(container)[0].value).toBe('01/15/2024') }) it('renders an empty field when no value is provided', () => { - const { container } = render(, { - wrapper: Wrapper, - }) + const { container } = render( + , + { + wrapper: Wrapper, + }, + ) expect(getInputs(container)[0].value).toBe('') }) @@ -75,7 +83,12 @@ describe('GluuDatePicker (single mode)', () => { it('fires onChange when a day is selected from the calendar', () => { const onChange = jest.fn() render( - , + , { wrapper: Wrapper }, ) openCalendarAndPickDay('20') diff --git a/admin-ui/app/components/GluuDatePicker/types.ts b/admin-ui/app/components/GluuDatePicker/types.ts index 02ad98258d..8c7df08d95 100644 --- a/admin-ui/app/components/GluuDatePicker/types.ts +++ b/admin-ui/app/components/GluuDatePicker/types.ts @@ -46,6 +46,7 @@ export type GluuDatePickerSingleProps = GluuDatePickerBase & { minDate?: Dayjs maxDate?: Dayjs labelShrink?: boolean + disabled?: boolean } export type GluuDatePickerRangeProps = GluuDatePickerBase & { diff --git a/admin-ui/app/components/GluuDropdown/__tests__/GluuDropdown.test.tsx b/admin-ui/app/components/GluuDropdown/__tests__/GluuDropdown.test.tsx index 43286be8c2..40fa16861d 100644 --- a/admin-ui/app/components/GluuDropdown/__tests__/GluuDropdown.test.tsx +++ b/admin-ui/app/components/GluuDropdown/__tests__/GluuDropdown.test.tsx @@ -124,10 +124,9 @@ describe('GluuDropdown', () => { it('fires onOpenChange when toggling open and closed', () => { const onOpenChange = jest.fn() - render( - , - { wrapper: Wrapper }, - ) + render(, { + wrapper: Wrapper, + }) fireEvent.click(getTrigger()) expect(onOpenChange).toHaveBeenLastCalledWith(true) @@ -169,10 +168,9 @@ describe('GluuDropdown', () => { }) it('renders the empty message when there are no options', () => { - render( - , - { wrapper: Wrapper }, - ) + render(, { + wrapper: Wrapper, + }) fireEvent.click(getTrigger()) diff --git a/admin-ui/app/components/GluuDynamicList/__tests__/GluuDynamicList.test.tsx b/admin-ui/app/components/GluuDynamicList/__tests__/GluuDynamicList.test.tsx index 1f97caf770..d48dc59366 100644 --- a/admin-ui/app/components/GluuDynamicList/__tests__/GluuDynamicList.test.tsx +++ b/admin-ui/app/components/GluuDynamicList/__tests__/GluuDynamicList.test.tsx @@ -26,9 +26,7 @@ const renderList = (overrides: Partial = {}) => const getRowInputs = (): HTMLInputElement[] => screen .queryAllByRole('textbox') - .filter((el): el is HTMLInputElement => - el.classList.contains('gluu-dynamic-list-input'), - ) + .filter((el): el is HTMLInputElement => el.classList.contains('gluu-dynamic-list-input')) describe('GluuDynamicList', () => { it('renders the title and add button when there are no items', () => { diff --git a/admin-ui/app/components/GluuSearchToolbar/__tests__/GluuSearchToolbar.test.tsx b/admin-ui/app/components/GluuSearchToolbar/__tests__/GluuSearchToolbar.test.tsx index b9b24a6bd3..54dc2b395a 100644 --- a/admin-ui/app/components/GluuSearchToolbar/__tests__/GluuSearchToolbar.test.tsx +++ b/admin-ui/app/components/GluuSearchToolbar/__tests__/GluuSearchToolbar.test.tsx @@ -137,10 +137,7 @@ describe('GluuSearchToolbar', () => { it('renders the primary action and calls its onClick', () => { const onClick = jest.fn() render( - , + , { wrapper: Wrapper }, ) const applyButton = screen.getByRole('button', { name: /Apply/i }) diff --git a/admin-ui/app/components/Sidebar/__tests__/Sidebar.test.tsx b/admin-ui/app/components/Sidebar/__tests__/Sidebar.test.tsx index fd72333073..7235e7fe28 100644 --- a/admin-ui/app/components/Sidebar/__tests__/Sidebar.test.tsx +++ b/admin-ui/app/components/Sidebar/__tests__/Sidebar.test.tsx @@ -28,7 +28,12 @@ const Providers = ({ children, pageConfig }: ProvidersProps) => ( const renderSidebar = ( children: React.ReactNode, pageConfig: PageConfig = makePageConfig({ animationsDisabled: true }), -) => render({children}) +) => + render( + + {children} + , + ) const getSidebar = (): HTMLElement => { const el = document.querySelector('.sidebar.custom-sidebar-container') diff --git a/admin-ui/app/routes/Apps/Gluu/__tests__/GluuInputEditor.test.tsx b/admin-ui/app/routes/Apps/Gluu/__tests__/GluuInputEditor.test.tsx index 52090874b7..17ae721b8f 100644 --- a/admin-ui/app/routes/Apps/Gluu/__tests__/GluuInputEditor.test.tsx +++ b/admin-ui/app/routes/Apps/Gluu/__tests__/GluuInputEditor.test.tsx @@ -103,8 +103,7 @@ const renderEditor = (props: GluuInputEditorProps) => , ) -const getEditor = (): HTMLTextAreaElement => - screen.getByTestId('ace') as HTMLTextAreaElement +const getEditor = (): HTMLTextAreaElement => screen.getByTestId('ace') as HTMLTextAreaElement describe('GluuInputEditor', () => { it('renders the label and the editor', () => { diff --git a/admin-ui/app/routes/Apps/Gluu/__tests__/GluuThemeFormFooter.test.tsx b/admin-ui/app/routes/Apps/Gluu/__tests__/GluuThemeFormFooter.test.tsx index 78f09e0787..4467865bc3 100644 --- a/admin-ui/app/routes/Apps/Gluu/__tests__/GluuThemeFormFooter.test.tsx +++ b/admin-ui/app/routes/Apps/Gluu/__tests__/GluuThemeFormFooter.test.tsx @@ -63,9 +63,7 @@ describe('GluuThemeFormFooter', () => { }) it('disables both Apply and Cancel when isLoading is true', () => { - renderFooter( - baseProps({ showBack: true, showApply: true, showCancel: true, isLoading: true }), - ) + renderFooter(baseProps({ showBack: true, showApply: true, showCancel: true, isLoading: true })) expect(screen.getByRole('button', { name: 'Apply' })).toBeDisabled() expect(screen.getByRole('button', { name: 'Cancel' })).toBeDisabled() }) diff --git a/admin-ui/plugins/auth-server/components/Ssa/__tests__/components/SsaForm.test.tsx b/admin-ui/plugins/auth-server/components/Ssa/__tests__/components/SsaForm.test.tsx index 387be78699..b24211581e 100644 --- a/admin-ui/plugins/auth-server/components/Ssa/__tests__/components/SsaForm.test.tsx +++ b/admin-ui/plugins/auth-server/components/Ssa/__tests__/components/SsaForm.test.tsx @@ -69,22 +69,32 @@ describe('SsaForm', () => { }) describe('expiration date', () => { - it('does not render the date picker until is_expirable is enabled', () => { + it('renders the expiration date field with a title', () => { render(, { wrapper: Wrapper }) + expect(screen.getByText('Expiration Date')).toBeInTheDocument() + }) + + it('disables the date picker until is_expirable is enabled', async () => { + render(, { wrapper: Wrapper }) + expect(screen.getByRole('button', { name: /choose date/i })).toBeDisabled() + const expirableToggle = document.querySelector( 'input#is_expirable[type="checkbox"]', ) as HTMLInputElement - expect(expirableToggle.checked).toBe(false) + fireEvent.click(expirableToggle) + await waitFor(() => { + expect(screen.getByRole('button', { name: /choose date/i })).toBeEnabled() + }) }) - it('shows the date picker after toggling is_expirable on', async () => { + it('surfaces a validation message when is_expirable is on without a date', async () => { render(, { wrapper: Wrapper }) const expirableToggle = document.querySelector( 'input#is_expirable[type="checkbox"]', ) as HTMLInputElement fireEvent.click(expirableToggle) await waitFor(() => { - expect(expirableToggle.checked).toBe(true) + expect(screen.getByText(/Expiration date is required/i)).toBeInTheDocument() }) }) }) diff --git a/admin-ui/plugins/auth-server/components/Ssa/components/SsaForm.tsx b/admin-ui/plugins/auth-server/components/Ssa/components/SsaForm.tsx index aa8c2968e0..466635cce3 100644 --- a/admin-ui/plugins/auth-server/components/Ssa/components/SsaForm.tsx +++ b/admin-ui/plugins/auth-server/components/Ssa/components/SsaForm.tsx @@ -4,16 +4,18 @@ import { useFormik, type FormikProps } from 'formik' import type { JsonValue } from 'Routes/Apps/Gluu/types/common' import { Form, Col, FormGroup } from 'Components' import { GluuDatePicker } from '@/components/GluuDatePicker' -import { createDate, DATE_FORMATS } from '@/utils/dayjsUtils' +import { addDate, createDate, DATE_FORMATS } from '@/utils/dayjsUtils' import type { Dayjs } from '@/utils/dayjsUtils' import GluuInputRow from 'Routes/Apps/Gluu/GluuInputRow' import GluuToggleRow from 'Routes/Apps/Gluu/GluuToggleRow' import GluuRemovableInputRow from 'Routes/Apps/Gluu/GluuRemovableInputRow' +import GluuLabel from 'Routes/Apps/Gluu/GluuLabel' import GluuText from 'Routes/Apps/Gluu/GluuText' import GluuAutocomplete from 'Routes/Apps/Gluu/GluuAutocomplete' import GluuThemeFormFooter from 'Routes/Apps/Gluu/GluuThemeFormFooter' import GluuCommitDialog from 'Routes/Apps/Gluu/GluuCommitDialog' import { SSA } from 'Utils/ApiResources' +import { CEDARLING_CONFIG_SPACING } from '@/constants' import { useTheme } from '@/context/theme/themeContext' import getThemeColor from '@/context/theme/config' import { DEFAULT_THEME, THEME_DARK } from '@/context/theme/constants' @@ -168,6 +170,8 @@ const SsaForm: React.FC = ({ [isSubmitting, formik.dirty, formik.isValid, modifiedFields, selectedAttributes], ) + const minExpirationDate = useMemo(() => addDate(createDate(), 1, 'day'), []) + const enterHerePlaceholder = useMemo(() => t('placeholders.enter_here'), [t]) const softwareRolesPlaceholder = useMemo( @@ -192,6 +196,7 @@ const SsaForm: React.FC = ({ const handleExpirationDateChange = useCallback( (date: Dayjs | null) => { formik.setFieldValue('expirationDate', date) + formik.setFieldTouched('expirationDate', true, false) }, [formik], ) @@ -310,17 +315,30 @@ const SsaForm: React.FC = ({ doc_category={SSA} /> -
- + + + {validationState.expirationDateError && ( + + {validationState.expirationDateErrorMessage} + + )}
@@ -349,19 +367,18 @@ const SsaForm: React.FC = ({ />
- {formik.values.is_expirable && ( -
- -
- )} +
+ +
@@ -374,10 +391,6 @@ const SsaForm: React.FC = ({ value={formik.values[attribute] as string} modifiedFields={modifiedFields} setModifiedFields={setModifiedFields} - // GluuRemovableInputRow constrains FormikProps to Record; - // SsaFormValues includes a Dayjs `expirationDate` (not JsonValue), but this row - // only ever reads the dynamic string-valued custom attribute fields, so the cast - // is safe at runtime. formik={formik as object as FormikProps>} lsize={12} handler={() => handleAttributeRemove(attribute)} diff --git a/admin-ui/plugins/auth-server/components/Ssa/components/styles/SsaForm.style.ts b/admin-ui/plugins/auth-server/components/Ssa/components/styles/SsaForm.style.ts index 74c0d2f5d3..0ea5089ced 100644 --- a/admin-ui/plugins/auth-server/components/Ssa/components/styles/SsaForm.style.ts +++ b/admin-ui/plugins/auth-server/components/Ssa/components/styles/SsaForm.style.ts @@ -165,11 +165,47 @@ export const useStyles = makeStyles()((_, { isDark, themeCo margin: '1px 2px', }, datePickerCell: { + 'position': 'relative' as const, + 'minWidth': 0, + 'boxSizing': 'border-box' as const, 'display': 'flex', - 'alignItems': 'center', + 'flexDirection': 'column', + 'alignItems': 'stretch', '& .MuiInputBase-root': { backgroundColor: `${inputBg} !important`, }, + '& .MuiPickersInputBase-root.Mui-disabled': { + opacity: OPACITY.DISABLED, + cursor: 'not-allowed', + }, + '& .MuiInputLabel-root, & .MuiPickersInputBase-root .MuiInputLabel-root': { + display: 'none', + }, + '& .MuiOutlinedInput-notchedOutline legend, & .MuiPickersOutlinedInput-notchedOutline legend': + { + display: 'none', + width: 0, + maxWidth: 0, + }, + '& label': { + flex: '0 0 auto', + display: 'block', + width: '100%', + maxWidth: '100%', + paddingTop: 0, + paddingBottom: 0, + paddingLeft: 0, + paddingRight: 0, + marginBottom: '4px !important', + }, + }, + datePickerError: { + position: 'absolute' as const, + top: '100%', + left: 0, + marginTop: 2, + color: themeColors.errorColor, + fontSize: fontSizes.sm, }, claimsPanelWrap: { flex: 1, diff --git a/admin-ui/plugins/auth-server/components/Ssa/helper/validations.ts b/admin-ui/plugins/auth-server/components/Ssa/helper/validations.ts index 03f6dfe123..226da3670f 100644 --- a/admin-ui/plugins/auth-server/components/Ssa/helper/validations.ts +++ b/admin-ui/plugins/auth-server/components/Ssa/helper/validations.ts @@ -71,7 +71,7 @@ export const getSsaValidationSchema = () => return false } - return dateValue.isAfter(dayjs()) + return dateValue.isAfter(dayjs(), 'day') }, ), }) as Yup.ObjectSchema diff --git a/admin-ui/plugins/auth-server/components/Ssa/hooks/useSsaValidationState.ts b/admin-ui/plugins/auth-server/components/Ssa/hooks/useSsaValidationState.ts index 2d0d6a89a6..086eb44754 100644 --- a/admin-ui/plugins/auth-server/components/Ssa/hooks/useSsaValidationState.ts +++ b/admin-ui/plugins/auth-server/components/Ssa/hooks/useSsaValidationState.ts @@ -13,6 +13,8 @@ type SsaValidationState = { grantTypesErrorMessage: string descriptionError: boolean descriptionErrorMessage: string + expirationDateError: boolean + expirationDateErrorMessage: string } export const useSsaValidationState = (formik: FormikProps): SsaValidationState => { @@ -33,6 +35,9 @@ export const useSsaValidationState = (formik: FormikProps): SsaVa descriptionError: Boolean(formik.touched.description && formik.errors.description), descriptionErrorMessage: typeof formik.errors.description === 'string' ? formik.errors.description : '', + expirationDateError: Boolean(formik.values.is_expirable && formik.errors.expirationDate), + expirationDateErrorMessage: + typeof formik.errors.expirationDate === 'string' ? formik.errors.expirationDate : '', }), [ formik.touched.software_id, @@ -45,6 +50,8 @@ export const useSsaValidationState = (formik: FormikProps): SsaVa formik.errors.grant_types, formik.touched.description, formik.errors.description, + formik.values.is_expirable, + formik.errors.expirationDate, ], ) }