diff --git a/.gitignore b/.gitignore index e61a201..a628326 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ coverage .cache .tmp .eslintcache +generated # package managers yarn-error.log diff --git a/packages/module/patternfly-docs/content/design-guidelines/design-guidelines.md b/packages/module/patternfly-docs/content/design-guidelines/design-guidelines.md index b1a1956..d8bb2f3 100644 --- a/packages/module/patternfly-docs/content/design-guidelines/design-guidelines.md +++ b/packages/module/patternfly-docs/content/design-guidelines/design-guidelines.md @@ -1,10 +1,10 @@ --- # Sidenav top-level section # should be the same for all markdown files for each extension -section: extensions +section: AI-infra-ui-components # Sidenav secondary level section # should be the same for all markdown files for each extension -id: AI-infra-ui-components +id: DeleteModal # Tab (react | react-demos | html | html-demos | design-guidelines | accessibility) source: design-guidelines --- diff --git a/packages/module/patternfly-docs/content/examples/Basic.tsx b/packages/module/patternfly-docs/content/examples/Basic.tsx deleted file mode 100644 index b532338..0000000 --- a/packages/module/patternfly-docs/content/examples/Basic.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import React from 'react'; -import { ExtendedButton } from '@patternfly/ai-infra-ui-components'; - -export const BasicExample: React.FunctionComponent = () => My custom extension button; diff --git a/packages/module/patternfly-docs/content/examples/DeleteModal.md b/packages/module/patternfly-docs/content/examples/DeleteModal.md new file mode 100644 index 0000000..7c96ae0 --- /dev/null +++ b/packages/module/patternfly-docs/content/examples/DeleteModal.md @@ -0,0 +1,23 @@ +--- +# Sidenav top-level section +# should be the same for all markdown files +section: AI-infra-ui-components +# Sidenav secondary level section +# should be the same for all markdown files +id: DeleteModal +# Tab (react | react-demos | html | html-demos | design-guidelines | accessibility) +source: react +# If you use typescript, the name of the interface to display props for +# These are found through the sourceProps function provided in patternfly-docs.source.js +propComponents: ['DeleteModal'] +--- + +import { DeleteModal } from "@patternfly/ai-infra-ui-components"; + +Note: this component documents the API and enhances the [existing DeleteModal](https://github.com/opendatahub-io/odh-dashboard/blob/main/frontend/src/pages/projects/components/DeleteModal.tsx) component from odh-dashboard. It can be imported from [@patternfly/ai-infra-ui-components](https://www.npmjs.com/package/@patternfly/AI-infra-ui-components). Alternatively, it can be used within the odh-dashboard via the import: `~/pages/projects/components/DeleteModal` + +### Example + +```js file="./DeleteModalBasic.tsx" + +``` diff --git a/packages/module/patternfly-docs/content/examples/DeleteModalBasic.tsx b/packages/module/patternfly-docs/content/examples/DeleteModalBasic.tsx new file mode 100644 index 0000000..b8813dc --- /dev/null +++ b/packages/module/patternfly-docs/content/examples/DeleteModalBasic.tsx @@ -0,0 +1,67 @@ +import React from 'react'; +import { Button, Stack, StackItem } from '@patternfly/react-core'; +import { DeleteModal } from '@patternfly/ai-infra-ui-components'; + +export const DeleteModalBasic: React.FunctionComponent = () => { + const [isModalRecoverableOpen, setIsModalRecoverableOpen] = React.useState(false); + const [isModalDestructiveOpen, setIsModalDestructiveOpen] = React.useState(false); + const [isModalExtraDestructiveOpen, setIsModalExtraDestructiveOpen] = React.useState(false); + + return ( + <> + + + + + + + + + + + + + {isModalRecoverableOpen && ( + {}} + deleteVariant="easily-recoverable" + onClose={() => setIsModalRecoverableOpen(false)} + > + The item-name item will be deleted. + + )} + + {isModalDestructiveOpen && ( + {}} + deleteVariant="destructive" + onClose={() => setIsModalDestructiveOpen(false)} + > + The item-name item will be deleted. [Brief sentence describing consequence of action]. + + )} + + {isModalExtraDestructiveOpen && ( + {}} + deleteVariant="extra-destructive" + onClose={() => setIsModalExtraDestructiveOpen(false)} + > + The item-name item will be deleted. [Brief sentence describing consequence of action]. + + )} + + ); +}; diff --git a/packages/module/patternfly-docs/content/examples/basic.md b/packages/module/patternfly-docs/content/examples/basic.md deleted file mode 100644 index fc7958b..0000000 --- a/packages/module/patternfly-docs/content/examples/basic.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -# Sidenav top-level section -# should be the same for all markdown files -section: extensions -# Sidenav secondary level section -# should be the same for all markdown files -id: AI-infra-ui-components -# Tab (react | react-demos | html | html-demos | design-guidelines | accessibility) -source: react -# If you use typescript, the name of the interface to display props for -# These are found through the sourceProps function provided in patternfly-docs.source.js -propComponents: ['ExtendedButton'] ---- - -import { ExtendedButton } from "@patternfly/ai-infra-ui-components"; - -## Basic usage - -### Example - -```js file="./Basic.tsx" - -``` - -### Fullscreen example - -```js file="./Basic.tsx" isFullscreen - -``` diff --git a/packages/module/patternfly-docs/generated/design-guidelines.js b/packages/module/patternfly-docs/generated/design-guidelines.js deleted file mode 100644 index 318789f..0000000 --- a/packages/module/patternfly-docs/generated/design-guidelines.js +++ /dev/null @@ -1,51 +0,0 @@ -import React from 'react'; -import { AutoLinkHeader, Example, Link as PatternflyThemeLink } from '@patternfly/documentation-framework/components'; - -const pageData = { - "id": "AI-infra-ui-components", - "section": "extensions", - "source": "design-guidelines", - "slug": "/extensions/ai-infra-ui-components/design-guidelines", - "sourceLink": "https://github.com/patternfly/patternfly-org/blob/main/packages/module/patternfly-docs/content/extensions/ai-infra-ui-components/design-guidelines/design-guidelines.md" -}; -pageData.relativeImports = { - -}; -pageData.examples = { - -}; - -const Component = () => ( - -

- {`Design guidelines intro`} -

- - {`Header`} - - - {`Sub-header`} - -

- {`Guidelines:`} -

-
    -
  1. - {`A`} -
  2. -
  3. - {`list`} -
  4. -
  5. - {`using`} -
  6. -
  7. - {`markdown`} -
  8. -
-
-); -Component.displayName = 'ExtensionsPatternflyExtensionSeedDesignGuidelinesDocs'; -Component.pageData = pageData; - -export default Component; diff --git a/packages/module/patternfly-docs/generated/extensions/ai-infra-ui-components/design-guidelines.js b/packages/module/patternfly-docs/generated/extensions/ai-infra-ui-components/design-guidelines.js deleted file mode 100644 index f9946d4..0000000 --- a/packages/module/patternfly-docs/generated/extensions/ai-infra-ui-components/design-guidelines.js +++ /dev/null @@ -1,56 +0,0 @@ -import React from 'react'; -import { AutoLinkHeader, Example, Link as PatternflyThemeLink } from '@patternfly/documentation-framework/components'; - -const pageData = { - "id": "AI-infra-ui-components", - "section": "extensions", - "subsection": "", - "deprecated": false, - "template": false, - "beta": false, - "demo": false, - "newImplementationLink": false, - "source": "design-guidelines", - "tabName": null, - "slug": "/extensions/ai-infra-ui-components/design-guidelines", - "sourceLink": "https://github.com/patternfly/patternfly-org/blob/main/packages/module/patternfly-docs/content/design-guidelines/design-guidelines.md", - "relPath": "packages/module/patternfly-docs/content/design-guidelines/design-guidelines.md" -}; -pageData.examples = { - -}; - -const Component = () => ( - -

- {`Design guidelines intro`} -

- - {`Header`} - - - {`Sub-header`} - -

- {`Guidelines:`} -

-
    -
  1. - {`A`} -
  2. -
  3. - {`list`} -
  4. -
  5. - {`using`} -
  6. -
  7. - {`markdown`} -
  8. -
-
-); -Component.displayName = 'ExtensionsAiInfraUiComponentsDesignGuidelinesDocs'; -Component.pageData = pageData; - -export default Component; diff --git a/packages/module/patternfly-docs/generated/extensions/ai-infra-ui-components/react.js b/packages/module/patternfly-docs/generated/extensions/ai-infra-ui-components/react.js deleted file mode 100644 index f157536..0000000 --- a/packages/module/patternfly-docs/generated/extensions/ai-infra-ui-components/react.js +++ /dev/null @@ -1,64 +0,0 @@ -import React from 'react'; -import { AutoLinkHeader, Example, Link as PatternflyThemeLink } from '@patternfly/documentation-framework/components'; -import { ExtendedButton } from "@patternfly/ai-infra-ui-components"; -const pageData = { - "id": "AI-infra-ui-components", - "section": "extensions", - "subsection": "", - "deprecated": false, - "template": false, - "beta": false, - "demo": false, - "newImplementationLink": false, - "source": "react", - "tabName": null, - "slug": "/extensions/ai-infra-ui-components/react", - "sourceLink": "https://github.com/patternfly/patternfly-react/blob/main/packages/module/patternfly-docs/content/examples/basic.md", - "relPath": "packages/module/patternfly-docs/content/examples/basic.md", - "propComponents": [ - { - "name": "ExtendedButton", - "description": "", - "props": [ - { - "name": "children", - "type": "React.ReactNode", - "description": "Content to render inside the extended button component" - } - ] - } - ], - "examples": [ - "Example" - ], - "fullscreenExamples": [ - "Fullscreen example" - ] -}; -pageData.liveContext = { - ExtendedButton -}; -pageData.examples = { - 'Example': props => - My custom extension button;\n","title":"Example","lang":"js","className":""}}> - - , - 'Fullscreen example': props => - My custom extension button;\n","title":"Fullscreen example","lang":"js","isFullscreen":true,"className":""}}> - - -}; - -const Component = () => ( - - - {`Basic usage`} - - {React.createElement(pageData.examples["Example"])} - {React.createElement(pageData.examples["Fullscreen example"])} - -); -Component.displayName = 'ExtensionsAiInfraUiComponentsReactDocs'; -Component.pageData = pageData; - -export default Component; diff --git a/packages/module/patternfly-docs/generated/index.js b/packages/module/patternfly-docs/generated/index.js deleted file mode 100644 index 61e8536..0000000 --- a/packages/module/patternfly-docs/generated/index.js +++ /dev/null @@ -1,24 +0,0 @@ -module.exports = { - '/extensions/ai-infra-ui-components/react': { - id: "AI-infra-ui-components", - title: "AI-infra-ui-components", - toc: [{"text":"Basic usage"},[{"text":"Example"},{"text":"Fullscreen example"}]], - examples: ["Example"], - fullscreenExamples: ["Fullscreen example"], - section: "extensions", - subsection: "", - source: "react", - tabName: null, - Component: () => import(/* webpackChunkName: "extensions/ai-infra-ui-components/react/index" */ './extensions/ai-infra-ui-components/react') - }, - '/extensions/ai-infra-ui-components/design-guidelines': { - id: "AI-infra-ui-components", - title: "AI-infra-ui-components", - toc: [{"text":"Header"},[{"text":"Sub-header"}]], - section: "extensions", - subsection: "", - source: "design-guidelines", - tabName: null, - Component: () => import(/* webpackChunkName: "extensions/ai-infra-ui-components/design-guidelines/index" */ './extensions/ai-infra-ui-components/design-guidelines') - } -}; \ No newline at end of file diff --git a/packages/module/patternfly-docs/generated/react.js b/packages/module/patternfly-docs/generated/react.js deleted file mode 100644 index 6cbee92..0000000 --- a/packages/module/patternfly-docs/generated/react.js +++ /dev/null @@ -1,59 +0,0 @@ -import React from 'react'; -import { AutoLinkHeader, Example } from '@patternfly/documentation-framework/components'; -import { ExtendedButton } from "@patternfly/ai-infra-ui-components"; -const pageData = { - "id": "AI-infra-ui-components", - "section": "extensions", - "source": "react", - "slug": "/extensions/ai-infra-ui-components/react", - "sourceLink": "https://github.com/patternfly/patternfly-react/blob/main/packages/module/patternfly-docs/content/extensions/ai-infra-ui-components/examples/basic.md", - "propComponents": [ - { - "name": "ExtendedButton", - "description": "", - "props": [ - { - "name": "children", - "type": "React.ReactNode", - "description": "Content to render inside the extended button component" - } - ] - } - ], - "examples": [ - "Example" - ], - "fullscreenExamples": [ - "Fullscreen example" - ] -}; -pageData.liveContext = { - ExtendedButton -}; -pageData.relativeImports = { - -}; -pageData.examples = { - 'Example': props => - My custom extension button;\n","title":"Example","lang":"js"}}> - - , - 'Fullscreen example': props => - My custom extension button;\n","title":"Fullscreen example","lang":"js","isFullscreen":true}}> - - -}; - -const Component = () => ( - - - {`Basic usage`} - - {React.createElement(pageData.examples["Example"])} - {React.createElement(pageData.examples["Fullscreen example"])} - -); -Component.displayName = 'ExtensionsPatternflyExtensionSeedReactDocs'; -Component.pageData = pageData; - -export default Component; diff --git a/packages/module/patternfly-docs/patternfly-docs.config.js b/packages/module/patternfly-docs/patternfly-docs.config.js index 7a4cdb6..4db7142 100644 --- a/packages/module/patternfly-docs/patternfly-docs.config.js +++ b/packages/module/patternfly-docs/patternfly-docs.config.js @@ -1,6 +1,6 @@ // This module is shared between NodeJS and babelled ES5 module.exports = { - sideNavItems: [{ section: 'extensions' }], + sideNavItems: [{ section: 'AI-infra-ui-components' }], topNavItems: [], port: 8006 }; diff --git a/packages/module/patternfly-docs/patternfly-docs.source.js b/packages/module/patternfly-docs/patternfly-docs.source.js index 5ae9c80..e1b1ab0 100644 --- a/packages/module/patternfly-docs/patternfly-docs.source.js +++ b/packages/module/patternfly-docs/patternfly-docs.source.js @@ -8,7 +8,7 @@ module.exports = (sourceMD, sourceProps) => { // Parse md files const contentBase = path.join(__dirname, './content'); - sourceMD(path.join(contentBase, '/**/*.md'), 'extensions'); + sourceMD(path.join(contentBase, '/**/*.md'), 'AI-infra-ui-components'); /** If you want to parse content from node_modules instead of providing a relative/absolute path, diff --git a/packages/module/release.config.js b/packages/module/release.config.js index 99445eb..dd7bc86 100644 --- a/packages/module/release.config.js +++ b/packages/module/release.config.js @@ -10,5 +10,5 @@ module.exports = { '@semantic-release/npm' ], tagFormat: 'v${version}', - dryRun: true -}; \ No newline at end of file + dryRun: false +}; diff --git a/packages/module/src/DeleteModal/DeleteModal.tsx b/packages/module/src/DeleteModal/DeleteModal.tsx new file mode 100644 index 0000000..3a62c5d --- /dev/null +++ b/packages/module/src/DeleteModal/DeleteModal.tsx @@ -0,0 +1,132 @@ +import React from 'react'; +import { + Alert, + AlertProps, + Button, + Flex, + FlexItem, + Modal, + ModalBody, + ModalFooter, + ModalHeader, + ModalProps, + Stack, + StackItem, + TextInput, + TextInputProps +} from '@patternfly/react-core'; + +export type DeleteModalProps = Omit & { + /** Content rendered inside the modal header title. */ + title: React.ReactNode; + /** Delete variant. Destructive and extra-destructive variants will show a warning icon and danger button. For extra-destructive variant, text input confirmation is needed. */ + deleteVariant?: 'extra-destructive' | 'destructive' | 'easily-recoverable'; + /** Text which the user should type in to confirm deletion (only for extra-destructive delete variant) */ + deleteName: string; + /** Message describing what should the user type in to confirm deletion (only for extra-destructive delete variant) */ + confirmationMessage?: (deleteName: string) => React.ReactNode; + /** Text of the delete button */ + submitButtonLabel?: string; + /** Text of the cancel button */ + cancelButtonText?: string; + /** Callback on clicking the delete button */ + onDelete: () => void; + /** Callback on clicking the close button */ + onClose: () => void; + /** Flag indicating that deletion is currently in progress */ + deleting?: boolean; + /** Error indicating deletion has failed */ + error?: Error; + /** Id of the modal for testing purposes */ + testId?: string; + /** Additional props for confirmation text input (only for extra-destructive delete variant) */ + textInputProps?: TextInputProps; + /** Additional props for error alert */ + errorAlertProps?: AlertProps; + /** Modal ref */ + ref?: React.RefObject; +}; + +export const DeleteModal: React.FunctionComponent = ({ + children, + title, + deleteVariant = 'extra-destructive', + deleteName, + confirmationMessage = (deleteName) => ( + <> + Type {deleteName} to confirm deletion: + + ), + submitButtonLabel: deleteButtonText = 'Delete', + cancelButtonText = 'Cancel', + onDelete, + deleting: isDeleting, + error, + testId = 'delete-modal', + textInputProps, + errorAlertProps, + onClose, + ...props +}: DeleteModalProps) => { + const [inputValue, setInputValue] = React.useState(''); + + const deleteNameSanitized = React.useMemo(() => deleteName.trim().replace(/\s+/g, ' '), [deleteName]); + + const confirmed = deleteVariant === 'extra-destructive' ? inputValue.trim() === deleteNameSanitized : true; + + return ( + + + + + {children} + {deleteVariant === 'extra-destructive' && ( + + + {confirmationMessage(deleteNameSanitized)} + setInputValue(value)} + onKeyDown={(event) => { + if (event.key === 'Enter' && confirmed && !isDeleting) { + event.preventDefault(); + onDelete(); + } + }} + /> + + + )} + {error && ( + + + {error.message} + + + )} + + + + + + + + ); +}; diff --git a/packages/module/src/DeleteModal/__tests__/DeleteModal.test.tsx b/packages/module/src/DeleteModal/__tests__/DeleteModal.test.tsx new file mode 100644 index 0000000..7c7546b --- /dev/null +++ b/packages/module/src/DeleteModal/__tests__/DeleteModal.test.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { DeleteModal } from '../DeleteModal'; + +test('Renders with confirmation text input by default', () => { + render( + {}} onClose={() => {}}> + Message + + ); + + expect(screen.getByRole('textbox')).toBeVisible(); +}); diff --git a/packages/module/src/DeleteModal/index.ts b/packages/module/src/DeleteModal/index.ts new file mode 100644 index 0000000..2d52cdf --- /dev/null +++ b/packages/module/src/DeleteModal/index.ts @@ -0,0 +1 @@ +export * from './DeleteModal'; diff --git a/packages/module/src/ExtendedButton/ExtendedButton.tsx b/packages/module/src/ExtendedButton/ExtendedButton.tsx deleted file mode 100644 index b8119e9..0000000 --- a/packages/module/src/ExtendedButton/ExtendedButton.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import React from 'react'; -import { Button, ButtonProps } from '@patternfly/react-core'; - -export interface ExtendedButtonProps extends ButtonProps { - /** Content to render inside the extended button component */ - children?: React.ReactNode; -} - -export const ExtendedButton: React.FunctionComponent = ({ - children, - ...props -}: ExtendedButtonProps) => { - const [currentVariantIndex, setCurrentVariantIndex] = React.useState(0); - - const buttonVariants: ButtonProps['variant'][] = [ - 'primary', - 'secondary', - 'tertiary' - ]; - - const handleClick = () => { - setCurrentVariantIndex((previousVariantIndex) => (previousVariantIndex + 1) % buttonVariants.length); - }; - - return ( - - ); -}; diff --git a/packages/module/src/ExtendedButton/__tests__/ExtendedButton.test.tsx b/packages/module/src/ExtendedButton/__tests__/ExtendedButton.test.tsx deleted file mode 100644 index 3a8b357..0000000 --- a/packages/module/src/ExtendedButton/__tests__/ExtendedButton.test.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import React from 'react'; -import { render, screen, waitFor } from '@testing-library/react'; -import '@testing-library/jest-dom'; -import userEvent from '@testing-library/user-event'; -import { ExtendedButton } from '../ExtendedButton'; - -test('Renders without children', () => { - render( -
- -
- ); - - expect(screen.getByTestId('container').firstChild).toBeVisible(); -}); - -test('Renders children', () => { - render(Test); - - expect(screen.getByRole('button', { name: 'Test' })).toBeVisible(); -}); - -test('Passes inherited props to the returned component', () => { - render(Test); - - expect(screen.getByRole('button')).toHaveAccessibleName('Test label'); -}); - -test('Renders as a primary button initially', () => { - render(Test); - - expect(screen.getByRole('button')).toHaveClass('pf-v6-c-button pf-m-primary', { exact: true }); -}); - -test('Renders as a secondary button once it has been clicked once', () => { - render(Test); - const button = screen.getByRole('button'); - userEvent.click(screen.getByRole('button')); - - waitFor(() => { - expect(button).toHaveClass('pf-v6-c-button pf-m-secondary', { exact: true }); - }); -}); - -test('Renders as a tertiary button once it has been clicked twice', () => { - render(Test); - - const button = screen.getByRole('button'); - userEvent.click(button); - userEvent.click(button); - - waitFor(() => { - expect(button).toHaveClass('pf-v6-c-button pf-m-tertiary', { exact: true }); - }); -}); - -test('Loops back to rendering a primary button again after being clicked three times', () => { - render(Test); - - const button = screen.getByRole('button'); - userEvent.click(button); - userEvent.click(button); - userEvent.click(button); - - expect(button).toHaveClass('pf-v6-c-button pf-m-primary', { exact: true }); -}); - -test('Matches expected default snapshot', () => { - const { asFragment } = render(Test); - - expect(asFragment()).toMatchSnapshot(); -}); diff --git a/packages/module/src/ExtendedButton/__tests__/__snapshots__/ExtendedButton.test.tsx.snap b/packages/module/src/ExtendedButton/__tests__/__snapshots__/ExtendedButton.test.tsx.snap deleted file mode 100644 index 17f6559..0000000 --- a/packages/module/src/ExtendedButton/__tests__/__snapshots__/ExtendedButton.test.tsx.snap +++ /dev/null @@ -1,20 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Matches expected default snapshot 1`] = ` - - - -`; diff --git a/packages/module/src/ExtendedButton/index.ts b/packages/module/src/ExtendedButton/index.ts deleted file mode 100644 index 03c57e3..0000000 --- a/packages/module/src/ExtendedButton/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './ExtendedButton'; diff --git a/packages/module/src/index.ts b/packages/module/src/index.ts index 03c57e3..2d52cdf 100644 --- a/packages/module/src/index.ts +++ b/packages/module/src/index.ts @@ -1 +1 @@ -export * from './ExtendedButton'; +export * from './DeleteModal';