diff --git a/package-lock.json b/package-lock.json index d103d4d3..c1d8ecf4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -404,6 +404,111 @@ "node": ">= 10" } }, + "node_modules/@next/swc-darwin-x64": { + "version": "15.1.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.1.3.tgz", + "integrity": "sha512-aw8901rjkVBK5mbq5oV32IqkJg+CQa6aULNlN8zyCWSsePzEG3kpDkAFkkTOh3eJ0p95KbkLyWBzslQKamXsLA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.1.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.1.3.tgz", + "integrity": "sha512-YbdaYjyHa4fPK4GR4k2XgXV0p8vbU1SZh7vv6El4bl9N+ZSiMfbmqCuCuNU1Z4ebJMumafaz6UCC2zaJCsdzjw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "15.1.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.1.3.tgz", + "integrity": "sha512-qgH/aRj2xcr4BouwKG3XdqNu33SDadqbkqB6KaZZkozar857upxKakbRllpqZgWl/NDeSCBYPmUAZPBHZpbA0w==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "15.1.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.1.3.tgz", + "integrity": "sha512-uzafnTFwZCPN499fNVnS2xFME8WLC9y7PLRs/yqz5lz1X/ySoxfaK2Hbz74zYUdEg+iDZPd8KlsWaw9HKkLEVw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "15.1.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.1.3.tgz", + "integrity": "sha512-el6GUFi4SiDYnMTTlJJFMU+GHvw0UIFnffP1qhurrN1qJV3BqaSRUjkDUgVV44T6zpw1Lc6u+yn0puDKHs+Sbw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.1.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.1.3.tgz", + "integrity": "sha512-6RxKjvnvVMM89giYGI1qye9ODsBQpHSHVo8vqA8xGhmRPZHDQUE4jcDbhBwK0GnFMqBnu+XMg3nYukNkmLOLWw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.1.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.1.3.tgz", + "integrity": "sha512-VId/f5blObG7IodwC5Grf+aYP0O8Saz1/aeU3YcWqNdIUAmFQY3VEPKPaIzfv32F/clvanOb2K2BR5DtDs6XyQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "dev": true, @@ -1445,7 +1550,9 @@ } }, "node_modules/cross-spawn": { - "version": "7.0.3", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "license": "MIT", "dependencies": { diff --git a/src/app/api/native/git/branches/route.ts b/src/app/api/native/git/branches/route.ts index c6e0e68f..2d53b331 100644 --- a/src/app/api/native/git/branches/route.ts +++ b/src/app/api/native/git/branches/route.ts @@ -9,6 +9,12 @@ const LOCAL_TAXONOMY_ROOT_DIR = process.env.NEXT_PUBLIC_LOCAL_TAXONOMY_ROOT_DIR const REMOTE_TAXONOMY_ROOT_DIR = process.env.NEXT_PUBLIC_TAXONOMY_ROOT_DIR || ''; const REMOTE_TAXONOMY_REPO_CONTAINER_MOUNT_DIR = '/tmp/.instructlab-ui'; +interface CommitDetails { + message: string; + email: string; + name: string; +} + interface Diffs { file: string; status: string; @@ -131,6 +137,29 @@ async function handleDiff(branchName: string, localTaxonomyDir: string) { return NextResponse.json({ error: 'Invalid branch name for comparison' }, { status: 400 }); } + // Resolve the reference to the branch's HEAD + const commitOid = await git.resolveRef({ + fs, + dir: localTaxonomyDir, + ref: `refs/heads/${branchName}` // Resolve the branch reference + }); + + // Read the commit object using its OID + const commit = await git.readCommit({ + fs, + dir: localTaxonomyDir, + oid: commitOid + }); + + const signoffMatch = commit.commit.message.split('Signed-off-by:'); + const message = signoffMatch ? signoffMatch[0].trim() : ''; + + const commitDetails: CommitDetails = { + message: message, + email: commit.commit.author.email, + name: commit.commit.author.name + }; + const changes = await findDiff(branchName, localTaxonomyDir); const enrichedChanges: Diffs[] = []; for (const change of changes) { @@ -142,7 +171,7 @@ async function handleDiff(branchName: string, localTaxonomyDir: string) { } } - return NextResponse.json({ changes: enrichedChanges }, { status: 200 }); + return NextResponse.json({ changes: enrichedChanges, commitDetails: commitDetails }, { status: 200 }); } catch (error) { console.error(`Failed to show contribution changes ${branchName}:`, error); return NextResponse.json( diff --git a/src/app/api/native/pr/knowledge/route.ts b/src/app/api/native/pr/knowledge/route.ts index 155e5f4c..af44c5ff 100644 --- a/src/app/api/native/pr/knowledge/route.ts +++ b/src/app/api/native/pr/knowledge/route.ts @@ -18,7 +18,14 @@ export async function POST(req: NextRequest) { const REPO_DIR = path.join(LOCAL_TAXONOMY_ROOT_DIR, '/taxonomy'); try { // Extract the data from the request body - const { content, attribution, name, email, submissionSummary, filePath } = await req.json(); + const { action, branchName, content, attribution, name, email, submissionSummary, filePath, oldFilesPath } = await req.json(); + + let knowledgeBranchName; + if (action == 'update' && branchName != '') { + knowledgeBranchName = branchName; + } else { + knowledgeBranchName = `knowledge-contribution-${Date.now()}`; + } // Parse the YAML string into an object const knowledgeData = yaml.load(content) as KnowledgeYamlData; @@ -27,9 +34,6 @@ export async function POST(req: NextRequest) { const yamlString = dumpYaml(knowledgeData); // Define branch name and file paths - const branchName = `knowledge-contribution-${Date.now()}`; - const newYamlFilePath = path.join(KNOWLEDGE_DIR, filePath, 'qna.yaml'); - const newAttributionFilePath = path.join(KNOWLEDGE_DIR, filePath, 'attribution.txt'); const attributionContent = `Title of work: ${attribution.title_of_work} Link to work: ${attribution.link_to_work} Revision: ${attribution.revision} @@ -37,14 +41,22 @@ License of the work: ${attribution.license_of_the_work} Creator names: ${attribution.creator_names} `; + // Set the flag if commit needs to be amended + let amendCommit = false; + // Initialize the repository if it doesn’t exist await git.init({ fs, dir: REPO_DIR }); - // Create a new branch - await git.branch({ fs, dir: REPO_DIR, ref: branchName }); + // Create a new branch if the knowledge is pushed for first time + if (action != 'update') { + await git.branch({ fs, dir: REPO_DIR, ref: knowledgeBranchName }); + } // Checkout the new branch - await git.checkout({ fs, dir: REPO_DIR, ref: branchName }); + await git.checkout({ fs, dir: REPO_DIR, ref: knowledgeBranchName }); + + const newYamlFilePath = path.join(KNOWLEDGE_DIR, filePath, 'qna.yaml'); + const newAttributionFilePath = path.join(KNOWLEDGE_DIR, filePath, 'attribution.txt'); // Write YAML file to the knowledge directory const yamlFilePath = path.join(REPO_DIR, newYamlFilePath); @@ -59,6 +71,28 @@ Creator names: ${attribution.creator_names} await git.add({ fs, dir: REPO_DIR, filepath: newYamlFilePath }); await git.add({ fs, dir: REPO_DIR, filepath: newAttributionFilePath }); + if (action == 'update') { + // Define file paths + const oldYamlFilePath = path.join(KNOWLEDGE_DIR, oldFilesPath, 'qna.yaml'); + const oldAttributionFilePath = path.join(KNOWLEDGE_DIR, oldFilesPath, 'attribution.txt'); + + if (oldYamlFilePath != newYamlFilePath) { + console.log('File path for the knowledge contribution is updated, removing the old files.'); + // Write the QnA YAML file + const yamlFilePath = path.join(REPO_DIR, oldYamlFilePath); + fs.unlinkSync(yamlFilePath); + + // Write the attribution text file + const attributionFilePath = path.join(REPO_DIR, oldAttributionFilePath); + fs.unlinkSync(attributionFilePath); + + await git.remove({ fs, dir: REPO_DIR, filepath: oldYamlFilePath }); + await git.remove({ fs, dir: REPO_DIR, filepath: oldAttributionFilePath }); + + amendCommit = true; + } + } + // Commit the changes await git.commit({ fs, @@ -67,12 +101,13 @@ Creator names: ${attribution.creator_names} author: { name: name, email: email - } + }, + amend: amendCommit }); // Respond with success message and branch name - console.log(`Knowledge contribution submitted successfully to local taxonomy repo. Submission Name is ${branchName}.`); - return NextResponse.json({ message: 'Knowledge contribution submitted successfully.', branch: branchName }, { status: 201 }); + console.log(`Knowledge contribution submitted successfully to local taxonomy repo. Submission Name is ${knowledgeBranchName}.`); + return NextResponse.json({ message: 'Knowledge contribution submitted successfully.', branch: knowledgeBranchName }, { status: 201 }); } catch (error) { console.error(`Failed to submit knowledge contribution to local taxonomy repo:`, error); return NextResponse.json({ error: 'Failed to submit knowledge contribution.' }, { status: 500 }); diff --git a/src/app/api/native/pr/skill/route.ts b/src/app/api/native/pr/skill/route.ts index cdfaefa0..5be13997 100644 --- a/src/app/api/native/pr/skill/route.ts +++ b/src/app/api/native/pr/skill/route.ts @@ -17,12 +17,14 @@ export async function POST(req: NextRequest) { const REPO_DIR = path.join(LOCAL_TAXONOMY_ROOT_DIR, '/taxonomy'); try { // Extract the QnA data from the request body TODO: what is documentOutline? - const { content, attribution, name, email, submissionSummary, documentOutline, filePath } = await req.json(); // eslint-disable-line @typescript-eslint/no-unused-vars + const { action, branchName, content, attribution, name, email, submissionSummary, documentOutline, filePath, oldFilesPath } = await req.json(); // eslint-disable-line @typescript-eslint/no-unused-vars - // Define file paths - const branchName = `skill-contribution-${Date.now()}`; - const newYamlFilePath = path.join(SKILLS_DIR, filePath, 'qna.yaml'); - const newAttributionFilePath = path.join(SKILLS_DIR, filePath, 'attribution.txt'); + let skillBranchName; + if (action == 'update' && branchName != '') { + skillBranchName = branchName; + } else { + skillBranchName = `skill-contribution-${Date.now()}`; + } const skillData = yaml.load(content) as SkillYamlData; const attributionData = attribution as AttributionData; @@ -34,14 +36,23 @@ License of the work: ${attributionData.license_of_the_work} Creator names: ${attributionData.creator_names} `; + // Set the flag if commit needs to be amended + let amendCommit = false; + // Initialize the repository if it doesn’t exist await git.init({ fs, dir: REPO_DIR }); - // Create a new branch - await git.branch({ fs, dir: REPO_DIR, ref: branchName }); + // Create a new branch if the skill is pushed for first time + if (action != 'update') { + await git.branch({ fs, dir: REPO_DIR, ref: skillBranchName }); + } // Checkout the new branch - await git.checkout({ fs, dir: REPO_DIR, ref: branchName }); + await git.checkout({ fs, dir: REPO_DIR, ref: skillBranchName }); + + // Define file paths + const newYamlFilePath = path.join(SKILLS_DIR, filePath, 'qna.yaml'); + const newAttributionFilePath = path.join(SKILLS_DIR, filePath, 'attribution.txt'); // Write the QnA YAML file const yamlFilePath = path.join(REPO_DIR, newYamlFilePath); @@ -56,6 +67,28 @@ Creator names: ${attributionData.creator_names} await git.add({ fs, dir: REPO_DIR, filepath: newYamlFilePath }); await git.add({ fs, dir: REPO_DIR, filepath: newAttributionFilePath }); + if (action == 'update') { + // Define file paths + const oldYamlFilePath = path.join(SKILLS_DIR, oldFilesPath, 'qna.yaml'); + const oldAttributionFilePath = path.join(SKILLS_DIR, oldFilesPath, 'attribution.txt'); + + if (oldYamlFilePath != newYamlFilePath) { + console.log('File path for the skill contribution is updated, removing the old files.'); + // Write the QnA YAML file + const yamlFilePath = path.join(REPO_DIR, oldYamlFilePath); + fs.unlinkSync(yamlFilePath); + + // Write the attribution text file + const attributionFilePath = path.join(REPO_DIR, oldAttributionFilePath); + fs.unlinkSync(attributionFilePath); + + await git.remove({ fs, dir: REPO_DIR, filepath: oldYamlFilePath }); + await git.remove({ fs, dir: REPO_DIR, filepath: oldAttributionFilePath }); + + amendCommit = true; + } + } + // Commit files await git.commit({ fs, @@ -64,12 +97,13 @@ Creator names: ${attributionData.creator_names} author: { name: name, email: email - } + }, + amend: amendCommit }); // Respond with success - console.log('Skill contribution submitted successfully. Submission name is ', branchName); - return NextResponse.json({ message: 'Skill contribution submitted successfully.', branch: branchName }, { status: 201 }); + console.log('Skill contribution submitted successfully. Submission name is ', skillBranchName); + return NextResponse.json({ message: 'Skill contribution submitted successfully.', branch: skillBranchName }, { status: 201 }); } catch (error) { console.error('Failed to create local branch and commit:', error); return NextResponse.json({ error: 'Failed to submit skill contribution.' }, { status: 500 }); diff --git a/src/app/edit-submission/knowledge/[id]/page.tsx b/src/app/edit-submission/knowledge/github/[id]/page.tsx similarity index 95% rename from src/app/edit-submission/knowledge/[id]/page.tsx rename to src/app/edit-submission/knowledge/github/[id]/page.tsx index 55b25ce4..22fc7639 100644 --- a/src/app/edit-submission/knowledge/[id]/page.tsx +++ b/src/app/edit-submission/knowledge/github/[id]/page.tsx @@ -1,7 +1,7 @@ // src/app/edit-submission/knowledge/[id]/page.tsx import * as React from 'react'; import { AppLayout } from '@/components/AppLayout'; -import EditKnowledge from '@/components/Contribute/EditKnowledge/EditKnowledge'; +import EditKnowledge from '@/components/Contribute/EditKnowledge/github/EditKnowledge'; type PageProps = { params: Promise<{ id: string }>; diff --git a/src/app/edit-submission/knowledge/native/[id]/page.tsx b/src/app/edit-submission/knowledge/native/[id]/page.tsx new file mode 100644 index 00000000..617aa078 --- /dev/null +++ b/src/app/edit-submission/knowledge/native/[id]/page.tsx @@ -0,0 +1,20 @@ +// src/app/edit-submission/knowledge/[id]/page.tsx +import * as React from 'react'; +import { AppLayout } from '@/components/AppLayout'; +import EditKnowledgeNative from '@/components/Contribute/EditKnowledge/native/EditKnowledge'; + +type PageProps = { + params: Promise<{ id: string }>; +}; + +const EditKnowledgePage = async ({ params }: PageProps) => { + const branchName = await params; + + return ( + + + + ); +}; + +export default EditKnowledgePage; diff --git a/src/app/edit-submission/skill/[id]/page.tsx b/src/app/edit-submission/skill/github/[id]/page.tsx similarity index 85% rename from src/app/edit-submission/skill/[id]/page.tsx rename to src/app/edit-submission/skill/github/[id]/page.tsx index 735436fa..aca0bbd5 100644 --- a/src/app/edit-submission/skill/[id]/page.tsx +++ b/src/app/edit-submission/skill/github/[id]/page.tsx @@ -1,7 +1,7 @@ // src/app/edit-submission/skill/[id]/page.tsx import * as React from 'react'; import { AppLayout } from '@/components/AppLayout'; -import EditSkill from '@/components/Contribute/EditSkill/EditSkill'; +import EditSkill from '@/components/Contribute/EditSkill/github/EditSkill'; type PageProps = { params: Promise<{ id: string }>; diff --git a/src/app/edit-submission/skill/native/[id]/page.tsx b/src/app/edit-submission/skill/native/[id]/page.tsx new file mode 100644 index 00000000..efb2ec13 --- /dev/null +++ b/src/app/edit-submission/skill/native/[id]/page.tsx @@ -0,0 +1,20 @@ +// src/app/edit-submission/skill/[id]/page.tsx +import * as React from 'react'; +import { AppLayout } from '@/components/AppLayout'; +import EditSkillNative from '@/components/Contribute/EditSkill/native/EditSkill'; + +type PageProps = { + params: Promise<{ id: string }>; +}; + +const EditSkillPage = async ({ params }: PageProps) => { + const branchName = await params; + + return ( + + + + ); +}; + +export default EditSkillPage; diff --git a/src/components/Contribute/EditKnowledge/EditKnowledge.tsx b/src/components/Contribute/EditKnowledge/github/EditKnowledge.tsx similarity index 91% rename from src/components/Contribute/EditKnowledge/EditKnowledge.tsx rename to src/components/Contribute/EditKnowledge/github/EditKnowledge.tsx index 394e4984..605e11b0 100644 --- a/src/components/Contribute/EditKnowledge/EditKnowledge.tsx +++ b/src/components/Contribute/EditKnowledge/github/EditKnowledge.tsx @@ -11,8 +11,8 @@ import axios from 'axios'; import { KnowledgeEditFormData, KnowledgeFormData, QuestionAndAnswerPair, KnowledgeSeedExample } from '@/types'; import { useEffect, useState } from 'react'; import { useRouter } from 'next/navigation'; -import KnowledgeFormGithub from '../Knowledge/Github'; -import { ValidatedOptions, Modal, ModalVariant } from '@patternfly/react-core'; +import KnowledgeFormGithub from '../../Knowledge/Github'; +import { ValidatedOptions, Modal, ModalVariant, ModalBody } from '@patternfly/react-core'; interface EditKnowledgeClientComponentProps { prNumber: number; @@ -57,8 +57,7 @@ const EditKnowledge: React.FC = ({ prNumber } branchName: '', knowledgeFormData: knowledgeExistingFormData, pullRequestNumber: prNumber, - yamlFile: { filename: '' }, - attributionFile: { filename: '' } + oldFilesPath: '' }; knowledgeExistingFormData.submissionSummary = prData.title; @@ -70,11 +69,14 @@ const EditKnowledge: React.FC = ({ prNumber } if (!foundYamlFile) { throw new Error('No YAML file found in the pull request.'); } - knowledgeEditFormData.yamlFile = foundYamlFile; + const existingFilesPath = foundYamlFile.filename.split('/').slice(1, -1).join('/'); + + // Set the current Yaml file path as a old files path + knowledgeEditFormData.oldFilesPath = existingFilesPath + '/'; const yamlContent = await fetchFileContent(session.accessToken, foundYamlFile.filename, prData.head.sha); const yamlData: KnowledgeYamlData = yaml.load(yamlContent) as KnowledgeYamlData; - console.log('Parsed YAML data:', yamlData); + console.log('Parsed Knowledge YAML data:', yamlData); // Populate the form fields with YAML data knowledgeExistingFormData.documentOutline = yamlData.document_outline; @@ -118,9 +120,8 @@ const EditKnowledge: React.FC = ({ prNumber } if (foundAttributionFile) { const attributionContent = await fetchFileContent(session.accessToken, foundAttributionFile.filename, prData.head.sha); const attributionData = parseAttributionContent(attributionContent); - console.log('Parsed attribution data:', attributionData); + console.log('Parsed knowledge attribution data:', attributionData); - knowledgeEditFormData.attributionFile = foundAttributionFile; // Populate the form fields with attribution data knowledgeExistingFormData.titleWork = attributionData.title_of_work; knowledgeExistingFormData.linkWork = attributionData.link_to_work ? attributionData.link_to_work : ''; @@ -165,19 +166,15 @@ const EditKnowledge: React.FC = ({ prNumber } if (isLoading) { return ( - // handleOnClose()}> - {loadingMsg} + + {loadingMsg} + - // ); } - return ( - // - - // - ); + return ; }; export default EditKnowledge; diff --git a/src/components/Contribute/EditKnowledge/native/EditKnowledge.tsx b/src/components/Contribute/EditKnowledge/native/EditKnowledge.tsx new file mode 100644 index 00000000..3dd00a34 --- /dev/null +++ b/src/components/Contribute/EditKnowledge/native/EditKnowledge.tsx @@ -0,0 +1,186 @@ +// src/app/edit-submission/knowledge/native/[id]/EditKnowledge.tsx +'use client'; + +import * as React from 'react'; +import { useSession } from 'next-auth/react'; +import { AttributionData, KnowledgeYamlData } from '@/types'; +import { KnowledgeSchemaVersion } from '@/types/const'; +import yaml from 'js-yaml'; +import { KnowledgeEditFormData, KnowledgeFormData, QuestionAndAnswerPair, KnowledgeSeedExample } from '@/types'; +import { useEffect, useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { ValidatedOptions, Modal, ModalVariant, ModalBody } from '@patternfly/react-core'; +import KnowledgeFormNative from '../../Knowledge/Native'; + +interface ChangeData { + file: string; + status: string; + content?: string; + commitSha?: string; +} + +interface EditKnowledgeClientComponentProps { + branchName: string; +} + +const EditKnowledgeNative: React.FC = ({ branchName }) => { + const { data: session } = useSession(); + const [isLoading, setIsLoading] = useState(true); + const [loadingMsg, setLoadingMsg] = useState(''); + const [knowledgeEditFormData, setKnowledgeEditFormData] = useState(); + const router = useRouter(); + + useEffect(() => { + setLoadingMsg('Fetching knowledge data from branch : ' + branchName); + const fetchBranchChanges = async () => { + try { + const response = await fetch('/api/native/git/branches', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ branchName, action: 'diff' }) + }); + + const result = await response.json(); + if (response.ok) { + // Create KnowledgeFormData from existing form. + const knowledgeExistingFormData: KnowledgeFormData = { + email: '', + name: '', + submissionSummary: '', + domain: '', + documentOutline: '', + filePath: '', + seedExamples: [], + knowledgeDocumentRepositoryUrl: '', + knowledgeDocumentCommit: '', + documentName: '', + titleWork: '', + linkWork: '', + revision: '', + licenseWork: '', + creators: '' + }; + + const knowledgeEditFormData: KnowledgeEditFormData = { + isEditForm: true, + knowledgeVersion: KnowledgeSchemaVersion, + branchName: branchName, + knowledgeFormData: knowledgeExistingFormData, + pullRequestNumber: 0, + oldFilesPath: '' + }; + + if (session?.user?.name && session?.user?.email) { + knowledgeExistingFormData.name = session?.user?.name; + knowledgeExistingFormData.email = session?.user?.email; + } + + if (result?.commitDetails != null) { + knowledgeExistingFormData.submissionSummary = result?.commitDetails.message; + knowledgeExistingFormData.name = result?.commitDetails.name; + knowledgeExistingFormData.email = result?.commitDetails.email; + } + + if (result?.changes.length > 0) { + result.changes.forEach((change: ChangeData) => { + if (change.status != 'deleted' && change.content) { + if (change.file.includes('qna.yaml')) { + const yamlData: KnowledgeYamlData = yaml.load(change.content) as KnowledgeYamlData; + console.log('Parsed Knowledge YAML data:', yamlData); + // Populate the form fields with YAML data + knowledgeExistingFormData.documentOutline = yamlData.document_outline; + knowledgeExistingFormData.domain = yamlData.domain; + knowledgeExistingFormData.knowledgeDocumentRepositoryUrl = yamlData.document.repo; + knowledgeExistingFormData.knowledgeDocumentCommit = yamlData.document.commit; + knowledgeExistingFormData.documentName = yamlData.document.patterns.join(', '); + + const seedExamples: KnowledgeSeedExample[] = []; + yamlData.seed_examples.forEach((seed, index) => { + // iterate through questions_and_answers and create a new object for each + const example: KnowledgeSeedExample = { + immutable: index < 5 ? true : false, + isExpanded: true, + context: seed.context, + isContextValid: ValidatedOptions.success, + questionAndAnswers: [] + }; + + const qnaExamples: QuestionAndAnswerPair[] = seed.questions_and_answers.map((qa, index) => { + const qna: QuestionAndAnswerPair = { + question: qa.question, + answer: qa.answer, + immutable: index < 3 ? true : false, + isQuestionValid: ValidatedOptions.success, + isAnswerValid: ValidatedOptions.success + }; + return qna; + }); + example.questionAndAnswers = qnaExamples; + seedExamples.push(example); + }); + + knowledgeExistingFormData.seedExamples = seedExamples; + // Set the file path from the current YAML file (remove the root folder name from the path) + const currentFilePath = change.file.split('/').slice(1, -1).join('/'); + knowledgeExistingFormData.filePath = currentFilePath + '/'; + + // Set the oldFilesPath to the existing qna.yaml file path. + knowledgeEditFormData.oldFilesPath = knowledgeExistingFormData.filePath; + } + if (change.file.includes('attribution.txt')) { + const attributionData = parseAttributionContent(change.content); + console.log('Parsed knowledge attribution data:', attributionData); + + // Populate the form fields with attribution data + knowledgeExistingFormData.titleWork = attributionData.title_of_work; + knowledgeExistingFormData.linkWork = attributionData.link_to_work ? attributionData.link_to_work : ''; + knowledgeExistingFormData.revision = attributionData.revision ? attributionData.revision : ''; + knowledgeExistingFormData.licenseWork = attributionData.license_of_the_work; + knowledgeExistingFormData.creators = attributionData.creator_names; + } + } + }); + setKnowledgeEditFormData(knowledgeEditFormData); + setIsLoading(false); + } + } + } catch (error) { + console.error('Error fetching branch changes:', error); + } + }; + fetchBranchChanges(); + }, [branchName]); + + const parseAttributionContent = (content: string): AttributionData => { + const lines = content.split('\n'); + const attributionData: { [key: string]: string } = {}; + lines.forEach((line) => { + const [key, ...value] = line.split(':'); + if (key && value) { + // Remove spaces in the attribution field for parsing + const normalizedKey = key.trim().toLowerCase().replace(/\s+/g, '_'); + attributionData[normalizedKey] = value.join(':').trim(); + } + }); + return attributionData as unknown as AttributionData; + }; + + const handleOnClose = () => { + router.push('/dashboard'); + setIsLoading(false); + }; + + if (isLoading) { + return ( + handleOnClose()}> + + {loadingMsg} + + + ); + } + + return ; +}; + +export default EditKnowledgeNative; diff --git a/src/components/Contribute/EditSkill/EditSkill.tsx b/src/components/Contribute/EditSkill/github/EditSkill.tsx similarity index 96% rename from src/components/Contribute/EditSkill/EditSkill.tsx rename to src/components/Contribute/EditSkill/github/EditSkill.tsx index 09f9d2b5..4ae30174 100644 --- a/src/components/Contribute/EditSkill/EditSkill.tsx +++ b/src/components/Contribute/EditSkill/github/EditSkill.tsx @@ -49,8 +49,7 @@ const EditSkill: React.FC = ({ prNumber }) => { branchName: '', skillFormData: skillExistingFormData, pullRequestNumber: prNumber, - yamlFile: { filename: '' }, - attributionFile: { filename: '' } + oldFilesPath: '' }; skillExistingFormData.submissionSummary = prData.title; @@ -62,7 +61,11 @@ const EditSkill: React.FC = ({ prNumber }) => { if (!foundYamlFile) { throw new Error('No YAML file found in the pull request.'); } - skillEditFormData.yamlFile = foundYamlFile; + + const existingFilesPath = foundYamlFile.filename.split('/').slice(1, -1).join('/'); + + // Set the current Yaml file path as a old files path + skillEditFormData.oldFilesPath = existingFilesPath + '/'; const yamlContent = await fetchFileContent(session.accessToken, foundYamlFile.filename, prData.head.sha); const yamlData: SkillYamlData = yaml.load(yamlContent) as SkillYamlData; @@ -98,7 +101,6 @@ const EditSkill: React.FC = ({ prNumber }) => { const attributionData = parseAttributionContent(attributionContent); console.log('Parsed attribution data:', attributionData); - skillEditFormData.attributionFile = foundAttributionFile; // Populate the form fields with attribution data skillExistingFormData.titleWork = attributionData.title_of_work; skillExistingFormData.licenseWork = attributionData.license_of_the_work; diff --git a/src/components/Contribute/EditSkill/native/EditSkill.tsx b/src/components/Contribute/EditSkill/native/EditSkill.tsx new file mode 100644 index 00000000..65b85633 --- /dev/null +++ b/src/components/Contribute/EditSkill/native/EditSkill.tsx @@ -0,0 +1,160 @@ +// src/app/edit-submission/skill/native/[id]/EditSkill.tsx +'use client'; + +import * as React from 'react'; +import { useEffect, useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { SkillEditFormData } from '@/components/Contribute/Skill/Native'; +import yaml from 'js-yaml'; +import { SkillYamlData, AttributionData, SkillFormData, SkillSeedExample } from '@/types'; +import { SkillSchemaVersion } from '@/types/const'; +import { ValidatedOptions, Modal, ModalVariant, ModalBody } from '@patternfly/react-core'; +import SkillFormNative from '../../Skill/Native'; +import { useSession } from 'next-auth/react'; + +interface ChangeData { + file: string; + status: string; + content?: string; + commitSha?: string; +} + +interface EditSkillClientComponentProps { + branchName: string; +} + +const EditSkillNative: React.FC = ({ branchName }) => { + const { data: session } = useSession(); + const [isLoading, setIsLoading] = useState(true); + const [loadingMsg, setLoadingMsg] = useState(''); + const [skillEditFormData, setSkillEditFormData] = useState(); + const router = useRouter(); + + useEffect(() => { + const fetchBranchChanges = async () => { + setLoadingMsg('Fetching skill data from branch: ' + branchName); + try { + const response = await fetch('/api/native/git/branches', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ branchName, action: 'diff' }) + }); + + const result = await response.json(); + if (response.ok) { + const skillExistingFormData: SkillFormData = { + email: '', + name: '', + submissionSummary: '', + documentOutline: '', + filePath: '', + seedExamples: [], + titleWork: '', + licenseWork: '', + creators: '' + }; + + const skillEditFormData: SkillEditFormData = { + isEditForm: true, + skillVersion: SkillSchemaVersion, + branchName: branchName, + skillFormData: skillExistingFormData, + oldFilesPath: '' + }; + + if (session?.user?.name && session?.user?.email) { + skillExistingFormData.name = session?.user?.name; + skillExistingFormData.email = session?.user?.email; + } + + if (result?.commitDetails != null) { + skillExistingFormData.submissionSummary = result?.commitDetails.message; + skillExistingFormData.name = result?.commitDetails.name; + skillExistingFormData.email = result?.commitDetails.email; + } + + if (result?.changes.length > 0) { + result.changes.forEach((change: ChangeData) => { + if (change.status != 'deleted' && change.content) { + if (change.file.includes('qna.yaml')) { + const yamlData: SkillYamlData = yaml.load(change.content) as SkillYamlData; + console.log('Parsed skill YAML data:', yamlData); + skillExistingFormData.documentOutline = yamlData.task_description; + const seedExamples: SkillSeedExample[] = []; + yamlData.seed_examples.forEach((seed, index) => { + const example: SkillSeedExample = { + immutable: index < 5 ? true : false, + isExpanded: true, + context: seed.context || '', + isContextValid: ValidatedOptions.success, + question: seed.question, + isQuestionValid: ValidatedOptions.success, + answer: seed.answer, + isAnswerValid: ValidatedOptions.success + }; + seedExamples.push(example); + }); + skillExistingFormData.seedExamples = seedExamples; + + //Extract filePath from the existing qna.yaml file path + const currentFilePath = change.file.split('/').slice(1, -1).join('/'); + skillEditFormData.skillFormData.filePath = currentFilePath + '/'; + + // Set the oldFilesPath to the existing qna.yaml file path. + skillEditFormData.oldFilesPath = skillEditFormData.skillFormData.filePath; + } + if (change.file.includes('attribution.txt')) { + const attributionData = parseAttributionContent(change.content); + console.log('Parsed skill attribution data:', attributionData); + // Populate the form fields with attribution data + skillExistingFormData.titleWork = attributionData.title_of_work; + skillExistingFormData.licenseWork = attributionData.license_of_the_work; + skillExistingFormData.creators = attributionData.creator_names; + } + } + }); + } + setSkillEditFormData(skillEditFormData); + } else { + console.error('Failed to get branch changes:', result.error); + } + setIsLoading(false); + } catch (error) { + console.error('Error fetching branch changes:', error); + } + }; + fetchBranchChanges(); + }, [branchName]); + + const parseAttributionContent = (content: string): AttributionData => { + const lines = content.split('\n'); + const attributionData: { [key: string]: string } = {}; + lines.forEach((line) => { + const [key, ...value] = line.split(':'); + if (key && value) { + const normalizedKey = key.trim().toLowerCase().replace(/\s+/g, '_'); + attributionData[normalizedKey] = value.join(':').trim(); + } + }); + return attributionData as unknown as AttributionData; + }; + + const handleOnClose = () => { + router.push('/dashboard'); + setIsLoading(false); + }; + + if (isLoading) { + return ( + + + {loadingMsg} + + + ); + } + + return ; +}; + +export default EditSkillNative; diff --git a/src/components/Contribute/Knowledge/Github/Update/Update.tsx b/src/components/Contribute/Knowledge/Github/Update/Update.tsx index 3c36ee25..a9010974 100644 --- a/src/components/Contribute/Knowledge/Github/Update/Update.tsx +++ b/src/components/Contribute/Knowledge/Github/Update/Update.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { ActionGroupAlertContent } from '..'; -import { AttributionData, KnowledgeFormData, KnowledgeYamlData, PullRequestFile } from '@/types'; +import { AttributionData, KnowledgeFormData, KnowledgeYamlData } from '@/types'; import { KnowledgeSchemaVersion } from '@/types/const'; import { dumpYaml } from '@/utils/yamlConfig'; import { validateFields } from '../../validation'; @@ -14,21 +14,12 @@ interface Props { disableAction: boolean; knowledgeFormData: KnowledgeFormData; pullRequestNumber: number; - yamlFile: PullRequestFile; - attributionFile: PullRequestFile; + oldFilesPath: string; branchName: string; setActionGroupAlertContent: React.Dispatch>; } -const Update: React.FC = ({ - disableAction, - knowledgeFormData, - pullRequestNumber, - yamlFile, - attributionFile, - branchName, - setActionGroupAlertContent -}) => { +const Update: React.FC = ({ disableAction, knowledgeFormData, pullRequestNumber, oldFilesPath, branchName, setActionGroupAlertContent }) => { const { data: session } = useSession(); const router = useRouter(); @@ -73,7 +64,7 @@ const Update: React.FC = ({ }; const yamlString = dumpYaml(knowledgeYamlData); - console.log('Updated YAML content:', yamlString); + console.log('Updated knowledge YAML content:', yamlString); const attributionData: AttributionData = { title_of_work: knowledgeFormData.titleWork!, @@ -89,20 +80,17 @@ License of the work: ${attributionData.license_of_the_work} Creator names: ${attributionData.creator_names} `; - console.log('Updated Attribution content:', attributionData); + console.log('Updated knowledge attribution content:', attributionData); const commitMessage = `Amend commit with updated content\n\nSigned-off-by: ${knowledgeFormData.name} <${knowledgeFormData.email}>`; // Ensure proper file paths for the edit - const finalYamlPath = KNOWLEDGE_DIR + knowledgeFormData.filePath.replace(/^\//, '').replace(/\/?$/, '/') + yamlFile.filename.split('/').pop(); - const finalAttributionPath = - KNOWLEDGE_DIR + knowledgeFormData.filePath.replace(/^\//, '').replace(/\/?$/, '/') + attributionFile.filename.split('/').pop(); - console.log('finalYamlPath:', finalYamlPath); + const finalYamlPath = KNOWLEDGE_DIR + knowledgeFormData.filePath.replace(/^\//, '').replace(/\/?$/, '/') + 'qna.yaml'; + const finalAttributionPath = KNOWLEDGE_DIR + knowledgeFormData.filePath.replace(/^\//, '').replace(/\/?$/, '/') + 'attribution.txt'; - const origFilePath = yamlFile.filename.split('/').slice(0, -1).join('/'); const oldFilePath = { - yaml: origFilePath.replace(/^\//, '').replace(/\/?$/, '/') + yamlFile.filename.split('/').pop(), - attribution: origFilePath.replace(/^\//, '').replace(/\/?$/, '/') + attributionFile.filename.split('/').pop() + yaml: oldFilesPath.replace(/^\//, '').replace(/\/?$/, '/') + 'qna.yaml', + attribution: oldFilesPath.replace(/^\//, '').replace(/\/?$/, '/') + 'qna.yaml' }; const newFilePath = { diff --git a/src/components/Contribute/Knowledge/Github/index.tsx b/src/components/Contribute/Knowledge/Github/index.tsx index b42f5179..6851eb8a 100644 --- a/src/components/Contribute/Knowledge/Github/index.tsx +++ b/src/components/Contribute/Knowledge/Github/index.tsx @@ -503,22 +503,6 @@ export const KnowledgeFormGithub: React.FunctionComponent = /> ) }, - { - id: 'seed-examples', - name: 'Seed Examples', - component: ( - - ) - }, { id: 'document-info', name: 'Document Information', @@ -537,6 +521,22 @@ export const KnowledgeFormGithub: React.FunctionComponent = /> ) }, + { + id: 'seed-examples', + name: 'Seed Examples', + component: ( + + ) + }, { id: 'attribution-info', name: 'Attribution Information', @@ -648,8 +648,7 @@ export const KnowledgeFormGithub: React.FunctionComponent = knowledgeFormData={knowledgeFormData} pullRequestNumber={knowledgeEditFormData.pullRequestNumber} setActionGroupAlertContent={setActionGroupAlertContent} - yamlFile={knowledgeEditFormData.yamlFile} - attributionFile={knowledgeEditFormData.attributionFile} + oldFilesPath={knowledgeEditFormData.oldFilesPath} branchName={knowledgeEditFormData.branchName} /> )} diff --git a/src/components/Contribute/Knowledge/Native/Submit/Submit.tsx b/src/components/Contribute/Knowledge/Native/Submit/Submit.tsx index 2e840218..54c6e81b 100644 --- a/src/components/Contribute/Knowledge/Native/Submit/Submit.tsx +++ b/src/components/Contribute/Knowledge/Native/Submit/Submit.tsx @@ -71,13 +71,16 @@ const Submit: React.FC = ({ disableAction, knowledgeFormData, setActionGr 'Content-Type': 'application/json' }, body: JSON.stringify({ + action: 'submit', + branch: '', content: yamlString, attribution: attributionData, name, email, submissionSummary, documentOutline, - filePath: sanitizedFilePath + filePath: sanitizedFilePath, + oldFilesPath: sanitizedFilePath }) }); diff --git a/src/components/Contribute/Knowledge/Native/Update/Update.tsx b/src/components/Contribute/Knowledge/Native/Update/Update.tsx new file mode 100644 index 00000000..42e4bedc --- /dev/null +++ b/src/components/Contribute/Knowledge/Native/Update/Update.tsx @@ -0,0 +1,118 @@ +// src/components/contribute/Knowledge/Native/Update/Update.tsx +import React from 'react'; +import { ActionGroupAlertContent } from '..'; +import { AttributionData, KnowledgeFormData, KnowledgeYamlData } from '@/types'; +import { KnowledgeSchemaVersion } from '@/types/const'; +import { dumpYaml } from '@/utils/yamlConfig'; +import { validateFields } from '@/components/Contribute/Knowledge/validation'; +import { Button } from '@patternfly/react-core'; +import { useRouter } from 'next/navigation'; + +interface Props { + disableAction: boolean; + knowledgeFormData: KnowledgeFormData; + oldFilesPath: string; + branchName: string; + email: string; + setActionGroupAlertContent: React.Dispatch>; +} + +const Update: React.FC = ({ disableAction, knowledgeFormData, oldFilesPath, branchName, email, setActionGroupAlertContent }) => { + const router = useRouter(); + + const handleUpdate = async (event: React.FormEvent) => { + event.preventDefault(); + if (!validateFields(knowledgeFormData, setActionGroupAlertContent)) return; + + // Strip leading slash and ensure trailing slash in the file path + let sanitizedFilePath = knowledgeFormData.filePath!.startsWith('/') ? knowledgeFormData.filePath!.slice(1) : knowledgeFormData.filePath; + sanitizedFilePath = sanitizedFilePath!.endsWith('/') ? sanitizedFilePath : `${sanitizedFilePath}/`; + + const knowledgeYamlData: KnowledgeYamlData = { + created_by: email, + version: KnowledgeSchemaVersion, + domain: knowledgeFormData.domain!, + document_outline: knowledgeFormData.documentOutline!, + seed_examples: knowledgeFormData.seedExamples.map((example) => ({ + context: example.context, + questions_and_answers: example.questionAndAnswers.map((questionAndAnswer) => ({ + question: questionAndAnswer.question, + answer: questionAndAnswer.answer + })) + })), + document: { + repo: knowledgeFormData.knowledgeDocumentRepositoryUrl!, + commit: knowledgeFormData.knowledgeDocumentCommit!, + patterns: knowledgeFormData.documentName!.split(',').map((pattern) => pattern.trim()) + } + }; + + const yamlString = dumpYaml(knowledgeYamlData); + + const attributionData: AttributionData = { + title_of_work: knowledgeFormData.titleWork!, + link_to_work: knowledgeFormData.linkWork!, + revision: knowledgeFormData.revision!, + license_of_the_work: knowledgeFormData.licenseWork!, + creator_names: knowledgeFormData.creators! + }; + + const waitForSubmissionAlert: ActionGroupAlertContent = { + title: 'Knowledge contribution submission in progress!', + message: `Once the submission is successful, it will provide the link to the newly created Pull Request.`, + success: true, + waitAlert: true, + timeout: false + }; + setActionGroupAlertContent(waitForSubmissionAlert); + + const name = knowledgeFormData.name; + const submissionSummary = knowledgeFormData.submissionSummary; + const documentOutline = knowledgeFormData.documentOutline; + const response = await fetch('/api/native/pr/knowledge', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + action: 'update', + branchName: branchName, + content: yamlString, + attribution: attributionData, + name, + email, + submissionSummary, + documentOutline, + filePath: sanitizedFilePath, + oldFilesPath: oldFilesPath + }) + }); + + if (!response.ok) { + const actionGroupAlertContent: ActionGroupAlertContent = { + title: `Failed data submission`, + message: response.statusText, + success: false + }; + setActionGroupAlertContent(actionGroupAlertContent); + return; + } + + await response.json(); + const actionGroupAlertContent: ActionGroupAlertContent = { + title: 'Knowledge contribution updated successfully!', + message: `Thank you for your contribution!`, + url: '/dashboard/', + success: true + }; + setActionGroupAlertContent(actionGroupAlertContent); + router.push('/dashboard'); + }; + return ( + + Update + + ); +}; + +export default Update; diff --git a/src/components/Contribute/Knowledge/Native/index.tsx b/src/components/Contribute/Knowledge/Native/index.tsx index da44bf10..d2983dd2 100644 --- a/src/components/Contribute/Knowledge/Native/index.tsx +++ b/src/components/Contribute/Knowledge/Native/index.tsx @@ -1,8 +1,7 @@ // src/components/Contribute/Native/Knowledge/index.tsx 'use client'; -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import '../knowledge.css'; -import { getGitHubUsername } from '@/utils/github'; import { useSession } from 'next-auth/react'; import AuthorInformation from '@/components/Contribute/AuthorInformation'; import { FormType } from '@/components/Contribute/AuthorInformation'; @@ -16,7 +15,7 @@ import KnowledgeSeedExampleNative from '@/components/Contribute/Knowledge/Native import { checkKnowledgeFormCompletion } from '@/components/Contribute/Knowledge/validation'; import { DownloadDropdown } from '@/components/Contribute/Knowledge/DownloadDropdown/DownloadDropdown'; import { ViewDropdown } from '@/components/Contribute/Knowledge/ViewDropdown/ViewDropdown'; -import Update from '@/components/Contribute/Knowledge/Github/Update/Update'; +import Update from '@/components/Contribute/Knowledge/Native/Update/Update'; import { KnowledgeEditFormData, KnowledgeFormData, KnowledgeSeedExample, KnowledgeYamlData, QuestionAndAnswerPair } from '@/types'; import { useRouter } from 'next/navigation'; import { autoFillKnowledgeFields } from '@/components/Contribute/Knowledge/AutoFill'; @@ -61,7 +60,6 @@ export const KnowledgeFormNative: React.FunctionComponent = const [devModeEnabled, setDevModeEnabled] = useState(); const { data: session } = useSession(); - const [githubUsername, setGithubUsername] = useState(''); // Author Information const [email, setEmail] = useState(''); const [name, setName] = useState(''); @@ -95,7 +93,7 @@ export const KnowledgeFormNative: React.FunctionComponent = const router = useRouter(); - const [activeStepIndex] = useState(1); + const [activeStepIndex, setActiveStepIndex] = useState(1); // Function to create a unique empty seed example const createEmptySeedExample = (): KnowledgeSeedExample => ({ @@ -160,28 +158,6 @@ export const KnowledgeFormNative: React.FunctionComponent = } }, [session?.user]); - useMemo(() => { - const fetchUsername = async () => { - if (session?.accessToken) { - try { - const headers = { - 'Content-Type': 'application/json', - Authorization: `Bearer ${session.accessToken}`, - Accept: 'application/vnd.github+json', - 'X-GitHub-Api-Version': '2022-11-28' - }; - - const fetchedUsername = await getGitHubUsername(headers); - setGithubUsername(fetchedUsername); - } catch (error) { - console.error('Failed to fetch GitHub username:', error); - } - } - }; - - fetchUsername(); - }, [session?.accessToken]); - useEffect(() => { // Set all elements from the knowledgeFormData to the state if (knowledgeEditFormData) { @@ -463,6 +439,8 @@ export const KnowledgeFormNative: React.FunctionComponent = // setReset is just reset button, value has no impact. setReset((prev) => !prev); + + setActiveStepIndex(1); devLog('Knowledge Form Reset.'); }; @@ -762,11 +740,10 @@ export const KnowledgeFormNative: React.FunctionComponent = )} {!knowledgeEditFormData?.isEditForm && ( @@ -778,8 +755,8 @@ export const KnowledgeFormNative: React.FunctionComponent = resetForm={resetForm} /> )} - - + + Cancel diff --git a/src/components/Contribute/Knowledge/validation.tsx b/src/components/Contribute/Knowledge/validation.tsx index eba684f1..acf0a796 100644 --- a/src/components/Contribute/Knowledge/validation.tsx +++ b/src/components/Contribute/Knowledge/validation.tsx @@ -161,6 +161,8 @@ export const validateFields = ( return true; }; +const optionalKeys = ['context', 'isContextValid', 'validationError', 'questionValidationError', 'answerValidationError']; + export const checkKnowledgeFormCompletion = (knowledgeFormData: object): boolean => { // Helper function to check if a value is non-empty // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -178,6 +180,10 @@ export const checkKnowledgeFormCompletion = (knowledgeFormData: object): boolean // eslint-disable-next-line @typescript-eslint/no-explicit-any const checkObject = (obj: Record): boolean => { return Object.keys(obj).every((key) => { + // Skip validation for optional keys + if (optionalKeys.includes(key)) { + return true; + } const value = obj[key]; if (typeof value === 'object' && !Array.isArray(value)) { return checkObject(value); // Recursively check nested objects diff --git a/src/components/Contribute/Skill/Github/Update/Update.tsx b/src/components/Contribute/Skill/Github/Update/Update.tsx index 16b68c84..811da915 100644 --- a/src/components/Contribute/Skill/Github/Update/Update.tsx +++ b/src/components/Contribute/Skill/Github/Update/Update.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { ActionGroupAlertContent } from '..'; -import { AttributionData, SkillYamlData, PullRequestFile, SkillFormData } from '@/types'; +import { AttributionData, SkillYamlData, SkillFormData } from '@/types'; import { SkillSchemaVersion } from '@/types/const'; import { dumpYaml } from '@/utils/yamlConfig'; import { validateFields } from '../../validation'; @@ -14,21 +14,12 @@ interface Props { disableAction: boolean; skillFormData: SkillFormData; pullRequestNumber: number; - yamlFile: PullRequestFile; - attributionFile: PullRequestFile; + oldFilesPath: string; branchName: string; setActionGroupAlertContent: React.Dispatch>; } -const Update: React.FC = ({ - disableAction, - skillFormData, - pullRequestNumber, - yamlFile, - attributionFile, - branchName, - setActionGroupAlertContent -}) => { +const Update: React.FC = ({ disableAction, skillFormData, pullRequestNumber, oldFilesPath, branchName, setActionGroupAlertContent }) => { const { data: session } = useSession(); const router = useRouter(); @@ -83,14 +74,12 @@ Creator names: ${attributionData.creator_names} const commitMessage = `Amend commit with updated content\n\nSigned-off-by: ${skillFormData.name} <${skillFormData.email}>`; // Ensure proper file paths for the edit - const finalYamlPath = SKILLS_DIR + skillFormData.filePath.replace(/^\//, '').replace(/\/?$/, '/') + yamlFile.filename.split('/').pop(); - const finalAttributionPath = - SKILLS_DIR + skillFormData.filePath.replace(/^\//, '').replace(/\/?$/, '/') + attributionFile.filename.split('/').pop(); + const finalYamlPath = SKILLS_DIR + skillFormData.filePath.replace(/^\//, '').replace(/\/?$/, '/') + 'qna.yaml'; + const finalAttributionPath = SKILLS_DIR + skillFormData.filePath.replace(/^\//, '').replace(/\/?$/, '/') + 'attribution.txt'; - const origFilePath = yamlFile.filename.split('/').slice(0, -1).join('/'); const oldFilePath = { - yaml: origFilePath.replace(/^\//, '').replace(/\/?$/, '/') + yamlFile.filename.split('/').pop(), - attribution: origFilePath.replace(/^\//, '').replace(/\/?$/, '/') + attributionFile.filename.split('/').pop() + yaml: oldFilesPath.replace(/^\//, '').replace(/\/?$/, '/') + 'qna.yaml', + attribution: oldFilesPath.replace(/^\//, '').replace(/\/?$/, '/') + 'attribution.txt' }; const newFilePath = { diff --git a/src/components/Contribute/Skill/Github/index.tsx b/src/components/Contribute/Skill/Github/index.tsx index d220ec22..aa347123 100644 --- a/src/components/Contribute/Skill/Github/index.tsx +++ b/src/components/Contribute/Skill/Github/index.tsx @@ -11,7 +11,7 @@ import { checkSkillFormCompletion } from '../validation'; import { DownloadDropdown } from '../DownloadDropdown/DownloadDropdown'; import { ViewDropdown } from '../ViewDropdown/ViewDropdown'; import Update from './Update/Update'; -import { SkillYamlData, PullRequestFile, SkillFormData, SkillSeedExample } from '@/types'; +import { SkillYamlData, SkillFormData, SkillSeedExample } from '@/types'; import { useRouter } from 'next/navigation'; import SkillsSeedExample from '../SkillsSeedExample/SkillsSeedExample'; import SkillsInformation from '../SkillsInformation/SkillsInformation'; @@ -46,8 +46,7 @@ export interface SkillEditFormData { skillVersion: number; pullRequestNumber: number; branchName: string; - yamlFile: PullRequestFile; - attributionFile: PullRequestFile; + oldFilesPath: string; skillFormData: SkillFormData; } @@ -518,8 +517,7 @@ export const SkillFormGithub: React.FunctionComponent = ({ skill skillFormData={skillFormData} pullRequestNumber={skillEditFormData.pullRequestNumber} setActionGroupAlertContent={setActionGroupAlertContent} - yamlFile={skillEditFormData.yamlFile} - attributionFile={skillEditFormData.attributionFile} + oldFilesPath={skillEditFormData.oldFilesPath} branchName={skillEditFormData.branchName} /> )} diff --git a/src/components/Contribute/Skill/Native/Submit/Submit.tsx b/src/components/Contribute/Skill/Native/Submit/Submit.tsx index 4012fadd..c6f053de 100644 --- a/src/components/Contribute/Skill/Native/Submit/Submit.tsx +++ b/src/components/Contribute/Skill/Native/Submit/Submit.tsx @@ -63,13 +63,16 @@ const Submit: React.FC = ({ disableAction, skillFormData, setActionGroupA 'Content-Type': 'application/json' }, body: JSON.stringify({ + action: 'submit', + branchName: '', content: yamlString, attribution: attributionData, name, email, submissionSummary, documentOutline, - filePath: sanitizedFilePath + filePath: sanitizedFilePath, + oldFilesPath: sanitizedFilePath }) }); diff --git a/src/components/Contribute/Skill/Native/Update/Update.tsx b/src/components/Contribute/Skill/Native/Update/Update.tsx new file mode 100644 index 00000000..3f0d54d5 --- /dev/null +++ b/src/components/Contribute/Skill/Native/Update/Update.tsx @@ -0,0 +1,109 @@ +import React from 'react'; +import { ActionGroupAlertContent } from '..'; +import { AttributionData, SkillFormData, SkillYamlData } from '@/types'; +import { SkillSchemaVersion } from '@/types/const'; +import { dumpYaml } from '@/utils/yamlConfig'; +import { validateFields } from '@/components/Contribute/Skill/validation'; +import { Button } from '@patternfly/react-core'; +import { useRouter } from 'next/navigation'; + +interface Props { + disableAction: boolean; + skillFormData: SkillFormData; + oldFilesPath: string; + branchName: string; + email: string; + setActionGroupAlertContent: React.Dispatch>; +} + +const Update: React.FC = ({ disableAction, skillFormData, oldFilesPath, branchName, email, setActionGroupAlertContent }) => { + const router = useRouter(); + + const handleUpdate = async (event: React.FormEvent) => { + event.preventDefault(); + if (!validateFields(skillFormData, setActionGroupAlertContent)) return; + + // Strip leading slash and ensure trailing slash in the file path + let sanitizedFilePath = skillFormData.filePath!.startsWith('/') ? skillFormData.filePath!.slice(1) : skillFormData.filePath; + sanitizedFilePath = sanitizedFilePath!.endsWith('/') ? sanitizedFilePath : `${sanitizedFilePath}/`; + + const skillYamlData: SkillYamlData = { + created_by: email, + version: SkillSchemaVersion, + task_description: skillFormData.documentOutline!, + seed_examples: skillFormData.seedExamples.map((example) => ({ + context: example.context, + question: example.question, + answer: example.answer + })) + }; + + const yamlString = dumpYaml(skillYamlData); + + const attributionData: AttributionData = { + title_of_work: skillFormData.titleWork!, + license_of_the_work: skillFormData.licenseWork!, + creator_names: skillFormData.creators!, + link_to_work: '', + revision: '' + }; + + const waitForSubmissionAlert: ActionGroupAlertContent = { + title: 'Skill contribution submission in progress!', + message: `Once the submission is successful, it will provide the link to the newly created Pull Request.`, + success: true, + waitAlert: true, + timeout: false + }; + setActionGroupAlertContent(waitForSubmissionAlert); + + const name = skillFormData.name; + const submissionSummary = skillFormData.submissionSummary; + const documentOutline = skillFormData.documentOutline; + const response = await fetch('/api/native/pr/skill/', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + action: 'update', + branchName: branchName, + content: yamlString, + attribution: attributionData, + name, + email, + submissionSummary, + documentOutline, + filePath: sanitizedFilePath, + oldFilesPath: oldFilesPath + }) + }); + + if (!response.ok) { + const actionGroupAlertContent: ActionGroupAlertContent = { + title: `Failed data submission`, + message: response.statusText, + success: false + }; + setActionGroupAlertContent(actionGroupAlertContent); + return; + } + + await response.json(); + const actionGroupAlertContent: ActionGroupAlertContent = { + title: 'Skill contribution updated successfully!', + message: `Thank you for your contribution!`, + url: '/dashboard', + success: true + }; + setActionGroupAlertContent(actionGroupAlertContent); + router.push('/dashboard'); + }; + return ( + + Update + + ); +}; + +export default Update; diff --git a/src/components/Contribute/Skill/Native/index.tsx b/src/components/Contribute/Skill/Native/index.tsx index 2d2be787..303b5538 100644 --- a/src/components/Contribute/Skill/Native/index.tsx +++ b/src/components/Contribute/Skill/Native/index.tsx @@ -11,8 +11,8 @@ import Submit from '@/components/Contribute/Skill/Native/Submit/Submit'; import { checkSkillFormCompletion } from '@/components/Contribute/Skill/validation'; import { DownloadDropdown } from '@/components/Contribute/Skill/DownloadDropdown/DownloadDropdown'; import { ViewDropdown } from '@/components/Contribute/Skill/ViewDropdown/ViewDropdown'; -import Update from '@/components/Contribute/Skill/Github/Update/Update'; -import { PullRequestFile, SkillSeedExample, SkillFormData, SkillYamlData } from '@/types'; +import Update from '@/components/Contribute/Skill/Native/Update/Update'; +import { SkillSeedExample, SkillFormData, SkillYamlData } from '@/types'; import { useRouter } from 'next/navigation'; import SkillsSeedExample from '@/components/Contribute/Skill/SkillsSeedExample/SkillsSeedExample'; import SkillsInformation from '@/components/Contribute/Skill/SkillsInformation/SkillsInformation'; @@ -44,10 +44,8 @@ import ReviewSubmission from '../ReviewSubmission'; export interface SkillEditFormData { isEditForm: boolean; skillVersion: number; - pullRequestNumber: number; branchName: string; - yamlFile: PullRequestFile; - attributionFile: PullRequestFile; + oldFilesPath: string; skillFormData: SkillFormData; } @@ -68,7 +66,6 @@ export const SkillFormNative: React.FunctionComponent = ({ skill const [devModeEnabled, setDevModeEnabled] = useState(); const { data: session } = useSession(); - const [githubUsername] = useState(''); // Author Information const [email, setEmail] = useState(''); const [name, setName] = useState(''); @@ -90,7 +87,7 @@ export const SkillFormNative: React.FunctionComponent = ({ skill const [disableAction, setDisableAction] = useState(true); const [reset, setReset] = useState(false); const [isModalOpen, setIsModalOpen] = React.useState(false); - const [activeStepIndex] = useState(1); + const [activeStepIndex, setActiveStepIndex] = useState(1); const router = useRouter(); const emptySeedExample: SkillSeedExample = { @@ -283,6 +280,7 @@ export const SkillFormNative: React.FunctionComponent = ({ skill // setReset is just reset button, value has no impact. setReset(reset ? false : true); + setActiveStepIndex(1); }; const autoFillForm = (): void => { @@ -493,10 +491,9 @@ export const SkillFormNative: React.FunctionComponent = ({ skill )} @@ -509,8 +506,8 @@ export const SkillFormNative: React.FunctionComponent = ({ skill resetForm={resetForm} /> )} - - + + Cancel diff --git a/src/components/Dashboard/Github/dashboard.tsx b/src/components/Dashboard/Github/dashboard.tsx index 9cb0b3d7..4464fe3f 100644 --- a/src/components/Dashboard/Github/dashboard.tsx +++ b/src/components/Dashboard/Github/dashboard.tsx @@ -82,9 +82,9 @@ const DashboardGithub: React.FunctionComponent = () => { const hasSkillLabel = pr.labels.some((label) => label.name === 'skill'); if (hasKnowledgeLabel) { - router.push(`/edit-submission/knowledge/${pr.number}`); + router.push(`/edit-submission/knowledge/github/${pr.number}`); } else if (hasSkillLabel) { - router.push(`/edit-submission/skill/${pr.number}`); + router.push(`/edit-submission/skill/github/${pr.number}`); } }; diff --git a/src/components/Dashboard/Native/dashboard.tsx b/src/components/Dashboard/Native/dashboard.tsx index 28a29a25..bd4d40c0 100644 --- a/src/components/Dashboard/Native/dashboard.tsx +++ b/src/components/Dashboard/Native/dashboard.tsx @@ -74,7 +74,6 @@ const DashboardNative: React.FunctionComponent = () => { const [isPublishModalOpen, setIsPublishModalOpen] = React.useState(false); const [selectedBranch, setSelectedBranch] = React.useState(null); const [isPublishing, setIsPublishing] = React.useState(false); - const [isEditModalOpen, setIsEditModalOpen] = React.useState(false); const [expandedFiles, setExpandedFiles] = React.useState>({}); const router = useRouter(); @@ -229,11 +228,13 @@ const DashboardNative: React.FunctionComponent = () => { const handleEditContribution = (branchName: string) => { setSelectedBranch(branchName); - setIsEditModalOpen(true); - }; - const closeEditModal = () => { - setIsEditModalOpen(false); + // Check if branchName contains string "knowledge" + if (branchName.includes('knowledge')) { + router.push(`/edit-submission/knowledge/native/${branchName}`); + } else { + router.push(`/edit-submission/skill/native/${branchName}`); + } }; const handlePublishContribution = async (branchName: string) => { @@ -456,26 +457,6 @@ const DashboardNative: React.FunctionComponent = () => { No differences found. )} - - - - - Not yet implemented for native mode. - - - - Close - - - -
No differences found.
Not yet implemented for native mode.