From 6b0630086c13a1988833df97c98ae65411d13939 Mon Sep 17 00:00:00 2001 From: Anil Vishnoi Date: Fri, 30 Aug 2024 13:48:37 -0700 Subject: [PATCH 1/2] Reset all validation conditions after the successful submit of the knowledge Signed-off-by: Anil Vishnoi --- .../AttributionInformation.tsx | 12 ++- .../AuthorInformation/AuthorInformation.tsx | 10 ++- .../DocumentInformation.tsx | 10 ++- .../FilePathInformation.tsx | 5 +- .../KnowledgeDescriptionContent.tsx | 1 + .../KnowledgeInformation.tsx | 10 ++- .../KnowledgeQuestionAnswerPairs.tsx | 8 +- src/components/Contribute/Knowledge/index.tsx | 90 +++++++++---------- .../Contribute/Knowledge/validation.tsx | 70 +++++++++++---- src/components/Contribute/Skill/index.tsx | 4 +- src/components/PathService/PathService.tsx | 8 +- 11 files changed, 150 insertions(+), 78 deletions(-) diff --git a/src/components/Contribute/Knowledge/AttributionInformation/AttributionInformation.tsx b/src/components/Contribute/Knowledge/AttributionInformation/AttributionInformation.tsx index 530697dd..534f5318 100644 --- a/src/components/Contribute/Knowledge/AttributionInformation/AttributionInformation.tsx +++ b/src/components/Contribute/Knowledge/AttributionInformation/AttributionInformation.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect } 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'; @@ -9,6 +9,7 @@ import { KnowledgeFormData } from '..'; import { checkKnowledgeFormCompletion } from '../validation'; interface Props { + reset: boolean; knowledgeFormData: KnowledgeFormData; setDisableAction: React.Dispatch>; titleWork: string; @@ -24,6 +25,7 @@ interface Props { } const AttributionInformation: React.FC = ({ + reset, knowledgeFormData, setDisableAction, titleWork, @@ -43,6 +45,14 @@ const AttributionInformation: React.FC = ({ const [validLicense, setValidLicense] = React.useState(); const [validCreators, setValidCreators] = React.useState(); + useEffect(() => { + setValidTitle(ValidatedOptions.default); + setValidLink(ValidatedOptions.default); + setValidRevision(ValidatedOptions.default); + setValidLicense(ValidatedOptions.default); + setValidCreators(ValidatedOptions.default); + }, [reset]); + const validateTitle = (title: string) => { if (title.length > 0) { setValidTitle(ValidatedOptions.success); diff --git a/src/components/Contribute/Knowledge/AuthorInformation/AuthorInformation.tsx b/src/components/Contribute/Knowledge/AuthorInformation/AuthorInformation.tsx index f27d3488..dcec4db7 100644 --- a/src/components/Contribute/Knowledge/AuthorInformation/AuthorInformation.tsx +++ b/src/components/Contribute/Knowledge/AuthorInformation/AuthorInformation.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useEffect, 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'; @@ -9,6 +9,7 @@ import { KnowledgeFormData } from '..'; import { checkKnowledgeFormCompletion } from '../validation'; interface Props { + reset: boolean; knowledgeFormData: KnowledgeFormData; setDisableAction: React.Dispatch>; email: string; @@ -16,7 +17,7 @@ interface Props { name: string; setName: React.Dispatch>; } -const AuthorInformation: React.FC = ({ knowledgeFormData, setDisableAction, email, setEmail, name, setName }) => { +const AuthorInformation: React.FC = ({ reset, knowledgeFormData, setDisableAction, email, setEmail, name, setName }) => { const [validEmail, setValidEmail] = useState(); const [validName, setValidName] = useState(); @@ -43,6 +44,11 @@ const AuthorInformation: React.FC = ({ knowledgeFormData, setDisableActio return; }; + useEffect(() => { + setValidEmail(ValidatedOptions.default); + setValidName(ValidatedOptions.default); + }, [reset]); + return ( >; knowledgeDocumentRepositoryUrl: string; @@ -25,6 +26,7 @@ interface Props { } const DocumentInformation: React.FC = ({ + reset, knowledgeFormData, setDisableAction, knowledgeDocumentRepositoryUrl, @@ -49,6 +51,12 @@ const DocumentInformation: React.FC = ({ const [validCommit, setValidCommit] = useState(); const [validDocumentName, setValidDocumentName] = useState(); + useEffect(() => { + setValidRepo(ValidatedOptions.default); + setValidCommit(ValidatedOptions.default); + setValidDocumentName(ValidatedOptions.default); + }, [reset]); + const validateRepo = (repo: string) => { if (repo.length === 0) { setDisableAction(true); diff --git a/src/components/Contribute/Knowledge/FilePathInformation/FilePathInformation.tsx b/src/components/Contribute/Knowledge/FilePathInformation/FilePathInformation.tsx index 003aef21..3429d963 100644 --- a/src/components/Contribute/Knowledge/FilePathInformation/FilePathInformation.tsx +++ b/src/components/Contribute/Knowledge/FilePathInformation/FilePathInformation.tsx @@ -3,10 +3,11 @@ import { FormFieldGroupExpandable, FormFieldGroupHeader, FormGroup } from '@patt import PathService from '@/components/PathService/PathService'; interface Props { + reset?: boolean; setFilePath: React.Dispatch>; } -const FilePathInformation: React.FC = ({ setFilePath }) => { +const FilePathInformation: React.FC = ({ reset, setFilePath }) => { return ( = ({ setFilePath }) => { } > - + ); diff --git a/src/components/Contribute/Knowledge/KnowledgeDescription/KnowledgeDescriptionContent.tsx b/src/components/Contribute/Knowledge/KnowledgeDescription/KnowledgeDescriptionContent.tsx index 05619fc1..ca5b0805 100644 --- a/src/components/Contribute/Knowledge/KnowledgeDescription/KnowledgeDescriptionContent.tsx +++ b/src/components/Contribute/Knowledge/KnowledgeDescription/KnowledgeDescriptionContent.tsx @@ -7,6 +7,7 @@ const KnowledgeDescriptionContent: React.FunctionComponent = () => {

+
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 5ea76d07..a54c6a4c 100644 --- a/src/components/Contribute/Knowledge/KnowledgeInformation/KnowledgeInformation.tsx +++ b/src/components/Contribute/Knowledge/KnowledgeInformation/KnowledgeInformation.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect } 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'; @@ -10,6 +10,7 @@ import { KnowledgeFormData } from '..'; import { checkKnowledgeFormCompletion } from '../validation'; interface Props { + reset: boolean; knowledgeFormData: KnowledgeFormData; setDisableAction: React.Dispatch>; submissionSummary: string; @@ -21,6 +22,7 @@ interface Props { } const KnowledgeInformation: React.FC = ({ + reset, knowledgeFormData, setDisableAction, submissionSummary, @@ -34,6 +36,12 @@ const KnowledgeInformation: React.FC = ({ const [validDomain, setValidDomain] = React.useState(); const [validOutline, setValidOutline] = React.useState(); + useEffect(() => { + setValidDescription(ValidatedOptions.default); + setValidDomain(ValidatedOptions.default); + setValidOutline(ValidatedOptions.default); + }, [reset]); + const validateDescription = (description: string) => { if (description.length > 0 && description.length < 60) { setValidDescription(ValidatedOptions.success); diff --git a/src/components/Contribute/Knowledge/KnowledgeQuestionAnswerPairs/KnowledgeQuestionAnswerPairs.tsx b/src/components/Contribute/Knowledge/KnowledgeQuestionAnswerPairs/KnowledgeQuestionAnswerPairs.tsx index ba13655d..e9ddb6db 100644 --- a/src/components/Contribute/Knowledge/KnowledgeQuestionAnswerPairs/KnowledgeQuestionAnswerPairs.tsx +++ b/src/components/Contribute/Knowledge/KnowledgeQuestionAnswerPairs/KnowledgeQuestionAnswerPairs.tsx @@ -51,7 +51,7 @@ const KnowledgeQuestionAnswerPairs: React.FC = ({ } variant={seedExample.isContextValid}> - Context is required. It must be non empty and less than 500 characters. + {seedExample.validationError || 'Context is required. It must be non empty and less than 500 characters.'} @@ -98,7 +98,8 @@ const KnowledgeQuestionAnswerPairs: React.FC = ({ } variant={seedExample.questionAndAnswers[questionAnswerIndex].isQuestionValid}> - Question is required. Total length of all Q&A pairs should be less than 250 characters. + {seedExample.questionAndAnswers[questionAnswerIndex].questionValidationError || + 'Question is required. Total length of all Q&A pairs should be less than 250 characters.'} @@ -118,7 +119,8 @@ const KnowledgeQuestionAnswerPairs: React.FC = ({ } variant={seedExample.questionAndAnswers[questionAnswerIndex].isAnswerValid}> - Answer is required. Total length of all Q&A pairs should be less than 250 characters. + {seedExample.questionAndAnswers[questionAnswerIndex].answerValidationError || + 'Answer is required. Total length of all Q&A pairs should be less than 250 characters.'} diff --git a/src/components/Contribute/Knowledge/index.tsx b/src/components/Contribute/Knowledge/index.tsx index bb09f9d0..5c5350e0 100644 --- a/src/components/Contribute/Knowledge/index.tsx +++ b/src/components/Contribute/Knowledge/index.tsx @@ -36,8 +36,10 @@ export interface QuestionAndAnswerPair { immutable: boolean; question: string; isQuestionValid: ValidatedOptions; + questionValidationError?: string; answer: string; isAnswerValid: ValidatedOptions; + answerValidationError?: string; } export interface SeedExample { @@ -45,6 +47,7 @@ export interface SeedExample { isExpanded: boolean; context: string; isContextValid: ValidatedOptions; + validationError?: string; questionAndAnswers: QuestionAndAnswerPair[]; } @@ -76,21 +79,6 @@ export interface ActionGroupAlertContent { export const KnowledgeForm: React.FunctionComponent = () => { const { data: session } = useSession(); const [githubUsername, setGithubUsername] = useState(''); - - useEffect(() => { - const fetchUsername = async () => { - if (session?.accessToken) { - try { - const fetchedUsername = await getGitHubUsername(session.accessToken); - setGithubUsername(fetchedUsername); - } catch (error) { - console.error('Failed to fetch GitHub username:', error); - } - } - }; - - fetchUsername(); - }, [session?.accessToken]); // Author Information const [email, setEmail] = useState(''); const [name, setName] = useState(''); @@ -103,9 +91,26 @@ export const KnowledgeForm: React.FunctionComponent = () => { // File Path Information const [filePath, setFilePath] = useState(''); - // Knowledge Question Answer Pairs + const [knowledgeDocumentRepositoryUrl, setKnowledgeDocumentRepositoryUrl] = useState(''); + const [knowledgeDocumentCommit, setKnowledgeDocumentCommit] = useState(''); + // This used to be 'patterns' but I am not totally sure what this variable actually is... + const [documentName, setDocumentName] = useState(''); + // Attribution Information // State + const [titleWork, setTitleWork] = useState(''); + const [linkWork, setLinkWork] = useState(''); + const [revision, setRevision] = useState(''); + const [licenseWork, setLicenseWork] = useState(''); + const [creators, setCreators] = useState(''); + + const [actionGroupAlertContent, setActionGroupAlertContent] = useState(); + + const [uploadedFiles, setUploadedFiles] = useState([]); + const [disableAction, setDisableAction] = useState(true); + const [isModalOpen, setIsModalOpen] = useState(false); + const [yamlContent, setYamlContent] = useState(''); + const [reset, setReset] = useState(false); const emptySeedExample: SeedExample = { immutable: true, @@ -145,6 +150,21 @@ export const KnowledgeForm: React.FunctionComponent = () => { emptySeedExample ]); + useEffect(() => { + const fetchUsername = async () => { + if (session?.accessToken) { + try { + const fetchedUsername = await getGitHubUsername(session.accessToken); + setGithubUsername(fetchedUsername); + } catch (error) { + console.error('Failed to fetch GitHub username:', error); + } + } + }; + + fetchUsername(); + }, [session?.accessToken]); + // Functions const validateContext = (context: string): ValidatedOptions => { @@ -324,39 +344,10 @@ export const KnowledgeForm: React.FunctionComponent = () => { setSeedExamples(seedExamples.filter((_, index: number) => index !== seedExampleIndex)); }; - // Document Information - // State - - const [knowledgeDocumentRepositoryUrl, setKnowledgeDocumentRepositoryUrl] = useState(''); - const [knowledgeDocumentCommit, setKnowledgeDocumentCommit] = useState(''); - // This used to be 'patterns' but I am not totally sure what this variable actually is... - const [documentName, setDocumentName] = useState(''); - - // Attribution Information - // State - const [titleWork, setTitleWork] = useState(''); - const [linkWork, setLinkWork] = useState(''); - const [revision, setRevision] = useState(''); - const [licenseWork, setLicenseWork] = useState(''); - const [creators, setCreators] = useState(''); - - const [actionGroupAlertContent, setActionGroupAlertContent] = useState(); - - // functions - const onCloseActionGroupAlert = () => { setActionGroupAlertContent(undefined); }; - // Submit - - // break - - const [uploadedFiles, setUploadedFiles] = useState([]); - const [disableAction, setDisableAction] = useState(true); - const [isModalOpen, setIsModalOpen] = useState(false); - const [yamlContent, setYamlContent] = useState(''); - const resetForm = (): void => { setEmail(''); setName(''); @@ -375,6 +366,9 @@ export const KnowledgeForm: React.FunctionComponent = () => { setFilePath(''); setSeedExamples([emptySeedExample, emptySeedExample, emptySeedExample, emptySeedExample, emptySeedExample]); setDisableAction(true); + + // setReset is just reset button, value has no impact. + setReset(reset ? false : true); }; const handleViewYaml = () => { @@ -445,6 +439,7 @@ export const KnowledgeForm: React.FunctionComponent = () => {

{ /> { setDocumentOutline={setDocumentOutline} /> - + { /> { /> { @@ -5,19 +6,32 @@ const validateEmail = (email: string): boolean => { return emailRegex.test(email); }; -const hasDuplicateSeedExamples = (seedExamples: SeedExample[]): boolean => { - // Just checking contexts for duplication. - const contexts = new Set(); - - seedExamples.forEach((seedExample) => { +const hasDuplicateSeedExamples = (seedExamples: SeedExample[]): { duplicate: boolean; index: number } => { + const contexts = new Set(); + for (let index = 0; index < seedExamples.length; index++) { + const seedExample = seedExamples[index]; if (!contexts.has(seedExample.context)) { contexts.add(seedExample.context); } else { - return true; + return { duplicate: true, index: index }; } - }); + } + return { duplicate: false, index: -1 }; +}; - return false; +// Check if the question in Q&A pairs in a each seed example are unique +const hasDuplicateQuestionAndAnswerPairs = (seedExample: SeedExample): { duplicate: boolean; index: number } => { + const questions = new Set(); + for (let index = 0; index < seedExample.questionAndAnswers.length; index++) { + const questionAndAnswerPair = seedExample.questionAndAnswers[index]; + const question = questionAndAnswerPair.question; + if (!questions.has(question)) { + questions.add(question); + } else { + return { duplicate: true, index: index }; + } + } + return { duplicate: false, index: -1 }; }; // Validate that the total length of all the question and answer pairs in a seed example is not more than 250 characters @@ -70,6 +84,20 @@ export const validateFields = ( return false; } + // checking for seedExample duplication + const { duplicate, index } = hasDuplicateSeedExamples(knowledgeFormData.seedExamples); + if (duplicate) { + knowledgeFormData.seedExamples[index].isContextValid = ValidatedOptions.error; + knowledgeFormData.seedExamples[index].validationError = 'This is duplicate context, please provide unique contexts.'; + const actionGroupAlertContent: ActionGroupAlertContent = { + title: `Seed example issue!`, + message: `Seed example ${index + 1} context is duplicate. Please provide unique contexts`, + success: false + }; + setActionGroupAlertContent(actionGroupAlertContent); + return false; + } + // Check that each seed example has at least 3 question and answer pairs for (let index = 0; index < knowledgeFormData.seedExamples.length; index++) { if (knowledgeFormData.seedExamples[index].questionAndAnswers.length < 3) { @@ -83,16 +111,21 @@ export const validateFields = ( } } - // checking for seedExample duplication - if (hasDuplicateSeedExamples(knowledgeFormData.seedExamples)) { - console.log('duplicate seed examples'); - const actionGroupAlertContent: ActionGroupAlertContent = { - title: `Seed example issue!`, - message: `There is duplicated context. Please provide unique contexts`, - success: false - }; - setActionGroupAlertContent(actionGroupAlertContent); - return false; + // Check that each seed example has at least 3 question and answer pairs + for (let index = 0; index < knowledgeFormData.seedExamples.length; index++) { + const { duplicate, index: qnaIndex } = hasDuplicateQuestionAndAnswerPairs(knowledgeFormData.seedExamples[index]); + if (duplicate) { + knowledgeFormData.seedExamples[index].questionAndAnswers[qnaIndex].isQuestionValid = ValidatedOptions.error; + knowledgeFormData.seedExamples[index].questionAndAnswers[qnaIndex].questionValidationError = + 'This is duplicate question, please provide unique questions.'; + const actionGroupAlertContent: ActionGroupAlertContent = { + title: `Seed example ${index + 1} has an issue!`, + message: `Question ${qnaIndex + 1} is a duplicate in the seed example. Please provide unique questions in each seed example.`, + success: false + }; + setActionGroupAlertContent(actionGroupAlertContent); + return false; + } } // checking for question and answer pairs length @@ -132,7 +165,6 @@ export const checkKnowledgeFormCompletion = (knowledgeFormData: KnowledgeFormDat // Helper function to check if a value is non-empty // eslint-disable-next-line @typescript-eslint/no-explicit-any const isNonEmpty = (value: any): boolean => { - console.log(value); if (Array.isArray(value)) { return value.every((item) => isNonEmpty(item)); } diff --git a/src/components/Contribute/Skill/index.tsx b/src/components/Contribute/Skill/index.tsx index e0036a43..4eb959eb 100644 --- a/src/components/Contribute/Skill/index.tsx +++ b/src/components/Contribute/Skill/index.tsx @@ -534,7 +534,7 @@ export const SkillForm: React.FunctionComponent = () => { } actionLinks={ @@ -550,7 +550,7 @@ export const SkillForm: React.FunctionComponent = () => { } > diff --git a/src/components/PathService/PathService.tsx b/src/components/PathService/PathService.tsx index 48fc9b34..0df3a22f 100644 --- a/src/components/PathService/PathService.tsx +++ b/src/components/PathService/PathService.tsx @@ -10,11 +10,12 @@ import { HelperTextItem } from '@patternfly/react-core/dist/dynamic/components/H import ExclamationCircleIcon from '@patternfly/react-icons/dist/dynamic/icons/exclamation-circle-icon'; interface PathServiceProps { + reset?: boolean; rootPath: string; handlePathChange: (value: string) => void; } -const PathService: React.FC = ({ rootPath, handlePathChange }) => { +const PathService: React.FC = ({ reset, rootPath, handlePathChange }) => { const [inputValue, setInputValue] = useState(''); const [items, setItems] = useState([]); const [showDropdown, setShowDropdown] = useState(false); @@ -71,6 +72,11 @@ const PathService: React.FC = ({ rootPath, handlePathChange }) }; }, []); + useEffect(() => { + setInputValue(''); + setShowDropdown(false); + }, [reset]); + useEffect(() => { // check if input value is empty or ends with a slash if (inputValue.endsWith('/')) { From 973c14be709c2175641c0df7974401e21c8a324a Mon Sep 17 00:00:00 2001 From: Anil Vishnoi Date: Fri, 30 Aug 2024 14:07:29 -0700 Subject: [PATCH 2/2] Disable submit/download button if user adds new seed example or Q&A pair Signed-off-by: Anil Vishnoi --- src/components/Contribute/Knowledge/index.tsx | 10 ++++++++++ src/components/PathService/PathService.tsx | 5 ----- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/components/Contribute/Knowledge/index.tsx b/src/components/Contribute/Knowledge/index.tsx index 5c5350e0..ad6ef6b9 100644 --- a/src/components/Contribute/Knowledge/index.tsx +++ b/src/components/Contribute/Knowledge/index.tsx @@ -318,6 +318,7 @@ export const KnowledgeForm: React.FunctionComponent = () => { : seedExample ) ); + setDisableAction(true); }; const deleteQuestionAnswerPair = (seedExampleIndex: number, questionAnswerIndex: number): void => { @@ -331,6 +332,8 @@ export const KnowledgeForm: React.FunctionComponent = () => { : seedExample ) ); + console.log('seedExamples qna', seedExamples); + setDisableAction(!checkKnowledgeFormCompletion(knowledgeFormData)); }; const addSeedExample = (): void => { @@ -338,10 +341,13 @@ export const KnowledgeForm: React.FunctionComponent = () => { seedExample.immutable = false; seedExample.isExpanded = true; setSeedExamples([...seedExamples, seedExample]); + setDisableAction(true); }; const deleteSeedExample = (seedExampleIndex: number): void => { setSeedExamples(seedExamples.filter((_, index: number) => index !== seedExampleIndex)); + console.log('seedExamples', seedExamples); + setDisableAction(!checkKnowledgeFormCompletion(knowledgeFormData)); }; const onCloseActionGroupAlert = () => { @@ -414,6 +420,10 @@ export const KnowledgeForm: React.FunctionComponent = () => { creators: creators }; + useEffect(() => { + setDisableAction(!checkKnowledgeFormCompletion(knowledgeFormData)); + }, [knowledgeFormData]); + return ( <> diff --git a/src/components/PathService/PathService.tsx b/src/components/PathService/PathService.tsx index 0df3a22f..5e757997 100644 --- a/src/components/PathService/PathService.tsx +++ b/src/components/PathService/PathService.tsx @@ -23,7 +23,6 @@ const PathService: React.FC = ({ reset, rootPath, handlePathCh const [validPath, setValidPath] = React.useState(); const validatePath = () => { - console.log('validating path' + inputValue); if (inputValue.length > 0) { setValidPath(ValidatedOptions.success); return; @@ -47,7 +46,6 @@ const PathService: React.FC = ({ reset, rootPath, handlePathCh } const result = await response.json(); - console.log(result); // set items to be displayed in the dropdown if (result.data === null || result.data.length === 0) { setItems([]); @@ -92,7 +90,6 @@ const PathService: React.FC = ({ reset, rootPath, handlePathCh }, [inputValue]); const handleChange = (value: string) => { - console.log('handleChange: ' + value); setInputValue(value); }; @@ -106,13 +103,11 @@ const PathService: React.FC = ({ reset, rootPath, handlePathCh }; const handleSelect = (item: string) => { - console.log('handleSelect: ' + item); setShowDropdown(false); setInputValue(inputValue + item + '/'); }; const handleBlurEvent = () => { - console.log('handleBlurEvent'); setShowDropdown(false); handlePathChange(inputValue); validatePath();