diff --git a/packages/react-storage/src/components/StorageBrowser/composables/ActionStart.tsx b/packages/react-storage/src/components/StorageBrowser/composables/ActionStart.tsx new file mode 100644 index 00000000000..daa3a8e9f4e --- /dev/null +++ b/packages/react-storage/src/components/StorageBrowser/composables/ActionStart.tsx @@ -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 => ( + + {label} + +); diff --git a/packages/react-storage/src/components/StorageBrowser/composables/__tests__/ActionStart.spec.tsx b/packages/react-storage/src/components/StorageBrowser/composables/__tests__/ActionStart.spec.tsx new file mode 100644 index 00000000000..7cf3e408565 --- /dev/null +++ b/packages/react-storage/src/components/StorageBrowser/composables/__tests__/ActionStart.spec.tsx @@ -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(); + const button = screen.getByRole('button'); + expect(button).toBeInTheDocument(); + }); + + it('renders a button with the expected className', () => { + render(); + const button = screen.getByRole('button'); + expect(button).toHaveClass(`${CLASS_BASE}__action-start`); + }); + + it('renders a button with the expected text', () => { + render(); + const button = screen.getByRole('button'); + expect(button).toHaveTextContent('Start'); + }); + + it('renders a button with the expected disabled state', () => { + render(); + const button = screen.getByRole('button'); + expect(button).toBeDisabled(); + }); +}); diff --git a/packages/react-storage/src/components/StorageBrowser/composables/types.ts b/packages/react-storage/src/components/StorageBrowser/composables/types.ts index ec6e6c9bc72..e4da6a57559 100644 --- a/packages/react-storage/src/components/StorageBrowser/composables/types.ts +++ b/packages/react-storage/src/components/StorageBrowser/composables/types.ts @@ -1,11 +1,13 @@ import { DataTableProps } from './DataTable'; import { StatusDisplayProps } from './StatusDisplay'; import { DataRefreshProps } from './DataRefresh'; +import { ActionStartProps } from './ActionStart'; export interface Composables { DataRefresh: React.ComponentType; DataTable: React.ComponentType; StatusDisplay: React.ComponentType; + ActionStart: React.ComponentType; } export interface ComposablesContext { diff --git a/packages/react-storage/src/components/StorageBrowser/controls/ActionStartControl.tsx b/packages/react-storage/src/components/StorageBrowser/controls/ActionStartControl.tsx new file mode 100644 index 00000000000..57cb5620473 --- /dev/null +++ b/packages/react-storage/src/components/StorageBrowser/controls/ActionStartControl.tsx @@ -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 ( + + + + ); +}; diff --git a/packages/react-storage/src/components/StorageBrowser/controls/__tests__/ActionStartControl.spec.tsx b/packages/react-storage/src/components/StorageBrowser/controls/__tests__/ActionStartControl.spec.tsx new file mode 100644 index 00000000000..c48f5d608dc --- /dev/null +++ b/packages/react-storage/src/components/StorageBrowser/controls/__tests__/ActionStartControl.spec.tsx @@ -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(); + }); + + it('renders', () => { + useActionStartSpy.mockReturnValue({ + isDisabled: false, + onStart: jest.fn(), + label: 'Start', + }); + render(); + + 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(); + + 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(); + + const button = screen.getByRole('button', { + name: 'Start', + }); + + button.click(); + + expect(mockOnStart).toHaveBeenCalled(); + }); +}); diff --git a/packages/react-storage/src/components/StorageBrowser/controls/hooks/__tests__/useActionStart.spec.ts b/packages/react-storage/src/components/StorageBrowser/controls/hooks/__tests__/useActionStart.spec.ts new file mode 100644 index 00000000000..49190cf635c --- /dev/null +++ b/packages/react-storage/src/components/StorageBrowser/controls/hooks/__tests__/useActionStart.spec.ts @@ -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', + }, + 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); + }); +}); diff --git a/packages/react-storage/src/components/StorageBrowser/controls/hooks/useActionStart.tsx b/packages/react-storage/src/components/StorageBrowser/controls/hooks/useActionStart.tsx new file mode 100644 index 00000000000..a461b09ef29 --- /dev/null +++ b/packages/react-storage/src/components/StorageBrowser/controls/hooks/useActionStart.tsx @@ -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, + }; +}; diff --git a/packages/react-storage/src/components/StorageBrowser/controls/types.ts b/packages/react-storage/src/components/StorageBrowser/controls/types.ts index a99cf419de6..2314159b5d9 100644 --- a/packages/react-storage/src/components/StorageBrowser/controls/types.ts +++ b/packages/react-storage/src/components/StorageBrowser/controls/types.ts @@ -35,6 +35,8 @@ export interface ControlsContext { taskCounts?: TaskCounts; tableData?: TableData; isDataRefreshDisabled?: boolean; + actionStartLabel?: string; + isActionStartDisabled?: boolean; }; actionsConfig?: { type: @@ -45,4 +47,5 @@ export interface ControlsContext { isCancelable?: boolean; }; onRefresh?: () => void; + onActionStart?: () => void; } diff --git a/packages/react-storage/src/components/StorageBrowser/views/Controls/Controls.tsx b/packages/react-storage/src/components/StorageBrowser/views/Controls/Controls.tsx index 599d2c32019..35cd4b734f7 100644 --- a/packages/react-storage/src/components/StorageBrowser/views/Controls/Controls.tsx +++ b/packages/react-storage/src/components/StorageBrowser/views/Controls/Controls.tsx @@ -7,7 +7,6 @@ import { MessageControl } from './Message'; import { NavigateControl } from './Navigate'; import { OverwriteControl } from './Overwrite'; import { PaginateControl } from './Paginate'; -import { PrimaryControl } from './Primary'; import { SearchControl } from './Search'; import { TableControl } from './Table'; import { TitleControl } from './Title'; @@ -21,7 +20,6 @@ export interface Controls { Message: typeof MessageControl; Overwrite: typeof OverwriteControl; Paginate: typeof PaginateControl; - Primary: typeof PrimaryControl; Navigate: typeof NavigateControl; Search: typeof SearchControl; Table: typeof TableControl; @@ -37,7 +35,6 @@ export const Controls: Controls = { Message: MessageControl, Overwrite: OverwriteControl, Paginate: PaginateControl, - Primary: PrimaryControl, Navigate: NavigateControl, Search: SearchControl, Table: TableControl, diff --git a/packages/react-storage/src/components/StorageBrowser/views/Controls/Primary.tsx b/packages/react-storage/src/components/StorageBrowser/views/Controls/Primary.tsx deleted file mode 100644 index 41cf65c7649..00000000000 --- a/packages/react-storage/src/components/StorageBrowser/views/Controls/Primary.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import React from 'react'; - -import { CLASS_BASE } from '../constants'; -import { ButtonElement } from '../../context/elements/definitions'; - -const BLOCK_NAME = `${CLASS_BASE}__primary`; -interface PrimaryControlProps { - onClick?: () => void; - disabled?: boolean; - children?: React.ReactNode; -} - -export const PrimaryControl = ({ - onClick, - disabled, - children, -}: PrimaryControlProps): React.JSX.Element => ( - - {children ?? 'Start'} - -); diff --git a/packages/react-storage/src/components/StorageBrowser/views/Controls/__tests__/Primary.spec.tsx b/packages/react-storage/src/components/StorageBrowser/views/Controls/__tests__/Primary.spec.tsx deleted file mode 100644 index 763840fe8ea..00000000000 --- a/packages/react-storage/src/components/StorageBrowser/views/Controls/__tests__/Primary.spec.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import React from 'react'; -import { render, screen } from '@testing-library/react'; -import { PrimaryControl } from '../Primary'; - -describe('PrimaryControl', () => { - it('renders the PrimaryControl', () => { - render(); - - const button = screen.getByRole('button', { - name: 'Start', - }); - - expect(button).toBeInTheDocument(); - }); -}); diff --git a/packages/react-storage/src/components/StorageBrowser/views/Controls/index.ts b/packages/react-storage/src/components/StorageBrowser/views/Controls/index.ts index fcddfad8b60..96dfbd0f956 100644 --- a/packages/react-storage/src/components/StorageBrowser/views/Controls/index.ts +++ b/packages/react-storage/src/components/StorageBrowser/views/Controls/index.ts @@ -7,7 +7,6 @@ export { MessageControl } from './Message'; export { NavigateControl, NavigateItem } from './Navigate'; export { OverwriteControl } from './Overwrite'; export { PaginateControl } from './Paginate'; -export { PrimaryControl } from './Primary'; export { SearchControl } from './Search'; export { TitleControl } from './Title'; export { TableControl, LocationDetailViewTable } from './Table'; diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CreateFolderControls.tsx b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CreateFolderControls.tsx index 2cede4cc6b9..b0d2a5695f6 100644 --- a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CreateFolderControls.tsx +++ b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CreateFolderControls.tsx @@ -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, + }, + onActionStart: hasCompletedStatus ? handleClose : handleCreateFolder, + }; return ( - <> + { handleClose(); }} /> - <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> ); }; diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/DeleteFilesControls.tsx b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/DeleteFilesControls.tsx index a355c2657cf..80368f1a0f5 100644 --- a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/DeleteFilesControls.tsx +++ b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/DeleteFilesControls.tsx @@ -11,8 +11,9 @@ import { StatusDisplayControl } from '../../controls/StatusDisplayControl'; import { ControlsContext } from '../../controls/types'; import { useStore } from '../../providers/store'; import { getDeleteActionViewTableData } from './utils'; +import { ActionStartControl } from '../../controls/ActionStartControl'; -const { Exit, Primary } = Controls; +const { Exit } = Controls; export const DeleteFilesControls = ({ onClose: _onClose, @@ -40,22 +41,21 @@ export const DeleteFilesControls = ({ }); const contextValue: ControlsContext = { - data: { taskCounts, tableData }, + data: { + taskCounts, + tableData, + isActionStartDisabled: disablePrimary, + actionStartLabel: 'Start', + }, actionsConfig: { type: 'BATCH_ACTION', isCancelable: true }, + onActionStart: onStart, }; return ( <ControlsContextProvider {...contextValue}> <Exit onClick={onClose} disabled={disableClose} /> <Title /> - <Primary - disabled={disablePrimary} - onClick={() => { - onStart(); - }} - > - Start - </Primary> + <ActionStartControl /> <ButtonElement variant="cancel" disabled={disableCancel} diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/UploadControls.tsx b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/UploadControls.tsx index c6b92f08cd3..af24150d845 100644 --- a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/UploadControls.tsx +++ b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/UploadControls.tsx @@ -35,10 +35,11 @@ import { STATUS_DISPLAY_VALUES, } from './constants'; import { FileItems } from '../../providers/store/files'; +import { ActionStartControl } from '../../controls/ActionStartControl'; const { Icon } = StorageBrowserElements; -const { Cancel, Exit, Overwrite, Primary, Table } = Controls; +const { Cancel, Exit, Overwrite, Table } = Controls; interface LocationActionViewColumns { cancel: (() => void) | undefined; @@ -329,8 +330,24 @@ export const UploadControls = ({ // FIXME: Eventually comes from useView hook const contextValue: ControlsContext = { - data: { taskCounts }, - actionsConfig: { type: 'BATCH_ACTION', isCancelable: true }, + data: { + taskCounts, + isActionStartDisabled: disablePrimary, + actionStartLabel: 'Start', + }, + actionsConfig: { + type: 'BATCH_ACTION', + isCancelable: true, + }, + onActionStart: () => { + if (hasInvalidPrefix) return; + + handleProcess({ + config: getInput(), + prefix, + options: { preventOverwrite }, + }); + }, }; return ( @@ -347,20 +364,7 @@ export const UploadControls = ({ }} /> <Title /> - <Primary - disabled={disablePrimary} - onClick={() => { - if (hasInvalidPrefix) return; - - handleProcess({ - config: getInput(), - prefix, - options: { preventOverwrite }, - }); - }} - > - Start - </Primary> + <ActionStartControl className={`${CLASS_BASE}__upload-action-start`} /> <ButtonElement variant="cancel" disabled={disableCancel}