From 672ce79d74d78f7019795e4625bac6e6d4d7eec8 Mon Sep 17 00:00:00 2001 From: adamviktora Date: Tue, 17 Sep 2024 18:28:29 +0200 Subject: [PATCH] feat(DeleteModal): first version --- .../content/examples/Basic.tsx | 141 +++++++++++++++- .../patternfly-docs/content/examples/basic.md | 3 +- .../ai-infra-ui-components/react.js | 41 ++++- packages/module/release.config.js | 4 +- .../module/src/DeleteModal/DeleteModal.tsx | 156 ++++++++++++++++++ packages/module/src/DeleteModal/index.ts | 1 + packages/module/src/index.ts | 1 + 7 files changed, 339 insertions(+), 8 deletions(-) create mode 100644 packages/module/src/DeleteModal/DeleteModal.tsx create mode 100644 packages/module/src/DeleteModal/index.ts diff --git a/packages/module/patternfly-docs/content/examples/Basic.tsx b/packages/module/patternfly-docs/content/examples/Basic.tsx index b532338..9dfd44a 100644 --- a/packages/module/patternfly-docs/content/examples/Basic.tsx +++ b/packages/module/patternfly-docs/content/examples/Basic.tsx @@ -1,4 +1,141 @@ import React from 'react'; -import { ExtendedButton } from '@patternfly/ai-infra-ui-components'; +import { Button, Stack, StackItem } from '@patternfly/react-core'; +import { DeleteModal } from '@patternfly/ai-infra-ui-components'; -export const BasicExample: React.FunctionComponent = () => My custom extension button; +export const DeleteModalBasic: React.FunctionComponent = () => { + const [isModalRecoverableOpen, setIsModalRecoverableOpen] = React.useState(false); + const [isModalDestructiveOpen, setIsModalDestructiveOpen] = React.useState(false); + const [isModalExtraDestructiveOpen, setIsModalExtraDestructiveOpen] = React.useState(false); + + const [isModalMultiExtraDestructiveOpen, setIsModalMultiExtraDestructiveOpen] = React.useState(false); + + const [isModalModelRegistryOpen, setIsModalModelRegistryOpen] = React.useState(false); + const [isModalPipelineServerOpen, setIsModalPipelineServerOpen] = React.useState(false); + + const handleModalRecoverableToggle = (_event: KeyboardEvent | React.MouseEvent) => { + setIsModalRecoverableOpen(!isModalRecoverableOpen); + }; + + const handleModalDestructiveToggle = (_event: KeyboardEvent | React.MouseEvent) => { + setIsModalDestructiveOpen(!isModalDestructiveOpen); + }; + + const handleModalExtraDestructiveToggle = (_event: KeyboardEvent | React.MouseEvent) => { + setIsModalExtraDestructiveOpen(!isModalExtraDestructiveOpen); + }; + + const handleModalMultiExtraDestructiveToggle = (_event: KeyboardEvent | React.MouseEvent) => { + setIsModalMultiExtraDestructiveOpen(!isModalMultiExtraDestructiveOpen); + }; + + const handleModalModelRegistryToggle = (_event: KeyboardEvent | React.MouseEvent) => { + setIsModalModelRegistryOpen(!isModalModelRegistryOpen); + }; + + const handleModalPipelineServerToggle = (_event: KeyboardEvent | React.MouseEvent) => { + setIsModalPipelineServerOpen(!isModalPipelineServerOpen); + }; + + return ( + <> + +
Modals with one item to delete
+ + + + + + + + + +
+ + +
Modals with multiple items to delete
+ + + +
+ + +
Modals with custom delete messages
+ + + + + + +
+ + + + + + {/* Modals with multiple items to delete */} + + + {/* Modals with custom delete messages */} + + + + ); +}; diff --git a/packages/module/patternfly-docs/content/examples/basic.md b/packages/module/patternfly-docs/content/examples/basic.md index fc7958b..31e66de 100644 --- a/packages/module/patternfly-docs/content/examples/basic.md +++ b/packages/module/patternfly-docs/content/examples/basic.md @@ -9,10 +9,11 @@ id: AI-infra-ui-components 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'] +propComponents: ['ExtendedButton', 'DeleteModal'] --- import { ExtendedButton } from "@patternfly/ai-infra-ui-components"; +import { DeleteModal } from "@patternfly/ai-infra-ui-components"; ## Basic usage 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 index f157536..2f20f82 100644 --- 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 @@ -1,6 +1,7 @@ import React from 'react'; import { AutoLinkHeader, Example, Link as PatternflyThemeLink } from '@patternfly/documentation-framework/components'; import { ExtendedButton } from "@patternfly/ai-infra-ui-components"; +import { DeleteModal } from "@patternfly/ai-infra-ui-components"; const pageData = { "id": "AI-infra-ui-components", "section": "extensions", @@ -26,6 +27,39 @@ const pageData = { "description": "Content to render inside the extended button component" } ] + }, + { + "name": "DeleteModal", + "description": "", + "props": [ + { + "name": "deleteVariant", + "type": "'extra-destructive' | 'destructive' | 'easily-recoverable'", + "description": "Delete variant. Destructive and extra-destructive variants will show a warning icon and danger button. For extra-destructive variant, text input confirmation is needed.", + "defaultValue": "'destructive'" + }, + { + "name": "error", + "type": "Error", + "description": "Error indicating deletion has failed" + }, + { + "name": "message", + "type": "{\n toDelete?: string;\n resourcesToDelete?: string;\n endNote?: string;\n}", + "description": "Message describing what will deleted", + "defaultValue": "{\n resourcesToDelete: '',\n endNote: ''\n}" + }, + { + "name": "onCancel", + "type": "() => void", + "description": "Callback on clicking the cancel button" + }, + { + "name": "onDelete", + "type": "() => void", + "description": "Callback on clicking the delete button" + } + ] } ], "examples": [ @@ -36,15 +70,16 @@ const pageData = { ] }; pageData.liveContext = { - ExtendedButton + ExtendedButton, + DeleteModal }; pageData.examples = { 'Example': props => - My custom extension button;\n","title":"Example","lang":"js","className":""}}> + {\n const [isModalRecoverableOpen, setIsModalRecoverableOpen] = React.useState(false);\n const [isModalDestructiveOpen, setIsModalDestructiveOpen] = React.useState(false);\n const [isModalExtraDestructiveOpen, setIsModalExtraDestructiveOpen] = React.useState(false);\n\n const [isModalMultiExtraDestructiveOpen, setIsModalMultiExtraDestructiveOpen] = React.useState(false);\n\n const [isModalModelRegistryOpen, setIsModalModelRegistryOpen] = React.useState(false);\n const [isModalPipelineServerOpen, setIsModalPipelineServerOpen] = React.useState(false);\n\n const handleModalRecoverableToggle = (_event: KeyboardEvent | React.MouseEvent) => {\n setIsModalRecoverableOpen(!isModalRecoverableOpen);\n };\n\n const handleModalDestructiveToggle = (_event: KeyboardEvent | React.MouseEvent) => {\n setIsModalDestructiveOpen(!isModalDestructiveOpen);\n };\n\n const handleModalExtraDestructiveToggle = (_event: KeyboardEvent | React.MouseEvent) => {\n setIsModalExtraDestructiveOpen(!isModalExtraDestructiveOpen);\n };\n\n const handleModalMultiExtraDestructiveToggle = (_event: KeyboardEvent | React.MouseEvent) => {\n setIsModalMultiExtraDestructiveOpen(!isModalMultiExtraDestructiveOpen);\n };\n\n const handleModalModelRegistryToggle = (_event: KeyboardEvent | React.MouseEvent) => {\n setIsModalModelRegistryOpen(!isModalModelRegistryOpen);\n };\n\n const handleModalPipelineServerToggle = (_event: KeyboardEvent | React.MouseEvent) => {\n setIsModalPipelineServerOpen(!isModalPipelineServerOpen);\n };\n\n return (\n <>\n \n
Modals with one item to delete
\n \n \n \n \n \n \n \n \n \n
\n\n \n
Modals with multiple items to delete
\n \n \n \n
\n\n \n
Modals with custom delete messages
\n \n \n \n \n \n \n
\n\n \n \n \n\n {/* Modals with multiple items to delete */}\n \n\n {/* Modals with custom delete messages */}\n \n \n \n );\n};\n","title":"Example","lang":"js","className":""}}>
, 'Fullscreen example': props => - My custom extension button;\n","title":"Fullscreen example","lang":"js","isFullscreen":true,"className":""}}> + {\n const [isModalRecoverableOpen, setIsModalRecoverableOpen] = React.useState(false);\n const [isModalDestructiveOpen, setIsModalDestructiveOpen] = React.useState(false);\n const [isModalExtraDestructiveOpen, setIsModalExtraDestructiveOpen] = React.useState(false);\n\n const [isModalMultiExtraDestructiveOpen, setIsModalMultiExtraDestructiveOpen] = React.useState(false);\n\n const [isModalModelRegistryOpen, setIsModalModelRegistryOpen] = React.useState(false);\n const [isModalPipelineServerOpen, setIsModalPipelineServerOpen] = React.useState(false);\n\n const handleModalRecoverableToggle = (_event: KeyboardEvent | React.MouseEvent) => {\n setIsModalRecoverableOpen(!isModalRecoverableOpen);\n };\n\n const handleModalDestructiveToggle = (_event: KeyboardEvent | React.MouseEvent) => {\n setIsModalDestructiveOpen(!isModalDestructiveOpen);\n };\n\n const handleModalExtraDestructiveToggle = (_event: KeyboardEvent | React.MouseEvent) => {\n setIsModalExtraDestructiveOpen(!isModalExtraDestructiveOpen);\n };\n\n const handleModalMultiExtraDestructiveToggle = (_event: KeyboardEvent | React.MouseEvent) => {\n setIsModalMultiExtraDestructiveOpen(!isModalMultiExtraDestructiveOpen);\n };\n\n const handleModalModelRegistryToggle = (_event: KeyboardEvent | React.MouseEvent) => {\n setIsModalModelRegistryOpen(!isModalModelRegistryOpen);\n };\n\n const handleModalPipelineServerToggle = (_event: KeyboardEvent | React.MouseEvent) => {\n setIsModalPipelineServerOpen(!isModalPipelineServerOpen);\n };\n\n return (\n <>\n \n
Modals with one item to delete
\n \n \n \n \n \n \n \n \n \n
\n\n \n
Modals with multiple items to delete
\n \n \n \n
\n\n \n
Modals with custom delete messages
\n \n \n \n \n \n \n
\n\n \n \n \n\n {/* Modals with multiple items to delete */}\n \n\n {/* Modals with custom delete messages */}\n \n \n \n );\n};\n","title":"Fullscreen example","lang":"js","isFullscreen":true,"className":""}}>
}; 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..844b390 --- /dev/null +++ b/packages/module/src/DeleteModal/DeleteModal.tsx @@ -0,0 +1,156 @@ +import React from 'react'; +import { + Button, + FormGroup, + List, + ListItem, + Modal, + ModalBody, + ModalFooter, + ModalHeader, + ModalProps, + Stack, + StackItem, + TextInput +} from '@patternfly/react-core'; + +/** Props specifying either one item or a list of items to delete */ +type ItemProps = + | { + /** Type of item to delete (e.g. project, pipeline server, model registry) */ + item: string; + /** Name of the item to delete */ + itemName: string; + items?: never; + itemNames?: never; + } + | { + /** Type of items to delete in plural (e.g. projects, pipeline servers, model registries) */ + items: string; + /** List of names of the items to delete */ + itemNames: string[]; + item?: never; + itemName?: never; + }; + +export type DeleteModalProps = Omit & + ItemProps & { + /** 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'; + /** Message describing what will deleted */ + message?: { + toDelete?: string; + resourcesToDelete?: string; + endNote?: string; + }; + /** Callback on clicking the delete button */ + onDelete?: () => void; + /** Callback on clicking the cancel button */ + onCancel?: () => void; + /** Error indicating deletion has failed */ + error?: Error; + }; + +export const DeleteModal: React.FunctionComponent = ({ + item, + items, + itemName, + itemNames, + deleteVariant = 'destructive', + onDelete, + onCancel, + message = { + resourcesToDelete: '', + endNote: '' + }, + error, + ref, + ...props +}: DeleteModalProps) => { + const isPlural = item === undefined; + + const [confirmationText, setConfirmationText] = React.useState(''); + const expectedConfirmationText = isPlural ? `delete ${itemNames.length} ${items}` : itemName; + + const confirmed = deleteVariant === 'extra-destructive' ? confirmationText === expectedConfirmationText : true; + + const toDeleteMessage = message.toDelete ?? ` and all of ${isPlural ? 'their' : 'its'} resources`; + + const itemToDelete = `${itemNames ? `${itemNames.length} ` : ''}${item ?? items}`; + const modalHeaderTitle = `Delete ${itemToDelete}?`; + + return ( + + + + + + The{' '} + {isPlural ? ( + `following ${items}` + ) : ( + <> + {itemName} {item} + + )} + {toDeleteMessage} + {message.resourcesToDelete} will be deleted{isPlural && !message.endNote ? ':' : '.'} {message.endNote} + {isPlural && ( + + {itemNames.map((name) => ( + + {name} + + ))} + + )} + + {deleteVariant === 'extra-destructive' && ( + + + Type {expectedConfirmationText} to confirm deletion: + + } + > + setConfirmationText(value)} + // validated={confirmed ? 'success' : 'default'} + /> + + + )} + + + + + + {/* {error && ( + + {error.message} + + )} */} + + + ); +}; 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/index.ts b/packages/module/src/index.ts index 03c57e3..81d39ab 100644 --- a/packages/module/src/index.ts +++ b/packages/module/src/index.ts @@ -1 +1,2 @@ +export * from './DeleteModal'; export * from './ExtendedButton';