Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Upload an existing yaml file #324

Merged
merged 20 commits into from
Dec 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
4afad5b
adding the file upload component that can be used for Knowledge and S…
aevo98765 Nov 4, 2024
e0ef48a
adding the YamlFileUpload component to the knowledge form and creatin…
aevo98765 Nov 6, 2024
a00e086
adding a function to convert the yaml qnas to SeedExamples[]
aevo98765 Nov 6, 2024
702a602
adding new logic in type checking knowledge or skills yaml upload
aevo98765 Nov 6, 2024
ec86d6f
adding a ternary to account for partially complete yaml
aevo98765 Nov 6, 2024
3a3d69a
accounting for partial qna yaml file
aevo98765 Nov 6, 2024
1b5f864
add YamlFileUpload to the skill form
aevo98765 Nov 6, 2024
406c36b
adding the onYamlUploadSkillsFillForm with logic to convert question …
aevo98765 Nov 6, 2024
6681a52
correcting the YamlFileUpload component isKnowledgeForm
aevo98765 Nov 6, 2024
c478577
removing any types from the type guards in YamlFileUpload
aevo98765 Nov 6, 2024
375ff85
COnverting the YamlFileUpload component to an expandable form section
aevo98765 Nov 6, 2024
537b298
finishing notht the skill and knowledge sections and cleaning up imports
aevo98765 Nov 6, 2024
1701dda
removed a console.log() that was being used for dev work
aevo98765 Nov 27, 2024
97e5b8d
adding and styling buttons for yaml upload
aevo98765 Nov 27, 2024
37c69fe
adding the initla modal and implementing in the skills form
aevo98765 Nov 27, 2024
f707989
first implementation of the upload file modal
aevo98765 Nov 28, 2024
d2d9c83
making the file upload functional
aevo98765 Nov 28, 2024
81fbf13
auto close the modal on successful upload
aevo98765 Nov 28, 2024
ebcdba5
finishing the first implementation. I will creste a new issue for err…
aevo98765 Nov 28, 2024
85de304
removed unused imports
aevo98765 Nov 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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