-
Notifications
You must be signed in to change notification settings - Fork 306
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
refactor(react-storage): Primary to composable ActionStart #5935
Changes from all commits
c1d8f59
ce21880
8592936
eea22d2
c812a87
c010bfd
5f8c2cc
dd97eb0
895397b
fd4b4e8
1569c1a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import React from 'react'; | ||
import { ButtonElement } from '../context/elements'; | ||
import { CLASS_BASE } from '../views/constants'; | ||
|
||
export interface ActionStartProps { | ||
onStart?: () => void; | ||
isDisabled?: boolean; | ||
label?: string; | ||
} | ||
|
||
export const ActionStart = ({ | ||
onStart, | ||
isDisabled, | ||
label, | ||
}: ActionStartProps): React.JSX.Element => ( | ||
<ButtonElement | ||
variant="primary" | ||
className={`${CLASS_BASE}__action-start`} | ||
onClick={onStart} | ||
disabled={isDisabled} | ||
> | ||
{label} | ||
</ButtonElement> | ||
); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import React from 'react'; | ||
import { render, screen } from '@testing-library/react'; | ||
import { ActionStart } from '../ActionStart'; | ||
import { CLASS_BASE } from '../../views/constants'; | ||
|
||
describe('ActionStart', () => { | ||
it('renders a button element', () => { | ||
render(<ActionStart />); | ||
const button = screen.getByRole('button'); | ||
expect(button).toBeInTheDocument(); | ||
}); | ||
|
||
it('renders a button with the expected className', () => { | ||
render(<ActionStart />); | ||
const button = screen.getByRole('button'); | ||
expect(button).toHaveClass(`${CLASS_BASE}__action-start`); | ||
}); | ||
|
||
it('renders a button with the expected text', () => { | ||
render(<ActionStart label="Start" />); | ||
const button = screen.getByRole('button'); | ||
expect(button).toHaveTextContent('Start'); | ||
}); | ||
|
||
it('renders a button with the expected disabled state', () => { | ||
render(<ActionStart isDisabled />); | ||
const button = screen.getByRole('button'); | ||
expect(button).toBeDisabled(); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import React from 'react'; | ||
|
||
import { ControlProps } from './types'; | ||
import { useResolvedComposable } from './hooks/useResolvedComposable'; | ||
import { ActionStart } from '../composables/ActionStart'; | ||
import { useActionStart } from './hooks/useActionStart'; | ||
import { ViewElement } from '../context/elements'; | ||
|
||
export const ActionStartControl = ({ | ||
className, | ||
}: ControlProps): React.JSX.Element | null => { | ||
const props = useActionStart(); | ||
const ResolvedActionStart = useResolvedComposable(ActionStart, 'ActionStart'); | ||
|
||
return ( | ||
<ViewElement className={className}> | ||
<ResolvedActionStart {...props} /> | ||
</ViewElement> | ||
); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
import React from 'react'; | ||
import { render, screen } from '@testing-library/react'; | ||
import { ActionStartControl } from '../ActionStartControl'; | ||
import * as useActionStartModule from '../hooks/useActionStart'; | ||
|
||
describe('ActionStartControl', () => { | ||
const useActionStartSpy = jest.spyOn(useActionStartModule, 'useActionStart'); | ||
|
||
afterEach(() => { | ||
useActionStartSpy.mockClear(); | ||
}); | ||
|
||
afterAll(() => { | ||
useActionStartSpy.mockRestore(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this necessary, aren't the tests already isolated? |
||
}); | ||
|
||
it('renders', () => { | ||
useActionStartSpy.mockReturnValue({ | ||
isDisabled: false, | ||
onStart: jest.fn(), | ||
label: 'Start', | ||
}); | ||
render(<ActionStartControl />); | ||
|
||
const button = screen.getByRole('button', { | ||
name: 'Start', | ||
}); | ||
|
||
expect(button).toBeInTheDocument(); | ||
}); | ||
|
||
it('renders with custom label', () => { | ||
useActionStartSpy.mockReturnValue({ | ||
isDisabled: false, | ||
onStart: jest.fn(), | ||
label: 'Custom Label', | ||
}); | ||
render(<ActionStartControl />); | ||
|
||
const button = screen.getByRole('button', { | ||
name: 'Custom Label', | ||
}); | ||
|
||
expect(button).toBeInTheDocument(); | ||
}); | ||
|
||
it('calls onStart when button is clicked', () => { | ||
const mockOnStart = jest.fn(); | ||
useActionStartSpy.mockReturnValue({ | ||
isDisabled: false, | ||
onStart: mockOnStart, | ||
label: 'Start', | ||
}); | ||
render(<ActionStartControl />); | ||
|
||
const button = screen.getByRole('button', { | ||
name: 'Start', | ||
}); | ||
|
||
button.click(); | ||
|
||
expect(mockOnStart).toHaveBeenCalled(); | ||
}); | ||
}); | ||
Samaritan1011001 marked this conversation as resolved.
Show resolved
Hide resolved
|
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,48 @@ | ||||||||||
import * as controlsContextModule from '../../../controls/context'; | ||||||||||
import { ControlsContext } from '../../types'; | ||||||||||
import { useActionStart } from '../useActionStart'; | ||||||||||
|
||||||||||
describe('useActionStart', () => { | ||||||||||
const controlsContext: ControlsContext = { | ||||||||||
data: { | ||||||||||
actionStartLabel: 'Start', | ||||||||||
}, | ||||||||||
actionsConfig: { | ||||||||||
isCancelable: true, | ||||||||||
type: 'BATCH_ACTION', | ||||||||||
}, | ||||||||||
Comment on lines
+10
to
+13
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: |
||||||||||
onActionStart: jest.fn(), | ||||||||||
}; | ||||||||||
|
||||||||||
const useControlsContextSpy = jest.spyOn( | ||||||||||
controlsContextModule, | ||||||||||
'useControlsContext' | ||||||||||
); | ||||||||||
|
||||||||||
afterEach(() => { | ||||||||||
useControlsContextSpy.mockClear(); | ||||||||||
}); | ||||||||||
|
||||||||||
afterAll(() => { | ||||||||||
useControlsContextSpy.mockRestore(); | ||||||||||
}); | ||||||||||
|
||||||||||
it('returns object as it is received from ControlsContext', () => { | ||||||||||
useControlsContextSpy.mockReturnValue(controlsContext); | ||||||||||
|
||||||||||
expect(useActionStart()).toStrictEqual({ | ||||||||||
label: controlsContext.data.actionStartLabel, | ||||||||||
onStart: expect.any(Function), | ||||||||||
isDisabled: controlsContext.data.isActionStartDisabled, | ||||||||||
}); | ||||||||||
}); | ||||||||||
|
||||||||||
it('calls onActionStart from ControlsContext when onStart is called', () => { | ||||||||||
useControlsContextSpy.mockReturnValue(controlsContext); | ||||||||||
|
||||||||||
const { onStart } = useActionStart(); | ||||||||||
onStart!(); | ||||||||||
|
||||||||||
expect(controlsContext.onActionStart).toHaveBeenCalledTimes(1); | ||||||||||
}); | ||||||||||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import { ActionStartProps } from '../../composables/ActionStart'; | ||
import { useControlsContext } from '../../controls/context'; | ||
|
||
export const useActionStart = (): ActionStartProps => { | ||
const { | ||
data: { actionStartLabel, isActionStartDisabled }, | ||
onActionStart, | ||
} = useControlsContext(); | ||
return { | ||
label: actionStartLabel, | ||
isDisabled: isActionStartDisabled, | ||
onStart: onActionStart, | ||
}; | ||
}; |
This file was deleted.
This file was deleted.
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
|
@@ -9,8 +9,12 @@ import { useStore } from '../../providers/store'; | |||||||||
import { Controls } from '../Controls'; | ||||||||||
|
||||||||||
import { Title } from './Controls/Title'; | ||||||||||
import { ActionStartControl } from '../../controls/ActionStartControl'; | ||||||||||
import { ControlsContext } from '../../controls/types'; | ||||||||||
import { ControlsContextProvider } from '../../controls/context'; | ||||||||||
import { CLASS_BASE } from '../constants'; | ||||||||||
|
||||||||||
const { Exit, Message, Primary } = Controls; | ||||||||||
const { Exit, Message } = Controls; | ||||||||||
|
||||||||||
export const isValidFolderName = (name: string | undefined): boolean => | ||||||||||
!!name?.length && !name.includes('/'); | ||||||||||
|
@@ -88,31 +92,34 @@ export const CreateFolderControls = ({ | |||||||||
handleCreateAction({ prefix: '', options: { reset: true } }); | ||||||||||
}; | ||||||||||
|
||||||||||
const primaryProps = | ||||||||||
result?.status === 'COMPLETE' | ||||||||||
? { | ||||||||||
onClick: () => { | ||||||||||
handleClose(); | ||||||||||
}, | ||||||||||
children: 'Folder created', | ||||||||||
} | ||||||||||
: { | ||||||||||
onClick: () => { | ||||||||||
handleCreateFolder(); | ||||||||||
}, | ||||||||||
children: 'Create Folder', | ||||||||||
disabled: !folderName || !!fieldValidationError, | ||||||||||
}; | ||||||||||
const hasCompletedStatus = result?.status === 'COMPLETE'; | ||||||||||
|
||||||||||
// FIXME: Eventually comes from useView hook | ||||||||||
const contextValue: ControlsContext = { | ||||||||||
data: { | ||||||||||
actionStartLabel: hasCompletedStatus ? 'Folder created' : 'Create Folder', | ||||||||||
isActionStartDisabled: !hasCompletedStatus | ||||||||||
? !folderName || !!fieldValidationError | ||||||||||
: undefined, | ||||||||||
}, | ||||||||||
actionsConfig: { | ||||||||||
type: 'SINGLE_ACTION', | ||||||||||
isCancelable: true, | ||||||||||
}, | ||||||||||
Comment on lines
+105
to
+108
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit |
||||||||||
onActionStart: hasCompletedStatus ? handleClose : handleCreateFolder, | ||||||||||
}; | ||||||||||
|
||||||||||
return ( | ||||||||||
<> | ||||||||||
<ControlsContextProvider {...contextValue}> | ||||||||||
<Exit | ||||||||||
onClick={() => { | ||||||||||
handleClose(); | ||||||||||
}} | ||||||||||
/> | ||||||||||
<Title /> | ||||||||||
<Primary {...primaryProps} /> | ||||||||||
<ActionStartControl | ||||||||||
className={`${CLASS_BASE}__create-folder-action-start`} | ||||||||||
/> | ||||||||||
<Field | ||||||||||
label="Enter folder name:" | ||||||||||
disabled={isLoading || !!result?.status} | ||||||||||
|
@@ -132,6 +139,6 @@ export const CreateFolderControls = ({ | |||||||||
{result?.status === 'COMPLETE' || result?.status === 'FAILED' ? ( | ||||||||||
<CreateFolderMessage /> | ||||||||||
) : null} | ||||||||||
</> | ||||||||||
</ControlsContextProvider> | ||||||||||
); | ||||||||||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: we typically add an empty line between third party imports and local imports
(really need to just have an eslint rule for this)