Skip to content

Commit

Permalink
Merge pull request #324 from aevo98765/issue/#241/upload-yaml-file
Browse files Browse the repository at this point in the history
Upload an existing yaml file
  • Loading branch information
vishnoianil authored Dec 3, 2024
2 parents d7724cb + 85de304 commit 0c188d4
Show file tree
Hide file tree
Showing 5 changed files with 310 additions and 10 deletions.
2 changes: 0 additions & 2 deletions src/components/Contribute/Knowledge/UploadFile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<>
Expand Down
55 changes: 51 additions & 4 deletions src/components/Contribute/Knowledge/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,20 @@ 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';
import { ValidatedOptions } from '@patternfly/react-core/dist/esm/helpers/constants';
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;
Expand Down Expand Up @@ -129,6 +131,8 @@ export const KnowledgeForm: React.FunctionComponent<KnowledgeFormProps> = ({ kno
const [disableAction, setDisableAction] = useState<boolean>(true);
const [reset, setReset] = useState<boolean>(false);

const [isModalOpen, setIsModalOpen] = React.useState(false);

const router = useRouter();

const emptySeedExample: SeedExample = {
Expand Down Expand Up @@ -432,6 +436,32 @@ export const KnowledgeForm: React.FunctionComponent<KnowledgeFormProps> = ({ 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,
Expand Down Expand Up @@ -468,9 +498,19 @@ export const KnowledgeForm: React.FunctionComponent<KnowledgeFormProps> = ({ kno
</PageBreadcrumb>

<PageSection hasBodyWrapper={false} style={{ backgroundColor: 'white' }}>
<Title headingLevel="h1" size="2xl" style={{ paddingTop: '10' }}>
Knowledge Contribution
</Title>
<Flex justifyContent={{ default: 'justifyContentSpaceBetween' }}>
<FlexItem>
<Title headingLevel="h1" size="2xl" style={{ paddingTop: '10px' }}>
Knowledge Contribution
</Title>
</FlexItem>
<FlexItem>
<Button variant="secondary" aria-label="User upload of pre-existing yaml file" onClick={() => setIsModalOpen(true)}>
Upload a YAML file
</Button>
</FlexItem>
</Flex>

<Content>
<KnowledgeDescriptionContent />
</Content>
Expand All @@ -480,6 +520,13 @@ export const KnowledgeForm: React.FunctionComponent<KnowledgeFormProps> = ({ kno
</Button>
)}

<YamlFileUploadModal
isModalOpen={isModalOpen}
setIsModalOpen={setIsModalOpen}
isKnowledgeForm={true}
onYamlUploadKnowledgeFillForm={onYamlUploadKnowledgeFillForm}
/>

<Form className="form-k">
<AuthorInformation
formType={FormType.Knowledge}
Expand Down
46 changes: 42 additions & 4 deletions src/components/Contribute/Skill/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,21 @@ 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 { checkSkillFormCompletion } from './validation';
import { ValidatedOptions } from '@patternfly/react-core/dist/esm/helpers/constants';
import { DownloadDropdown } from './DownloadDropdown/DownloadDropdown';
import { ViewDropdown } from './ViewDropdown/ViewDropdown';
import Update from './Update/Update';
import { PullRequestFile } from '@/types';
import { SkillYamlData, PullRequestFile } from '@/types';
import { Button } from '@patternfly/react-core/dist/esm/components/Button/Button';
import { useRouter } from 'next/navigation';
import SkillsSeedExample from './SkillsSeedExample/SkillsSeedExample';
import SkillsInformation from './SkillsInformation/SkillsInformation';
import SkillsDescriptionContent from './SkillsDescription/SkillsDescriptionContent';
import { autoFillSkillsFields } from './AutoFill';
import { Spinner } from '@patternfly/react-core/dist/dynamic/components/Spinner';
import { YamlFileUploadModal } from '../YamlFileUploadModal';

export interface SeedExample {
immutable: boolean;
Expand Down Expand Up @@ -109,6 +111,8 @@ export const SkillForm: React.FunctionComponent<SkillFormProps> = ({ skillEditFo
const [disableAction, setDisableAction] = useState<boolean>(true);
const [reset, setReset] = useState<boolean>(false);

const [isModalOpen, setIsModalOpen] = React.useState(false);

const router = useRouter();

const emptySeedExample: SeedExample = {
Expand Down Expand Up @@ -330,6 +334,23 @@ export const SkillForm: React.FunctionComponent<SkillFormProps> = ({ 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,
Expand Down Expand Up @@ -360,9 +381,18 @@ export const SkillForm: React.FunctionComponent<SkillFormProps> = ({ skillEditFo
</PageBreadcrumb>

<PageSection hasBodyWrapper={false} style={{ backgroundColor: 'white' }}>
<Title headingLevel="h1" size="2xl" style={{ paddingTop: '10' }}>
Skill Contribution
</Title>
<Flex justifyContent={{ default: 'justifyContentSpaceBetween' }}>
<FlexItem>
<Title headingLevel="h1" size="2xl" style={{ paddingTop: '10px' }}>
Skill Contribution
</Title>
</FlexItem>
<FlexItem>
<Button variant="secondary" aria-label="User upload of pre-existing yaml file" onClick={() => setIsModalOpen(true)}>
Upload a YAML file
</Button>
</FlexItem>
</Flex>
<Content>
<SkillsDescriptionContent />
</Content>
Expand All @@ -371,6 +401,14 @@ export const SkillForm: React.FunctionComponent<SkillFormProps> = ({ skillEditFo
Auto-Fill
</Button>
)}

<YamlFileUploadModal
isModalOpen={isModalOpen}
setIsModalOpen={setIsModalOpen}
isKnowledgeForm={false}
onYamlUploadSkillsFillForm={onYamlUploadSkillsFillForm}
/>

<Form className="form-s">
<AuthorInformation
formType={FormType.Knowledge}
Expand Down
168 changes: 168 additions & 0 deletions src/components/Contribute/YamlFileUpload.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import React from 'react';
import yaml from 'js-yaml';
import { KnowledgeYamlData, SkillYamlData } from '@/types';
import { MultipleFileUpload } from '@patternfly/react-core/dist/esm/components/MultipleFileUpload/MultipleFileUpload';
import { MultipleFileUploadMain } from '@patternfly/react-core/dist/esm/components/MultipleFileUpload/MultipleFileUploadMain';
import { DropEvent } from '@patternfly/react-core/dist/esm/helpers/typeUtils';
import { UploadIcon } from '@patternfly/react-icons/dist/esm/icons/upload-icon';

interface readFile {
fileName: string;
data?: string;
loadResult?: 'danger' | 'success';
loadError?: DOMException;
}

interface YamlFileUploadProps {
setIsModalOpen: React.Dispatch<React.SetStateAction<boolean>>;
isKnowledgeForm: boolean;
onYamlUploadKnowledgeFillForm?: (data: KnowledgeYamlData) => void;
onYamlUploadSkillsFillForm?: (data: SkillYamlData) => void;
}

const YamlFileUpload: React.FC<YamlFileUploadProps> = ({
setIsModalOpen,
isKnowledgeForm,
onYamlUploadKnowledgeFillForm,
onYamlUploadSkillsFillForm
}) => {
const [currentFiles, setCurrentFiles] = React.useState<File[]>([]);
const [readFileData, setReadFileData] = React.useState<readFile[]>([]);
// 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 (
// <HelperText isLiveRegion>
// <HelperTextItem variant="error">{fileResult.loadError.toString()}</HelperTextItem>
// </HelperText>
// );
// }
// };

return (
<>
<MultipleFileUpload
onFileDrop={handleFileDrop}
dropzoneProps={{
accept: {
'application/x-yaml': ['.yaml', '.yml'],
'text/yaml': ['.yaml', '.yml']
}
}}
>
<MultipleFileUploadMain
titleIcon={<UploadIcon />}
titleText="Drag and drop files here"
titleTextSeparator="or"
infoText="Accepted file types: YAML"
/>
</MultipleFileUpload>
</>
);
};

export default YamlFileUpload;
Loading

0 comments on commit 0c188d4

Please sign in to comment.