diff --git a/src/components/Contribute/Knowledge/UploadFile.tsx b/src/components/Contribute/Knowledge/UploadFile.tsx index db5fff8c..096a496f 100644 --- a/src/components/Contribute/Knowledge/UploadFile.tsx +++ b/src/components/Contribute/Knowledge/UploadFile.tsx @@ -111,8 +111,6 @@ export const UploadFile: React.FunctionComponent<{ onFilesChange: (files: File[] }; const successfullyReadFileCount = readFileData.filter((fileData) => fileData.loadResult === 'success').length; - console.log('Successfully read file count:', successfullyReadFileCount); - console.log('Current files count:', currentFiles.length); return ( <> diff --git a/src/components/Contribute/Knowledge/index.tsx b/src/components/Contribute/Knowledge/index.tsx index 73651889..fc472935 100644 --- a/src/components/Contribute/Knowledge/index.tsx +++ b/src/components/Contribute/Knowledge/index.tsx @@ -21,6 +21,7 @@ import { PageGroup } from '@patternfly/react-core/dist/dynamic/components/Page'; import { PageSection } from '@patternfly/react-core/dist/dynamic/components/Page'; import { Content } from '@patternfly/react-core/dist/dynamic/components/Content'; import { Title } from '@patternfly/react-core/dist/dynamic/components/Title'; +import { Flex, FlexItem } from '@patternfly/react-core/dist/dynamic/layouts/Flex'; import KnowledgeDescriptionContent from './KnowledgeDescription/KnowledgeDescriptionContent'; import KnowledgeSeedExample from './KnowledgeSeedExample/KnowledgeSeedExample'; import { checkKnowledgeFormCompletion } from './validation'; @@ -28,11 +29,12 @@ import { ValidatedOptions } from '@patternfly/react-core/dist/esm/helpers/consta import { DownloadDropdown } from './DownloadDropdown/DownloadDropdown'; import { ViewDropdown } from './ViewDropdown/ViewDropdown'; import Update from './Update/Update'; -import { PullRequestFile } from '@/types'; +import { KnowledgeYamlData, PullRequestFile } from '@/types'; import { Button } from '@patternfly/react-core/dist/esm/components/Button/Button'; import { useRouter } from 'next/navigation'; import { autoFillKnowledgeFields } from './AutoFill'; import { Spinner } from '@patternfly/react-core/dist/esm/components/Spinner'; +import { YamlFileUploadModal } from '../YamlFileUploadModal'; export interface QuestionAndAnswerPair { immutable: boolean; @@ -129,6 +131,8 @@ export const KnowledgeForm: React.FunctionComponent = ({ kno const [disableAction, setDisableAction] = useState(true); const [reset, setReset] = useState(false); + const [isModalOpen, setIsModalOpen] = React.useState(false); + const router = useRouter(); const emptySeedExample: SeedExample = { @@ -432,6 +436,32 @@ export const KnowledgeForm: React.FunctionComponent = ({ kno setSeedExamples(autoFillKnowledgeFields.seedExamples); }; + const yamlSeedExampleToFormSeedExample = ( + yamlSeedExamples: { context: string; questions_and_answers: { question: string; answer: string }[] }[] + ) => { + return yamlSeedExamples.map((yamlSeedExample) => ({ + immutable: true, + isExpanded: false, + context: yamlSeedExample.context ?? '', + isContextValid: ValidatedOptions.default, + questionAndAnswers: yamlSeedExample.questions_and_answers.map((questionAndAnswer) => ({ + question: questionAndAnswer.question ?? '', + answer: questionAndAnswer.answer ?? '' + })) + })) as SeedExample[]; + }; + + const onYamlUploadKnowledgeFillForm = (data: KnowledgeYamlData): void => { + setName(data.created_by ?? ''); + setDocumentOutline(data.document_outline ?? ''); + setSubmissionSummary(data.document_outline ?? ''); + setDomain(data.domain ?? ''); + setKnowledgeDocumentRepositoryUrl(data.document.repo ?? ''); + setKnowledgeDocumentCommit(data.document.commit ?? ''); + setDocumentName(data.document.patterns.join(', ') ?? ''); + setSeedExamples(yamlSeedExampleToFormSeedExample(data.seed_examples)); + }; + const knowledgeFormData: KnowledgeFormData = { email: email, name: name, @@ -468,9 +498,19 @@ export const KnowledgeForm: React.FunctionComponent = ({ kno - - Knowledge Contribution - + + + + Knowledge Contribution + + + + + + + @@ -480,6 +520,13 @@ export const KnowledgeForm: React.FunctionComponent = ({ kno )} + +
= ({ skillEditFo const [disableAction, setDisableAction] = useState(true); const [reset, setReset] = useState(false); + const [isModalOpen, setIsModalOpen] = React.useState(false); + const router = useRouter(); const emptySeedExample: SeedExample = { @@ -330,6 +334,23 @@ export const SkillForm: React.FunctionComponent = ({ skillEditFo setSeedExamples(autoFillSkillsFields.seedExamples); }; + const yamlSeedExampleToFormSeedExample = (yamlSeedExamples: { question: string; context?: string | undefined; answer: string }[]) => { + return yamlSeedExamples.map((yamlSeedExample) => ({ + immutable: true, + isExpanded: false, + context: yamlSeedExample.context ?? '', + isContextValid: ValidatedOptions.default, + question: yamlSeedExample.question, + answer: yamlSeedExample.answer + })) as SeedExample[]; + }; + + const onYamlUploadSkillsFillForm = (data: SkillYamlData): void => { + setName(data.created_by ?? ''); + setDocumentOutline(data.task_description ?? ''); + setSeedExamples(yamlSeedExampleToFormSeedExample(data.seed_examples)); + }; + const skillFormData: SkillFormData = { email: email, name: name, @@ -360,9 +381,18 @@ export const SkillForm: React.FunctionComponent = ({ skillEditFo - - Skill Contribution - + + + + Skill Contribution + + + + + + @@ -371,6 +401,14 @@ export const SkillForm: React.FunctionComponent = ({ skillEditFo Auto-Fill )} + + + >; + isKnowledgeForm: boolean; + onYamlUploadKnowledgeFillForm?: (data: KnowledgeYamlData) => void; + onYamlUploadSkillsFillForm?: (data: SkillYamlData) => void; +} + +const YamlFileUpload: React.FC = ({ + setIsModalOpen, + isKnowledgeForm, + onYamlUploadKnowledgeFillForm, + onYamlUploadSkillsFillForm +}) => { + const [currentFiles, setCurrentFiles] = React.useState([]); + const [readFileData, setReadFileData] = React.useState([]); + // Implement a failiure condition in a future PR + // const [fileUploadShouldFail, setFileUploadShouldFail] = React.useState(false); + const fileUploadShouldFail = false; + + const handleFileInputChange = (file: File) => { + if (file) { + readFileContent(file); + } + }; + + const readFileContent = (file: File) => { + const reader = new FileReader(); + + reader.onload = (event) => { + const fileContent = event.target?.result as string; + + try { + const parsedData = yaml.load(fileContent); + if (isKnowledgeForm && isKnowledgeFormData(parsedData)) { + onYamlUploadKnowledgeFillForm?.(parsedData); + setIsModalOpen(false); + } else if (!isKnowledgeForm && isSkillFormData(parsedData)) { + onYamlUploadSkillsFillForm?.(parsedData); + setIsModalOpen(false); + } else { + console.error('This yaml file does not match the Skills or Knowledge schema'); + } + } catch (error) { + console.error('Error parsing YAML file:', error); + } + }; + + reader.onerror = () => { + console.error('Error reading file'); + }; + + reader.readAsText(file); + }; + + // Type guard for KnowledgeFormData + const isKnowledgeFormData = (data: unknown): data is KnowledgeYamlData => { + if (!data) return false; + return data && typeof data === 'object' && 'document' in data && 'document_outline' in data; + }; + + // Type guard for SkillFormData + const isSkillFormData = (data: unknown): data is SkillYamlData => { + if (!data) return false; + return data && typeof data === 'object' && 'task_description' in data; + }; + + // remove files from both state arrays based on their name + const removeFiles = (namesOfFilesToRemove: string[]) => { + const newCurrentFiles = currentFiles.filter((currentFile) => !namesOfFilesToRemove.some((fileName) => fileName === currentFile.name)); + + setCurrentFiles(newCurrentFiles); + + const newReadFiles = readFileData.filter((readFile) => !namesOfFilesToRemove.some((fileName) => fileName === readFile.fileName)); + + setReadFileData(newReadFiles); + }; + + /** Forces uploaded files to become corrupted if "Demonstrate error reporting by forcing uploads to fail" is selected in the example, + * only used in this example for demonstration purposes */ + const updateCurrentFiles = (files: File[]) => { + if (fileUploadShouldFail) { + const corruptedFiles = files.map((file) => ({ ...file, lastModified: 'foo' as unknown as number })); + + setCurrentFiles((prevFiles) => [...prevFiles, ...corruptedFiles]); + } else { + setCurrentFiles((prevFiles) => [...prevFiles, ...files]); + const latestFile = files.at(-1); + if (latestFile) { + handleFileInputChange(latestFile); + } else { + console.error('No latest file found!'); + } + } + }; + + // callback that will be called by the react dropzone with the newly dropped file objects + const handleFileDrop = (_event: DropEvent, droppedFiles: File[]) => { + // identify what, if any, files are re-uploads of already uploaded files + const currentFileNames = currentFiles.map((file) => file.name); + const reUploads = droppedFiles.filter((droppedFile) => currentFileNames.includes(droppedFile.name)); + + /** this promise chain is needed because if the file removal is done at the same time as the file adding react + * won't realize that the status items for the re-uploaded files needs to be re-rendered */ + Promise.resolve() + .then(() => removeFiles(reUploads.map((file) => file.name))) + .then(() => updateCurrentFiles(droppedFiles)); + }; + + // callback called by the status item when a file is successfully read with the built-in file reader + // const handleReadSuccess = (data: string, file: File) => { + // setReadFileData((prevReadFiles) => [...prevReadFiles, { data, fileName: file.name, loadResult: 'success' }]); + // }; + + // // callback called by the status item when a file encounters an error while being read with the built-in file reader + // const handleReadFail = (error: DOMException, file: File) => { + // setReadFileData((prevReadFiles) => [...prevReadFiles, { loadError: error, fileName: file.name, loadResult: 'danger' }]); + // }; + + // add helper text to a status item showing any error encountered during the file reading process + // const createHelperText = (file: File) => { + // const fileResult = readFileData.find((readFile) => readFile.fileName === file.name); + // if (fileResult?.loadError) { + // return ( + // + // {fileResult.loadError.toString()} + // + // ); + // } + // }; + + return ( + <> + + } + titleText="Drag and drop files here" + titleTextSeparator="or" + infoText="Accepted file types: YAML" + /> + + + ); +}; + +export default YamlFileUpload; diff --git a/src/components/Contribute/YamlFileUploadModal.tsx b/src/components/Contribute/YamlFileUploadModal.tsx new file mode 100644 index 00000000..387daa4d --- /dev/null +++ b/src/components/Contribute/YamlFileUploadModal.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import { Modal, ModalBody, ModalFooter, ModalHeader, ModalVariant } from '@patternfly/react-core/dist/dynamic/components/Modal'; +import YamlFileUpload from './YamlFileUpload'; +import { KnowledgeYamlData, SkillYamlData } from '@/types'; + +interface Props { + isModalOpen: boolean; + setIsModalOpen: React.Dispatch>; + isKnowledgeForm: boolean; + onYamlUploadKnowledgeFillForm?: (data: KnowledgeYamlData) => void; + onYamlUploadSkillsFillForm?: (data: SkillYamlData) => void; +} + +export const YamlFileUploadModal: React.FunctionComponent = ({ + isModalOpen, + setIsModalOpen, + isKnowledgeForm, + onYamlUploadKnowledgeFillForm, + onYamlUploadSkillsFillForm +}) => { + const handleModalToggle = () => { + setIsModalOpen(!isModalOpen); + }; + + return ( + + + + + Uploading your YAML will bring in all its data and streamline the contribution process. + + + + + + ); +};