diff --git a/src/components/Contribute/Knowledge/AttributionInformation/AttributionInformation.tsx b/src/components/Contribute/Knowledge/AttributionInformation/AttributionInformation.tsx index d34c98b6..530697dd 100644 --- a/src/components/Contribute/Knowledge/AttributionInformation/AttributionInformation.tsx +++ b/src/components/Contribute/Knowledge/AttributionInformation/AttributionInformation.tsx @@ -1,8 +1,16 @@ import React from 'react'; -import { FormFieldGroupExpandable, FormFieldGroupHeader, FormGroup } from '@patternfly/react-core/dist/dynamic/components/Form'; +import { FormFieldGroupExpandable, FormFieldGroupHeader, FormGroup, FormHelperText } from '@patternfly/react-core/dist/dynamic/components/Form'; import { TextInput } from '@patternfly/react-core/dist/dynamic/components/TextInput'; +import { HelperText } from '@patternfly/react-core/dist/dynamic/components/HelperText'; +import { HelperTextItem } from '@patternfly/react-core/dist/dynamic/components/HelperText'; +import ExclamationCircleIcon from '@patternfly/react-icons/dist/dynamic/icons/exclamation-circle-icon'; +import { ValidatedOptions } from '@patternfly/react-core/dist/esm/helpers/constants'; +import { KnowledgeFormData } from '..'; +import { checkKnowledgeFormCompletion } from '../validation'; interface Props { + knowledgeFormData: KnowledgeFormData; + setDisableAction: React.Dispatch>; titleWork: string; setTitleWork: React.Dispatch>; linkWork: string; @@ -16,6 +24,8 @@ interface Props { } const AttributionInformation: React.FC = ({ + knowledgeFormData, + setDisableAction, titleWork, setTitleWork, linkWork, @@ -27,12 +37,87 @@ const AttributionInformation: React.FC = ({ creators, setCreators }) => { + const [validTitle, setValidTitle] = React.useState(); + const [validLink, setValidLink] = React.useState(); + const [validRevision, setValidRevision] = React.useState(); + const [validLicense, setValidLicense] = React.useState(); + const [validCreators, setValidCreators] = React.useState(); + + const validateTitle = (title: string) => { + if (title.length > 0) { + setValidTitle(ValidatedOptions.success); + setDisableAction(!checkKnowledgeFormCompletion(knowledgeFormData)); + return; + } + setDisableAction(true); + setValidTitle(ValidatedOptions.error); + return; + }; + + const validateLink = (link: string) => { + if (link.length === 0) { + setDisableAction(true); + setValidLink(ValidatedOptions.error); + return; + } + try { + new URL(link); + setValidLink(ValidatedOptions.success); + setDisableAction(!checkKnowledgeFormCompletion(knowledgeFormData)); + return; + } catch (e) { + setDisableAction(true); + setValidLink(ValidatedOptions.warning); + return; + } + }; + + const validateRevision = (revision: string) => { + if (revision.length > 0) { + setValidRevision(ValidatedOptions.success); + setDisableAction(!checkKnowledgeFormCompletion(knowledgeFormData)); + return; + } + setDisableAction(true); + setValidRevision(ValidatedOptions.error); + return; + }; + + const validateLicense = (license: string) => { + if (license.length > 0) { + setValidLicense(ValidatedOptions.success); + setDisableAction(!checkKnowledgeFormCompletion(knowledgeFormData)); + return; + } + setDisableAction(true); + setValidLicense(ValidatedOptions.error); + return; + }; + + const validateCreators = (creators: string) => { + if (creators.length > 0) { + setValidCreators(ValidatedOptions.success); + setDisableAction(!checkKnowledgeFormCompletion(knowledgeFormData)); + return; + } + setDisableAction(true); + setValidCreators(ValidatedOptions.error); + return; + }; + return ( + Attribution Info * +

+ ), + id: 'attribution-info-id' + }} titleDescription="Provide attribution information." /> } @@ -43,41 +128,106 @@ const AttributionInformation: React.FC = ({ type="text" aria-label="title_work" placeholder="Enter title of work" + validated={validTitle} value={titleWork} onChange={(_event, value) => setTitleWork(value)} + onBlur={() => validateTitle(titleWork)} /> + {validTitle === ValidatedOptions.error && ( + + + } variant={validTitle}> + Title is required. + + + + )} + setLinkWork(value)} + onBlur={() => validateLink(linkWork)} /> + {validLink === ValidatedOptions.error && ( + + + } variant={validLink}> + Link to title is required. + + + + )} + {validLink === ValidatedOptions.warning && ( + + + } variant={validLink}> + Please enter a valid URL. + + + + )} setRevision(value)} + onBlur={() => validateRevision(revision)} /> + {validRevision === ValidatedOptions.error && ( + + + } variant={validRevision}> + Revision is required. + + + + )} setLicenseWork(value)} + onBlur={() => validateLicense(licenseWork)} /> + {validLicense === ValidatedOptions.error && ( + + + } variant={validLicense}> + License is required. + + + + )} setCreators(value)} + onBlur={() => validateCreators(creators)} /> + {validCreators === ValidatedOptions.error && ( + + + } variant={validCreators}> + Creators is required. + + + + )}
); diff --git a/src/components/Contribute/Knowledge/AuthorInformation/AuthorInformation.tsx b/src/components/Contribute/Knowledge/AuthorInformation/AuthorInformation.tsx index bad1b65e..f27d3488 100644 --- a/src/components/Contribute/Knowledge/AuthorInformation/AuthorInformation.tsx +++ b/src/components/Contribute/Knowledge/AuthorInformation/AuthorInformation.tsx @@ -1,21 +1,62 @@ -import React from 'react'; -import { FormFieldGroupExpandable, FormFieldGroupHeader, FormGroup } from '@patternfly/react-core/dist/dynamic/components/Form'; +import React, { useState } from 'react'; +import { FormFieldGroupExpandable, FormFieldGroupHeader, FormGroup, FormHelperText } from '@patternfly/react-core/dist/dynamic/components/Form'; import { TextInput } from '@patternfly/react-core/dist/dynamic/components/TextInput'; +import { HelperText } from '@patternfly/react-core/dist/dynamic/components/HelperText'; +import { HelperTextItem } from '@patternfly/react-core/dist/dynamic/components/HelperText'; +import ExclamationCircleIcon from '@patternfly/react-icons/dist/dynamic/icons/exclamation-circle-icon'; +import { ValidatedOptions } from '@patternfly/react-core/dist/esm/helpers/constants'; +import { KnowledgeFormData } from '..'; +import { checkKnowledgeFormCompletion } from '../validation'; interface Props { + knowledgeFormData: KnowledgeFormData; + setDisableAction: React.Dispatch>; email: string; setEmail: React.Dispatch>; name: string; setName: React.Dispatch>; } -const AuthorInformation: React.FC = ({ email, setEmail, name, setName }) => { +const AuthorInformation: React.FC = ({ knowledgeFormData, setDisableAction, email, setEmail, name, setName }) => { + const [validEmail, setValidEmail] = useState(); + const [validName, setValidName] = useState(); + + const validateEmail = (email: string) => { + const re = /\S+@\S+\.\S+/; + if (re.test(email)) { + setValidEmail(ValidatedOptions.success); + setDisableAction(!checkKnowledgeFormCompletion(knowledgeFormData)); + return; + } + setDisableAction(true); + setValidEmail(ValidatedOptions.error); + return; + }; + + const validateName = (name: string) => { + if (name.length > 0) { + setValidName(ValidatedOptions.success); + setDisableAction(!checkKnowledgeFormCompletion(knowledgeFormData)); + return; + } + setDisableAction(true); + setValidName(ValidatedOptions.error); + return; + }; + return ( + Author Info * +

+ ), + id: 'author-info-id' + }} titleDescription="Provide your information required for a GitHub DCO sign-off." /> } @@ -27,16 +68,38 @@ const AuthorInformation: React.FC = ({ email, setEmail, name, setName }) aria-label="email" placeholder="Enter your email address" value={email} + validated={validEmail} onChange={(_event, value) => setEmail(value)} + onBlur={() => validateEmail(email)} /> + {validEmail === ValidatedOptions.error && ( + + + } variant={validEmail}> + Please enter a valid email address. + + + + )} setName(value)} + onBlur={() => validateName(name)} /> + {validName === ValidatedOptions.error && ( + + + } variant={validName}> + Name is required. + + + + )}
); diff --git a/src/components/Contribute/Knowledge/DocumentInformation/DocumentInformation.tsx b/src/components/Contribute/Knowledge/DocumentInformation/DocumentInformation.tsx index 38a445bf..aa162413 100644 --- a/src/components/Contribute/Knowledge/DocumentInformation/DocumentInformation.tsx +++ b/src/components/Contribute/Knowledge/DocumentInformation/DocumentInformation.tsx @@ -4,8 +4,16 @@ import { Button } from '@patternfly/react-core/dist/dynamic/components/Button'; import { TextInput } from '@patternfly/react-core/dist/dynamic/components/TextInput'; import { UploadFile } from './../UploadFile'; import { Alert, AlertActionLink, AlertActionCloseButton } from '@patternfly/react-core/dist/dynamic/components/Alert'; +import { HelperText } from '@patternfly/react-core/dist/dynamic/components/HelperText'; +import { HelperTextItem } from '@patternfly/react-core/dist/dynamic/components/HelperText'; +import ExclamationCircleIcon from '@patternfly/react-icons/dist/dynamic/icons/exclamation-circle-icon'; +import { ValidatedOptions } from '@patternfly/react-core/dist/esm/helpers/constants'; +import { KnowledgeFormData } from '..'; +import { checkKnowledgeFormCompletion } from '../validation'; interface Props { + knowledgeFormData: KnowledgeFormData; + setDisableAction: React.Dispatch>; knowledgeDocumentRepositoryUrl: string; setKnowledgeDocumentRepositoryUrl: React.Dispatch>; knowledgeDocumentCommit: string; @@ -17,6 +25,8 @@ interface Props { } const DocumentInformation: React.FC = ({ + knowledgeFormData, + setDisableAction, knowledgeDocumentRepositoryUrl, setKnowledgeDocumentRepositoryUrl, knowledgeDocumentCommit, @@ -35,6 +45,50 @@ const DocumentInformation: React.FC = ({ const [failureAlertTitle, setFailureAlertTitle] = useState(); const [failureAlertMessage, setFailureAlertMessage] = useState(); + const [validRepo, setValidRepo] = useState(); + const [validCommit, setValidCommit] = useState(); + const [validDocumentName, setValidDocumentName] = useState(); + + const validateRepo = (repo: string) => { + if (repo.length === 0) { + setDisableAction(true); + setValidRepo(ValidatedOptions.error); + return; + } + try { + new URL(repo); + setValidRepo(ValidatedOptions.success); + setDisableAction(!checkKnowledgeFormCompletion(knowledgeFormData)); + return; + } catch (e) { + setDisableAction(true); + setValidRepo(ValidatedOptions.warning); + return; + } + }; + + const validateCommit = (commit: string) => { + if (commit.length > 0) { + setValidCommit(ValidatedOptions.success); + setDisableAction(!checkKnowledgeFormCompletion(knowledgeFormData)); + return; + } + setDisableAction(true); + setValidCommit(ValidatedOptions.error); + return; + }; + + const validateDocumentName = (documentName: string) => { + if (documentName.length > 0) { + setValidDocumentName(ValidatedOptions.success); + setDisableAction(!checkKnowledgeFormCompletion(knowledgeFormData)); + return; + } + setDisableAction(true); + setValidDocumentName(ValidatedOptions.error); + return; + }; + const handleFilesChange = (files: File[]) => { setUploadedFiles(files); setDocumentName(files.map((file) => file.name).join(', ')); // Populate the patterns field @@ -104,7 +158,17 @@ const DocumentInformation: React.FC = ({ + + Document Info * +

+ ), + id: 'doc-info-id' + }} + titleDescription="Add the relevant document's information" + /> } > @@ -132,26 +196,69 @@ const DocumentInformation: React.FC = ({ isRequired type="url" aria-label="repo" + validated={validRepo} placeholder="Enter repo url where document exists" value={knowledgeDocumentRepositoryUrl} onChange={(_event, value) => setKnowledgeDocumentRepositoryUrl(value)} + onBlur={() => validateRepo(knowledgeDocumentRepositoryUrl)} /> + {validRepo === ValidatedOptions.error && ( + + + } variant={validRepo}> + Repo URL is required. + + + + )} + {validRepo === ValidatedOptions.warning && ( + + + } variant="error"> + Please enter a valid URL. + + + + )} + setKnowledgeDocumentCommit(value)} + onBlur={() => validateCommit(knowledgeDocumentCommit)} /> + {validCommit === ValidatedOptions.error && ( + + + } variant={validCommit}> + Valid commit SHA is required. + + + + )} setDocumentName(value)} + onBlur={() => validateDocumentName(documentName)} /> + {validDocumentName === ValidatedOptions.error && ( + + + } variant={validDocumentName}> + Document name is required. + + + + )} ) : ( <> diff --git a/src/components/Contribute/Knowledge/DownloadAttribution/DownloadAttribution.tsx b/src/components/Contribute/Knowledge/DownloadAttribution/DownloadAttribution.tsx index 7b4ba60e..e979ba38 100644 --- a/src/components/Contribute/Knowledge/DownloadAttribution/DownloadAttribution.tsx +++ b/src/components/Contribute/Knowledge/DownloadAttribution/DownloadAttribution.tsx @@ -4,11 +4,12 @@ import { validateFields } from '../validation'; import { ActionGroupAlertContent, KnowledgeFormData } from '..'; interface Props { + disableAction: boolean; knowledgeFormData: KnowledgeFormData; setActionGroupAlertContent: React.Dispatch>; } -const DownloadAttribution: React.FC = ({ knowledgeFormData, setActionGroupAlertContent }) => { +const DownloadAttribution: React.FC = ({ disableAction, knowledgeFormData, setActionGroupAlertContent }) => { const handleDownloadAttribution = () => { // Because I have overly complicated the validatedFields function all fields are being checked and not just the attribution ones here. Not ideal. if (!validateFields(knowledgeFormData, setActionGroupAlertContent)) return; @@ -31,7 +32,7 @@ const DownloadAttribution: React.FC = ({ knowledgeFormData, setActionGrou }; return ( - ); diff --git a/src/components/Contribute/Knowledge/DownloadYaml/DownloadYaml.tsx b/src/components/Contribute/Knowledge/DownloadYaml/DownloadYaml.tsx index 399acfd6..13c182a9 100644 --- a/src/components/Contribute/Knowledge/DownloadYaml/DownloadYaml.tsx +++ b/src/components/Contribute/Knowledge/DownloadYaml/DownloadYaml.tsx @@ -6,12 +6,13 @@ import { KnowledgeYamlData, SchemaVersion } from '@/types'; import { dumpYaml } from '@/utils/yamlConfig'; interface Props { + disableAction: boolean; knowledgeFormData: KnowledgeFormData; setActionGroupAlertContent: React.Dispatch>; githubUsername: string | undefined; } -const DownloadYaml: React.FC = ({ knowledgeFormData, setActionGroupAlertContent, githubUsername }) => { +const DownloadYaml: React.FC = ({ disableAction, knowledgeFormData, setActionGroupAlertContent, githubUsername }) => { const handleDownloadYaml = () => { if (!validateFields(knowledgeFormData, setActionGroupAlertContent)) return; @@ -45,7 +46,7 @@ const DownloadYaml: React.FC = ({ knowledgeFormData, setActionGroupAlertC document.body.removeChild(a); }; return ( - ); diff --git a/src/components/Contribute/Knowledge/FilePathInformation/FilePathInformation.tsx b/src/components/Contribute/Knowledge/FilePathInformation/FilePathInformation.tsx index 56f3607e..003aef21 100644 --- a/src/components/Contribute/Knowledge/FilePathInformation/FilePathInformation.tsx +++ b/src/components/Contribute/Knowledge/FilePathInformation/FilePathInformation.tsx @@ -13,7 +13,14 @@ const FilePathInformation: React.FC = ({ setFilePath }) => { toggleAriaLabel="Details" header={ + File Path Info * +

+ ), + id: 'file-path-info-id' + }} titleDescription="Specify the file path for the QnA and Attribution files." /> } diff --git a/src/components/Contribute/Knowledge/KnowledgeDescription/KnowledgeDescription.tsx b/src/components/Contribute/Knowledge/KnowledgeDescription/KnowledgeDescription.tsx deleted file mode 100644 index 5bcb5b48..00000000 --- a/src/components/Contribute/Knowledge/KnowledgeDescription/KnowledgeDescription.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react'; -import { FormFieldGroupExpandable, FormFieldGroupHeader } from '@patternfly/react-core/dist/dynamic/components/Form'; -import KnowledgeDescriptionContent from './KnowledgeDescriptionContent'; - -const KnowledgeDescription: React.FC = () => { - return ( - - } - > - - - ); -}; - -export default KnowledgeDescription; diff --git a/src/components/Contribute/Knowledge/KnowledgeDescription/KnowledgeDescriptionContent.tsx b/src/components/Contribute/Knowledge/KnowledgeDescription/KnowledgeDescriptionContent.tsx index 329e7db9..05619fc1 100644 --- a/src/components/Contribute/Knowledge/KnowledgeDescription/KnowledgeDescriptionContent.tsx +++ b/src/components/Contribute/Knowledge/KnowledgeDescription/KnowledgeDescriptionContent.tsx @@ -6,17 +6,17 @@ const KnowledgeDescriptionContent: React.FunctionComponent = () => { return (

- Knowledge in InstructLab is represented by question and answer pairs that involve facts, data, or references. This knowledge is represented in - the taxonomy tree and each node of this tree contains a qna.yaml file. -

-
+ + Knowledge in InstructLab is represented by question and answer pairs that involve facts, data, or references. This knowledge is represented + in the taxonomy tree and each node of this tree contains a qna.yaml file. + - -
+

); }; diff --git a/src/components/Contribute/Knowledge/KnowledgeInformation/KnowledgeInformation.tsx b/src/components/Contribute/Knowledge/KnowledgeInformation/KnowledgeInformation.tsx index 69567fef..5ea76d07 100644 --- a/src/components/Contribute/Knowledge/KnowledgeInformation/KnowledgeInformation.tsx +++ b/src/components/Contribute/Knowledge/KnowledgeInformation/KnowledgeInformation.tsx @@ -2,8 +2,16 @@ import React from 'react'; import { FormFieldGroupExpandable, FormFieldGroupHeader, FormGroup } from '@patternfly/react-core/dist/dynamic/components/Form'; import { TextInput } from '@patternfly/react-core/dist/dynamic/components/TextInput'; import { TextArea } from '@patternfly/react-core/dist/dynamic/components/TextArea'; +import { HelperText } from '@patternfly/react-core/dist/dynamic/components/HelperText'; +import { HelperTextItem } from '@patternfly/react-core/dist/dynamic/components/HelperText'; +import ExclamationCircleIcon from '@patternfly/react-icons/dist/dynamic/icons/exclamation-circle-icon'; +import { ValidatedOptions } from '@patternfly/react-core/dist/esm/helpers/constants'; +import { KnowledgeFormData } from '..'; +import { checkKnowledgeFormCompletion } from '../validation'; interface Props { + knowledgeFormData: KnowledgeFormData; + setDisableAction: React.Dispatch>; submissionSummary: string; setSubmissionSummary: React.Dispatch>; domain: string; @@ -13,6 +21,8 @@ interface Props { } const KnowledgeInformation: React.FC = ({ + knowledgeFormData, + setDisableAction, submissionSummary, setSubmissionSummary, domain, @@ -20,13 +30,57 @@ const KnowledgeInformation: React.FC = ({ documentOutline, setDocumentOutline }) => { + const [validDescription, setValidDescription] = React.useState(); + const [validDomain, setValidDomain] = React.useState(); + const [validOutline, setValidOutline] = React.useState(); + + const validateDescription = (description: string) => { + if (description.length > 0 && description.length < 60) { + setValidDescription(ValidatedOptions.success); + setDisableAction(!checkKnowledgeFormCompletion(knowledgeFormData)); + return; + } + setDisableAction(true); + setValidDescription(ValidatedOptions.error); + return; + }; + + const validateDomain = (domain: string) => { + if (domain.length > 0) { + setValidDomain(ValidatedOptions.success); + setDisableAction(!checkKnowledgeFormCompletion(knowledgeFormData)); + return; + } + setDisableAction(true); + setValidDomain(ValidatedOptions.error); + return; + }; + + const validateOutline = (outline: string) => { + if (outline.length > 40) { + setValidOutline(ValidatedOptions.success); + setDisableAction(!checkKnowledgeFormCompletion(knowledgeFormData)); + return; + } + setDisableAction(true); + setValidOutline(ValidatedOptions.error); + return; + }; + return ( + Knowledge Info * +

+ ), + id: 'knowledge-info-id' + }} titleDescription="Provide brief information about the knowledge." /> } @@ -38,26 +92,55 @@ const KnowledgeInformation: React.FC = ({ aria-label="submission_summary" placeholder="Enter a brief description for a submission summary (60 character max)" value={submissionSummary} + validated={validDescription} onChange={(_event, value) => setSubmissionSummary(value)} + onBlur={() => validateDescription(submissionSummary)} maxLength={60} /> + {validDescription === ValidatedOptions.error && ( + + } variant={validDescription}> + Description is required and must be less than 60 characters + + + )} + setDomain(value)} + onBlur={() => validateDomain(domain)} /> + {validDomain === ValidatedOptions.error && ( + + } variant={validDomain}> + Domain is required + + + )} +