Home Reference Source

src/js/githubOauth.js

import Molecule from './molecules/molecule.js'
import GlobalVariables from './globalvariables.js'
import { licenses } from './licenseOptions.js'
import { extractBomTags, convertLinks } from './BOM.js'
import { OAuth } from 'oauthio-web'

/**
 * This function works like a class to sandbox interaction with GitHub.
 */
export default function GitHubModule(){
    const { Octokit } = require("@octokit/rest")
    /** 
     * The octokit instance which allows authenticated interaction with GitHub.
     * @type {object}
     */
    var octokit = new Octokit()
    /** 
     * The HTML element which is the popup.
     * @type {object}
     */
    var popup = document.getElementById('projects-popup')
    /** 
     * The name of the current repo.
     * @type {string}
     */
    var currentRepoName = null
    /** 
     * The name of the currently logged in user.
     * @type {string}
     */
    var currentUser = null
    /** 
     * The text to display at the top of the bill of materials.
     * @type {string}
     */
    var bomHeader = "###### Note: Do not edit this file directly, it is automatically generated from the CAD model \n# Bill Of Materials \n |Part|Number Needed|Price|Source| \n |----|----------|-----|-----|"
     
    /** 
     * The text to display at the top of the ReadMe file.
     * @type {string}
     */
    var readmeHeader = "###### Note: Do not edit this file directly, it is automatically generated from the CAD model"

    /** 
     * The timer used to trigger saving of the file.
     * @type {object}
     */
    var intervalTimer

    /** 
     * The timer used to trigger saving of the file.
     * @type {object}
     */
    var page = 1

    //Github pop up event listeners
    document.getElementById("loginButton").addEventListener("mousedown", () => {
        this.tryLogin()
    })
    
    /** 
     * Try to login using the oauth popup.
     */
    this.tryLogin = function(){
        
        // Initialize with OAuth.io app public key
        if(window.location.href.includes('private')){
            OAuth.initialize('6CQQE8MMCBFjdWEjevnTBMCQpsw') //app public key for repo scope
        }
        else{
            OAuth.initialize('BYP9iFpD7aTV9SDhnalvhZ4fwD8') //app public key for public_repo scope
        }
        // Use popup for oauth
        OAuth.popup('github').then(github => {
        
            /** 
             * Oktokit object to access github
             * @type {object}
             */
            octokit = new Octokit({
                auth: github.access_token
            })
            
            //Test the authentication 
            octokit.users.getAuthenticated({}).then(result => {
                currentUser = result.data.login
                this.showProjectsToLoad()
            })
        })
    }
    
    /** 
     * Display projects which can be loaded in the popup.
     */
    this.showProjectsToLoad = function(){
        //Remove everything in the popup now
        while (popup.firstChild) {
            popup.removeChild(popup.firstChild)
        }

        //Close button (Mac style)
        if(GlobalVariables.topLevelMolecule && GlobalVariables.topLevelMolecule.name != "Maslow Create"){ //Only offer a close button if there is a project to go back to
            var closeButton = document.createElement("button")
            closeButton.setAttribute("class", "closeButton")
            closeButton.addEventListener("click", () => {
                popup.classList.add('off')
            })
            popup.appendChild(closeButton)
        }
        //Welcome title
        var welcome = document.createElement("div")
        welcome.setAttribute("style", " display: flex; margin: 10px; align-items: center;")
        popup.appendChild(welcome)

        var welcome1 = document.createElement("IMG")
        welcome1.setAttribute("src", "/imgs/maslow-logo.png" )
        welcome1.setAttribute("style", " height:25px; border-radius:50%;")
        welcome.appendChild(welcome1)
        var welcome2 = document.createElement("IMG")
        welcome2.setAttribute("src", "/imgs/maslowcreate.svg" )
        welcome2.setAttribute("style", "height:20px; padding: 10px;")
        welcome.appendChild(welcome2)
        var middleBrowseDiv = document.createElement("div")
        if (currentUser == null){

            var githubSign = document.createElement("button")
            githubSign.setAttribute("id", "loginButton2" )
            githubSign.setAttribute("class", "form browseButton githubSign")
            githubSign.setAttribute("style", "width: 90px; font-size: .7rem; margin-left: auto;")
            githubSign.textContent = "Login"
            welcome.appendChild(githubSign)   

            var githubSignUp = document.createElement("button")
            githubSignUp.setAttribute("class", "form browseButton githubSign")
            githubSignUp.setAttribute("onclick", "window.open('https://github.com/join')")
            githubSignUp.setAttribute("style", "width: 130px; font-size: .7rem;margin-left: 5px;")
            githubSignUp.textContent = "Create an account"
            welcome.appendChild(githubSignUp)  

            //Welcome title
            var welcome3 = document.createElement("div")
            welcome3.innerHTML = "Maslow Create User Projects"
            welcome3.setAttribute("style", "justify-content: flex-start; display: inline; width: 100%; font-size: 18px;")
            popup.appendChild(welcome3)

            middleBrowseDiv.setAttribute("style","margin-top:25px")

            githubSign.addEventListener("mousedown", () => {
                this.tryLogin()
            })
        }
        
        popup.classList.remove('off')
        popup.setAttribute("style", "padding: 0;text-align: center; background-color: #f9f6f6; border: 10px solid #3e3d3d;")
       
        var tabButtons = document.createElement("DIV")
        tabButtons.setAttribute("class", "tab")
        popup.appendChild(tabButtons)
     
        middleBrowseDiv.setAttribute("class", "middleBrowse")
        popup.appendChild(middleBrowseDiv)

        var searchIcon = document.createElement("IMG")
        searchIcon.setAttribute("src", '/imgs/search_icon.svg')
        searchIcon.setAttribute("style", "width: 20px; float: right; color: white; position: relative;right: 3px; opacity: 0.5;")
        middleBrowseDiv.appendChild(searchIcon)

        var searchBar = document.createElement("input")
        searchBar.setAttribute("type", "text")
        searchBar.setAttribute("contenteditable", "true")
        searchBar.setAttribute("placeholder", "Search for project..")
        searchBar.setAttribute("class", "menu_search")
        searchBar.setAttribute("id", "project_search")
        middleBrowseDiv.appendChild(searchBar)

        //Display option buttons
        var browseDisplay1 = document.createElement("div")
        browseDisplay1.setAttribute("class", "browseDisplay")
        var listPicture = document.createElement("IMG")
        listPicture.setAttribute("src", '/imgs/list-with-dots.svg') //https://www.freeiconspng.com/img/1454
        listPicture.setAttribute("style", "height: 75%;padding: 3px;")
        browseDisplay1.appendChild(listPicture)
        middleBrowseDiv.appendChild(browseDisplay1)
        var browseDisplay2 = document.createElement("div")
        browseDisplay2.setAttribute("class", "browseDisplay active_filter")
        browseDisplay2.setAttribute("id", "thumb")
        var listPicture2 = document.createElement("IMG")
        listPicture2.setAttribute("src", '/imgs/thumb_icon.png') 
        listPicture2.setAttribute("style", "height: 80%;padding: 3px;")
        browseDisplay2.appendChild(listPicture2)
        middleBrowseDiv.appendChild(browseDisplay2)

        //Input to search for projects

        searchBar.addEventListener('keydown', (e) => {
            
            this.loadProjectsBySearch("yoursButton",e, searchBar.value, "updated")
            this.loadProjectsBySearch("githubButton",e, searchBar.value, "stars") // updated just sorts content by most recently updated
        })
        

        this.projectsSpaceDiv = document.createElement("DIV")
        this.projectsSpaceDiv.setAttribute("class", "float-left-div")
        this.projectsSpaceDiv.setAttribute("style", "overflow-x: hidden; margin-top: 10px;")
        popup.appendChild(this.projectsSpaceDiv)
        
        const pageChange = document.createElement("div")
        const pageBack = document.createElement("button")
        pageBack.setAttribute("id", "back")
        pageBack.setAttribute("class", "page_change")
        pageBack.innerHTML = "‹"

        const pageForward = document.createElement("button")
        pageChange.appendChild(pageBack)
        pageChange.appendChild(pageForward)
        pageForward.setAttribute("id", "forward")
        pageForward.setAttribute("class", "page_change")
        pageForward.innerHTML = "›"

        popup.appendChild(pageChange)

        
        this.openTab(page)

        //Event listeners 

        browseDisplay1.addEventListener("click", () => {
            // titlesDiv.style.display = "flex"
            browseDisplay2.classList.remove("active_filter")
            this.openTab(page)
        })
        browseDisplay2.addEventListener("click", () => {
            // titlesDiv.style.display = "none"
            browseDisplay2.classList.add("active_filter")
            this.openTab(page)
        })
        pageForward.addEventListener("click", () => {
            if (page >=1){ page +=1 }
            this.openTab(page)
        })
        pageBack.addEventListener("click", () => {
            if (page >1){page -=1}
            this.openTab(page)
        })

    }

    /** 
     * Search for the name of a project and then return results which match that search.
     */
    this.loadProjectsBySearch = async function(tabName, ev, searchString, sorting, pageNumber, clear = true){
        
        if(ev.key == "Enter"){
            //Remove projects shown now
            if(clear){
                while (this.projectsSpaceDiv.firstChild) {
                    this.projectsSpaceDiv.removeChild(this.projectsSpaceDiv.firstChild)
                }
            }
            // add initial projects to div

            //New project div
            if (currentUser !== null && clear){
                var browseDiv = document.createElement("div")
                browseDiv.setAttribute("class", "browseDiv")
                this.projectsSpaceDiv.appendChild(browseDiv)
            
                var createNewProject = document.createElement("div")
                createNewProject.setAttribute("class", "newProject")

                browseDiv.appendChild(createNewProject)
                this.NewProject("New Project", null, true, "")
            }
            //header for project list style display
            var titlesDiv = document.createElement("div")
            titlesDiv.setAttribute("id","titlesDiv")
            var titles = document.createElement("div")
            titles.innerHTML = ""
            titles.setAttribute("class","browseColumn")
            titlesDiv.appendChild(titles)
            var titles2 = document.createElement("div")
            titles2.innerHTML = "Project"
            titles2.setAttribute("class","browseColumn")
            titlesDiv.appendChild(titles2)
            var titles3 = document.createElement("div")
            titles3.innerHTML = "Creator"
            titles3.setAttribute("class","browseColumn")
            titlesDiv.appendChild(titles3)
            var titles4 = document.createElement("div")
            titles4.innerHTML = "Created on"
            titles4.setAttribute("class","browseColumn")
            titlesDiv.appendChild(titles4)
            var titles5 = document.createElement("div")
            titles5.innerHTML = "Last Modified"
            titles5.setAttribute("class","browseColumn")
            titlesDiv.appendChild(titles5)

            if (!document.getElementById("thumb").classList.contains("active_filter")){
                titlesDiv.style.display = "flex"
                titlesDiv.style.marginTop = "10px"
                browseDiv.style.width = "100%"
                createNewProject.style.height = "80px"
            }
        
            this.projectsSpaceDiv.appendChild(titlesDiv)
              
            //Load projects
            var query
            var owned

            var sortMethod = sorting  //drop down input. temporarily inactive until we figure some better way to sort
            if(tabName == "yoursButton"){
                owned = true
                query = searchString + ' ' + 'fork:true user:' + currentUser + ' topic:maslowcreate'
            }
            else{
                owned = false
                query = searchString + ' topic:maslowcreate -user:' + currentUser
            }
            
            //Figure out how many repos this user has, search will throw an error if they have 0;
            // octokit.repos.list({
            // affiliation: 'owner',
            // })
            
            return octokit.search.repos({
                q: query,
                sort: sortMethod,
                per_page: 50,
                page: pageNumber,
                headers: {
                    accept: 'application/vnd.github.mercy-preview+json'
                }
            }).then(result => {
                result.data.items.forEach(repo => {
                    const thumbnailPath = "https://raw.githubusercontent.com/"+repo.full_name+"/master/project.svg?sanitize=true"
                    
                    this.addProject(repo.name, repo.id, repo.owner.login, repo.created_at, repo.updated_at, owned, thumbnailPath)
                })
                
            }) 
        } 
    }
    
    /** 
     * Adds a new project to the load projects display.
     */
    this.NewProject = function(projectName, id, owned, thumbnailPath){
        //create a project element to display
        
        var project = document.createElement("DIV")
        project.classList.add("newProjectdiv")
        
        var projectPicture = document.createElement("IMG")
        projectPicture.setAttribute("src", thumbnailPath)
        projectPicture.setAttribute("onerror", "this.src='/defaultThumbnail.svg'")
        projectPicture.setAttribute("style", "height: 80%; float: left;")
        project.appendChild(projectPicture)
        
        var projectText = document.createElement("span")
        projectText.innerHTML = "Start a new project"
        projectText.setAttribute("style","align-self: center")
        project.appendChild(projectText)

        document.querySelector(".newProject").appendChild(project) 
        
        project.addEventListener('click', () => {
            this.projectClicked(projectName, id, owned)
        })

    }
    
    /** 
     * Adds a new project to the load projects display.
     */
    this.addProject = function(projectName, id, owner, createdAt, updatedAt, owned, thumbnailPath){
        
        this.projectsSpaceDiv.classList.remove("float-left-div-thumb")
        var project = document.createElement("DIV")
        var projectPicture = document.createElement("IMG")
        projectPicture.setAttribute("src", thumbnailPath)
        projectPicture.setAttribute("onerror", "this.src='/defaultThumbnail.svg'")
        project.appendChild(projectPicture)
        project.setAttribute("id", projectName)
        project.classList.add("project")

        if (owned){
            project.classList.add("mine")
        }

        //create a project element to display
        if (document.getElementById("thumb").classList.contains("active_filter")){
            
            projectPicture.setAttribute("style", "width: 100%; height: 80%;")
            project.appendChild(document.createElement("BR"))

            var shortProjectName
            if(projectName.length > 13){
                shortProjectName = document.createTextNode(projectName.substr(0,9)+"..")
            }
            else{
                shortProjectName = document.createTextNode(projectName)
            }
            project.setAttribute("title",projectName)
            project.appendChild(shortProjectName) 
        }
        else{
            project.setAttribute("style", "display:flex; flex-direction:row; flex-wrap:wrap; width: 100%; border-bottom: 1px solid darkgrey;")
            projectPicture.setAttribute("class", "browseColumn")
            
            shortProjectName = document.createElement("DIV")
            shortProjectName.innerHTML = projectName
            shortProjectName.setAttribute("class", "browseColumn")
            project.appendChild(shortProjectName) 

            var ownerName = document.createElement("DIV")
            var ownerNameIn = document.createTextNode(owner)
            ownerName.appendChild(ownerNameIn) 
            ownerName.setAttribute("class", "browseColumn")
            project.appendChild(ownerName) 

            var date = new Date(createdAt)
            var months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]
            var createdTime = document.createElement("DIV")
            createdTime.setAttribute("class", "browseColumn")
            var createdTimeIn = document.createTextNode(months[date.getMonth()] + " " + date.getFullYear())
            createdTime.appendChild(createdTimeIn) 
            project.appendChild(createdTime) 

            var updated = new Date(updatedAt)
            var updatedTime = document.createElement("DIV")
            var updatedTimeIn = document.createTextNode(months[updated.getMonth()] + " " + date.getFullYear())
            updatedTime.appendChild(updatedTimeIn)
            updatedTime.setAttribute("class", "browseColumn")
            project.appendChild(updatedTime) 
   
        }

        this.projectsSpaceDiv.appendChild(project) 

        project.addEventListener('click', () => {
            this.projectClicked(projectName, id, owned)
        })
    }
    
    /** 
     * Runs when you click on a project.
     */
    this.projectClicked = function(projectName, projectID, owned){
        //runs when you click on one of the projects
        if(projectName == "New Project"){
            this.createNewProjectPopup()
        }
        else if(owned){
            this.loadProject(projectName)
        }
        else{
            window.open('/run?'+projectID)
        }
    }
    
    /** 
     * Runs owned search first and then full github search
     */
    this.openTab = function(page) {

        // Show the current tab, and add an "active" class to the button that opened the tab
        //Click on the search bar so that when you start typing it shows updateCommands
        document.getElementById('menuInput').focus()
        
        this.loadProjectsBySearch("yoursButton", {key: "Enter"}, document.getElementById("project_search").value, "updated", page, true)
            .then( () => {
                this.loadProjectsBySearch("githubButton", {key: "Enter"}, document.getElementById("project_search").value, "stars", page, false)
            })
    }
    
    /** 
     * The popup to create a new project (giving it a name and whatnot).
     */
    this.createNewProjectPopup = function(){
        //Clear the popup and populate the fields we will need to create the new repo
        
        while (popup.firstChild) {
            popup.removeChild(popup.firstChild)
        }
        popup.setAttribute("style", "padding:2% 0")
        //Project name
        // <div class="form">
        var createNewProjectDiv = document.createElement("DIV")
        createNewProjectDiv.setAttribute("class", "form")
        createNewProjectDiv.setAttribute("style", "color:whitesmoke")
        
        //Add a title
        var header = document.createElement("H1")
        var title = document.createTextNode("Create a new project")
        header.appendChild(title)
        createNewProjectDiv.appendChild(header)
        
        //Create the form object
        var form = document.createElement("form")
        form.setAttribute("class", "login-form")
        createNewProjectDiv.appendChild(form)
        
        //Create the name field
        var name = document.createElement("input")
        name.setAttribute("id","project-name")
        name.setAttribute("type","text")
        name.setAttribute("placeholder","Project name")
        form.appendChild(name)
        
        //Add the description field
        var description = document.createElement("input")
        description.setAttribute("id", "project-description")
        description.setAttribute("type", "text")
        description.setAttribute("placeholder", "Project description")
        form.appendChild(description)
        
        //Grab all of the available licenses
        var licenseOptions = document.createElement('select')
        licenseOptions.setAttribute("id", "license-options")
        Object.keys(licenses).forEach( key => {
            var option = document.createElement('option')
            option.value = key
            option.text = key
            licenseOptions.appendChild(option)
        })
        
        form.appendChild(licenseOptions)
        
        //Add the button
        var createButton = document.createElement("button")
        createButton.setAttribute("type", "button")
        createButton.setAttribute("style", "height: 50px; border: 1px solid whitesmoke;")
        createButton.addEventListener('click', () => {
            this.createNewProject()
        })
        var buttonText = document.createTextNode("Create Project")
        createButton.appendChild(buttonText)
        form.appendChild(createButton)
    
        popup.appendChild(createNewProjectDiv)

    }
    
    /** 
     * Open a new tab with a sharable copy of the project.
     */
    this.shareOpenedProject = function(){
        alert("A page with a shareable url to this project will open in a new window. Share the link to that page with anyone you would like to share the project with.")
         

        octokit.repos.get({
            owner: currentUser,
            repo: currentRepoName
        }).then(result => {
            var ID = result.data.id
            window.open('/run?'+ID)
        })
    }
    
    /** 
     * Open a new tab with the github page for the project.
     */
    this.openGitHubPage = function(){
        //Open the github page for the current project in a new tab
        octokit.repos.get({
            owner: currentUser,
            repo: currentRepoName
        }).then(result => {
            var url = result.data.html_url
            window.open(url)
        })
    }
    
    /** 
     * Open a new tab with the README page for the project.
     */
    this.openREADMEPage = function(){
        //Open the github page for the current project in a new tab
        octokit.repos.get({
            owner: currentUser,
            repo: currentRepoName
        }).then(result => {
            var url = result.data.html_url + '/blob/master/README.md'
            window.open(url)
        })
    }
    
    /** 
     * Open a new tab with the Bill Of Materials page for the project.
     */
    this.openBillOfMaterialsPage = function(){
        //Open the github page for the current project in a new tab
        octokit.repos.get({
            owner: currentUser,
            repo: currentRepoName
        }).then(result => {
            var url = result.data.html_url + '/blob/master/BillOfMaterials.md'
            window.open(url)
        })
    }
    
    /** 
     * Search github for projects which match a string.
     */
    this.searchGithub = async (searchString,owned) => {
        //Load projects
        var query

        if(owned){
            query = searchString + ' ' + 'user:' + currentUser + ' topic:maslowcreate'
        }
        else{
            query = searchString + ' topic:maslowcreate -user:' + currentUser
        }

        return await octokit.search.repos({
            q: query,
            sort: 'stars',
            per_page: 10,
            page: 1,
            headers: {
                accept: 'application/vnd.github.mercy-preview+json'
            }
        })
    }
    
    /** 
     * Send user to GitHub settings page to delete project.
     */
    this.deleteProject = function(){
        //Open the github page for the current project in a new tab
        octokit.repos.get({
            owner: currentUser,
            repo: currentRepoName
        }).then(result => {
            var url = result.data.html_url + '/settings'
            window.open(url)
        })
    }

    /** 
     * Open pull request if it's a forked project.
     */
    this.makePullRequest = function(){
      
        //Open the github page for making a pull request to the current project in a new tab
        octokit.repos.get({
            owner: currentUser,
            repo: currentRepoName
        }).then(result => {
            
            const webString = "https://github.com/" + result.data.parent.full_name + "/compare/" + result.data.parent.default_branch + "..." + result.data.owner.login + ":" + result.data.default_branch
            
            window.open(webString)
        })
    }
    
    /** 
     * Creates a new blank project.
     */
    this.createNewProject = function(){

        if(typeof intervalTimer != undefined){
            clearInterval(intervalTimer) //Turn of auto saving
        }
        
        //Get name and description
        const name = document.getElementById('project-name').value
        const description = document.getElementById('project-description').value
        const licenseText = licenses[document.getElementById('license-options').value]
        
        //Load a blank project
        GlobalVariables.topLevelMolecule = new Molecule({
            x: 0, 
            y: 0, 
            topLevel: true, 
            name: name,
            atomType: "Molecule",
            uniqueID: GlobalVariables.generateUniqueID()
        })
        
        GlobalVariables.currentMolecule = GlobalVariables.topLevelMolecule
        
        //Create a new repo
        octokit.repos.createForAuthenticatedUser({
            name: name,
            description: description
        }).then(result => {
            //Once we have created the new repo we need to create a file within it to store the project in
            currentRepoName = result.data.name
            var jsonRepOfProject = GlobalVariables.topLevelMolecule.serialize()
            jsonRepOfProject.filetypeVersion = 1
            jsonRepOfProject.circleSegmentSize = GlobalVariables.circleSegmentSize
            const projectContent = window.btoa(JSON.stringify(jsonRepOfProject, null, 4))
            
            octokit.repos.createOrUpdateFileContents({
                owner: currentUser,
                repo: currentRepoName,
                path: "project.maslowcreate",
                message: "initialize repo", 
                content: projectContent
            }).then(() => {
                //Then create the BOM file
                var content = window.btoa(bomHeader) // create a file with just the header in it and base64 encode it
                octokit.repos.createOrUpdateFileContents({
                    owner: currentUser,
                    repo: currentRepoName,
                    path: "BillOfMaterials.md",
                    message: "initialize BOM", 
                    content: content
                }).then(() => {
                    //Then create the README file
                    content = window.btoa(readmeHeader) // create a file with just the word "init" in it and base64 encode it
                    octokit.repos.createOrUpdateFileContents({
                        owner: currentUser,
                        repo: currentRepoName,
                        path: "README.md",
                        message: "initialize README", 
                        content: content
                    }).then(() => {
                        octokit.repos.createOrUpdateFileContents({
                            owner: currentUser,
                            repo: currentRepoName,
                            path: "project.svg",
                            message: "SVG Picture", 
                            content: ""
                        }).then(()=>{
                            octokit.repos.createOrUpdateFileContents({
                                owner: currentUser,
                                repo: currentRepoName,
                                path: ".gitattributes",
                                message: "Create gitattributes", 
                                content: window.btoa("data binary")
                            }).then(()=>{
                                octokit.repos.createOrUpdateFileContents({ 
                                    owner: currentUser,
                                    repo: currentRepoName,
                                    path: "data.json",
                                    message: "Data file", 
                                    content: ""
                                }).then(()=>{
                                    octokit.repos.createOrUpdateFileContents({ 
                                        owner: currentUser,
                                        repo: currentRepoName,
                                        path: "LICENSE.txt",
                                        message: "Establish license", 
                                        content:  window.btoa(licenseText)
                                    }).then(()=>{
                                        intervalTimer = setInterval(() => { this.saveProject() }, 1200000) //Save the project regularly
                                    })
                                })
                            })
                        })
                    })
                })
            })
            
            //Update the project topics
            octokit.repos.replaceAllTopics({
                owner: currentUser,
                repo: currentRepoName,
                names: ["maslowcreate", "maslowcreate-project"],
                headers: {
                    accept: 'application/vnd.github.mercy-preview+json'
                }
            })
        })
        
        GlobalVariables.currentMolecule.backgroundClick()
        
        //Clear and hide the popup
        while (popup.firstChild) {
            popup.removeChild(popup.firstChild)
        }
        popup.classList.add('off')
        
        
    }

    /** 
     * Save the current project to github.
     */
    this.saveProject = function(){
        
        //Save the current project into the github repo
        if(currentRepoName != null){
            
            //Store the target repo incase a new project is loaded during the save
            const saveRepoName = currentRepoName
            const saveUser = currentUser
            
            if(typeof intervalTimer != undefined){
                clearInterval(intervalTimer) //Turn off auto saving to prevent it from saving again during this save
            }
            this.progressSave(0)
            // var shape = null

            // if(GlobalVariables.topLevelMolecule.value != null && typeof GlobalVariables.topLevelMolecule.value != 'number'){
            // shape = GlobalVariables.topLevelMolecule.value
            // }
            
            const passBOMOn = (bomItems) => {
                const values = {op: "svg", readPath: GlobalVariables.topLevelMolecule.path}
                const {answer} = window.ask(values)
                answer.then( answer => {
                    this.progressSave(10)
                    
                    var contentSvg = answer //Would compute the svg picture here
                    
                    var bomContent = bomHeader
                    
                    var totalParts = 0
                    var totalCost  = 0
                    if(bomItems != undefined){
                        bomItems.forEach(item => {
                            totalParts += item.numberNeeded
                            totalCost  += item.costUSD
                            bomContent = bomContent + "\n|" + item.BOMitemName + "|" + item.numberNeeded + "|$" + item.costUSD.toFixed(2) + "|" + convertLinks(item.source) + "|"
                        })
                    }
                    bomContent = bomContent + "\n|" + "Total: " + "|" + totalParts + "|$" + totalCost.toFixed(2) + "|" + " " + "|"
                    bomContent = bomContent+"\n\n 3xCOG MSRP: $" + (3*totalCost).toFixed(2)
                    
                    var readmeContent = readmeHeader + "\n\n" + "# " + saveRepoName + "\n\n![](/project.svg)\n\n"
                    GlobalVariables.topLevelMolecule.requestReadme().forEach(item => {
                        readmeContent = readmeContent + item + "\n\n\n"
                    })
                        
                    var jsonRepOfProject = GlobalVariables.topLevelMolecule.serialize()
                    jsonRepOfProject.filetypeVersion = 1
                    jsonRepOfProject.circleSegmentSize = GlobalVariables.circleSegmentSize
                    const projectContent = JSON.stringify(jsonRepOfProject, null, 4)
                           
                    var decoder = new TextDecoder('utf8')
                    var finalSVG = decoder.decode(contentSvg)
                    
                    
                    const askJsonVals = {op: "getJSON", readPath: GlobalVariables.topLevelMolecule.path}
                    const  {answer: answer2} = window.ask(askJsonVals)
                    answer2.then( JSONData => {
                        
                        this.createCommit(octokit,{
                            owner: saveUser,
                            repo: saveRepoName,
                            changes: {
                                files: {
                                    'BillOfMaterials.md': bomContent,
                                    'README.md': readmeContent,
                                    'project.svg': finalSVG,
                                    'project.maslowcreate': projectContent,
                                    'data.json': JSONData
                                },
                                commit: 'Autosave'
                            }
                        })
                    })

                    intervalTimer = setInterval(() => this.saveProject(), 1200000)
                })
            }
            extractBomTags(GlobalVariables.topLevelMolecule.path, passBOMOn)
        }
    }
    
    /** 
     * Creates saving/saved pop up
     */
    this.progressSave = function (progress, saving = true) {
        
        progress = Math.max(0, progress) //Make it so the progress can't be displayed negitive
        
        var popUp = document.getElementById("popUp")   
        let popUpBox = document.querySelector('#Progress_Status') 
        //var width = 1; 
        popUp.setAttribute("style","display:block")
        popUpBox.setAttribute("style","display:block")
        
        if (progress >= 100) { 
            popUp.style.width = progress + '%' 
            if(saving){
                popUp.textContent = "Project Saved"
                setTimeout(function() {
                    popUp.setAttribute("style","display:none")
                    popUpBox.setAttribute("style","display:none")
                }, 4000)
            }
            else{
                popUp.setAttribute("style","display:none")
                popUpBox.setAttribute("style","display:none")
            }
        } else { 
            if(saving){
                popUp.textContent = "Saving..."
            }
            else{
                popUp.textContent = "Loading..."+progress.toFixed(1)+"%"
            }
            popUp.style.width = progress + '%'  
        } 
    } 
        
    /** 
     * Create a commit as part of the saving process.
     */
    this.createCommit = async function(octokit, { owner, repo, base, changes }) {
        this.progressSave(30)
        let response
        
        if (!base) {
            response = await octokit.repos.get({ owner, repo })
            base = response.data.default_branch
        }
        this.progressSave(40)
        
        response = await octokit.repos.listCommits({
            owner,
            repo,
            sha: base,
            per_page: 1
        })
        
        let latestCommitSha = response.data[0].sha
        const treeSha = response.data[0].commit.tree.sha
        this.progressSave(60)
      
        response = await octokit.git.createTree({
            owner,
            repo,
            base_tree: treeSha,
            tree: Object.keys(changes.files).map(path => {
                if(changes.files[path] != null){
                    return {
                        path,
                        mode: '100644',
                        content: changes.files[path]
                    }
                }
                else{
                    return {
                        path,
                        mode: '100644',
                        sha: null
                    }
                }
            })
        })
        const newTreeSha = response.data.sha
        this.progressSave(80)

        response = await octokit.git.createCommit({
            owner,
            repo,
            message: changes.commit,
            tree: newTreeSha,
            parents: [latestCommitSha]
        })
        latestCommitSha = response.data.sha

        this.progressSave(90)
      
        await octokit.git.updateRef({
            owner,
            repo,
            sha: latestCommitSha,
            ref: "heads/" + base,
            force: true
        })
        this.progressSave(100)
        console.warn("Project saved")
       
    }
    
    /** 
     * Loads a project from github by name.
     */
    this.loadProject = async function(projectName){
        
        this.totalAtomCount = 0
        this.numberOfAtomsToLoad = 0

        GlobalVariables.startTime = new Date().getTime()
        
        if(typeof intervalTimer != undefined){
            clearInterval(intervalTimer) //Turn off auto saving
        }

        //Clear and hide the popup
        while (popup.firstChild) {
            popup.removeChild(popup.firstChild)
        }
        popup.classList.add('off')
        
        currentRepoName = projectName
        
        //Load a blank project
        GlobalVariables.topLevelMolecule = new Molecule({
            x: 0, 
            y: 0, 
            topLevel: true, 
            atomType: "Molecule"
        })
        
        GlobalVariables.currentMolecule = GlobalVariables.topLevelMolecule
        
        octokit.repos.getContent({
            owner: currentUser,
            repo: projectName,
            path: 'project.maslowcreate'
        }).then(result => {
            //content will be base64 encoded
            let rawFile = JSON.parse(atob(result.data.content))
            
            if(rawFile.circleSegmentSize){
                GlobalVariables.circleSegmentSize = rawFile.circleSegmentSize
            }
            
            if(rawFile.filetypeVersion == 1){
                GlobalVariables.topLevelMolecule.deserialize(rawFile)
            }
            else{
                GlobalVariables.topLevelMolecule.deserialize(this.convertFromOldFormat(rawFile))
            }
        })
        octokit.repos.get({
            owner: currentUser,
            repo: currentRepoName
        }).then(result => {
            GlobalVariables.fork = result.data.fork
            if(!GlobalVariables.fork){
                document.getElementById("pull_top").style.display = "none"
            }
            else{
                document.getElementById("pull_top").style.display = "inline"
            }
        })
        
    }
    
    this.convertFromOldFormat = function(json){
        
        var listOfMoleculeAtoms = json.molecules
        
        //Find the top level molecule
        var projectObject = listOfMoleculeAtoms.filter((molecule) => { return molecule.topLevel == true })[0]
        //Remove that element from the listOfMoleculeAtoms
        listOfMoleculeAtoms.splice(listOfMoleculeAtoms.findIndex(e => e.topLevel == true),1)
        
        //Recursive function to walk the tree and find molecule placeholders
        function walkForMolecules(projectObject){
            projectObject.allAtoms.forEach(function(atom, allAtomsIndex, allAtomsObject) {
                if(atom.atomType == "Molecule"){
                    
                    if(atom.allAtoms != undefined){ //If this molecule has allAtoms
                        walkForMolecules(atom)//Walk it
                    }
                    else{ //Else replace it with a version which does have allAtoms from the list
                        //Find the version in molecules list and plug it in
                        allAtomsObject[allAtomsIndex] = listOfMoleculeAtoms.filter((molecule) => { return molecule.uniqueID == atom.uniqueID })[0]
                        //Remove that element from the listOfMoleculeAtoms
                        listOfMoleculeAtoms.splice(listOfMoleculeAtoms.findIndex(e => e.uniqueID == atom.uniqueID),1)
                    }
                }
            })
        }
        
        //Find any placeholder molecules in there (this needs to be a full tree walk for everything to work)
        while(listOfMoleculeAtoms.length > 0){
            walkForMolecules(projectObject)
        }
        
        return projectObject
    }
    
    /** 
     * Begins the automatic process of saving the project
     */
    this.beginAutosave = function(){
        intervalTimer = setInterval(() => this.saveProject(), 120000) //Save the project regularly
    }
    
    /** 
     * Loads a project from github by its github ID.
     */
    this.getProjectByID = async function(id, saveUserInfo){
        let repo = await octokit.request('GET /repositories/:id', {id})
        //Find out the owners info;
        const user     = repo.data.owner.login
        const repoName = repo.data.name
        const description = repo.data.description
        //Get the file contents
        let result = await octokit.repos.getContent({
            owner: user,
            repo: repoName,
            path: 'project.maslowcreate'
        })
        
        //If this is the top level we will save the rep info at the top level
        if(saveUserInfo){
            currentUser = user
            currentRepoName = repoName
        }
        
        let rawFile = JSON.parse(atob(result.data.content))
        
        rawFile.description = description
        
        if(rawFile.filetypeVersion == 1){
            return rawFile
        }
        else{
            return this.convertFromOldFormat(rawFile)
        }
    }
    
    /** 
     * Loads a project's data from github by its github ID.
     */
    this.getProjectDataByID = async function(id){
        let repo = await octokit.request('GET /repositories/:id', {id})
        //Find out the owners info;
        const user     = repo.data.owner.login
        const repoName = repo.data.name
        
        try{
            let jsonData = await octokit.repos.getContent({
                owner: user,
                repo: repoName,
                path: 'data.json'
            })
            
            jsonData = atob(jsonData.data.content)
            return jsonData
            
        }catch(err){
            console.warn("Unable to load project data from github...using full model")
            return false
        }
    }
    
    /** 
     * Export a molecule as a new github project.
     */
    this.exportCurrentMoleculeToGithub = function(molecule){
        
        //Get name and description
        var name = molecule.name
        var description = "A stand alone molecule exported from Maslow Create"
        
        //Create a new repo
        octokit.repos.createForAuthenticatedUser({
            name: name,
            description: description
        }).then(result => {
            //Once we have created the new repo we need to create a file within it to store the project in
            var repoName = result.data.name
            var id       = result.data.id
            var path     = "project.maslowcreate"
            var content  = window.btoa("init") // create a file with just the word "init" in it and base64 encode it
            octokit.repos.createOrUpdateFileContents({
                owner: currentUser,
                repo: repoName,
                path: path,
                message: "initialize repo", 
                content: content
            }).then(() => {
                
                //Save the molecule into the newly created repo
                
                var path = "project.maslowcreate"
                
                molecule.topLevel = true //force the molecule to export in the long form as if it were the top level molecule
                var content = window.btoa(JSON.stringify(molecule.serialize({molecules: []}), null, 4)) //Convert the passed molecule object to a JSON string and then convert it to base64 encoding
                
                //Get the SHA for the file
                octokit.repos.getContent({
                    owner: currentUser,
                    repo: repoName,
                    path: path
                }).then(result => {
                    var sha = result.data.sha
                    
                    //Save the repo to the file
                    octokit.repos.updateFile({
                        owner: currentUser,
                        repo: repoName,
                        path: path,
                        message: "export Molecule", 
                        content: content,
                        sha: sha
                    }).then(() => {
                        //Replace the existing molecule now that we just exported
                        molecule.replaceThisMoleculeWithGithub(id)
                    })
                })

            })
            
            //Update the project topics
            octokit.repos.replaceTopics({
                owner: currentUser,
                repo: repoName,
                names: ["maslowcreate", "maslowcreate-molecule"],
                headers: {
                    accept: 'application/vnd.github.mercy-preview+json'
                }
            })
            
        })
    }

    /** 
     * Like a project on github by unique ID.
     */
    this.starProject = function(id){
        //Authenticate - Initialize with OAuth.io app public key
        OAuth.initialize('BYP9iFpD7aTV9SDhnalvhZ4fwD8')
        // Use popup for oauth
        OAuth.popup('github').then(github => {

            octokit = new Octokit({
                auth: github.access_token
            })
            
            octokit.request('GET /repositories/:id', {id}).then(result => {
                //Find out the information of who owns the project we are trying to like
                
                var user     = result.data.owner.login
                var repoName = result.data.name
                
                octokit.repos.listTopics({
                    owner: user, 
                    repo: repoName,
                    headers: {
                        accept: 'application/vnd.github.mercy-preview+json'
                    }
                }).then(()=> { 
                    //Find out if the project has been starred and unstar if it is
                    octokit.activity.checkStarringRepo({
                        owner:user,
                        repo: repoName
                    }).then(() => { 
                        var button= document.getElementById("Star-button")
                        button.setAttribute("class","browseButton")
                        button.innerHTML = "Star"
                        octokit.activity.unstarRepo({
                            owner: user,
                            repo: repoName
                        })
                    })
                        
                }).then(() =>{ 
                    var button= document.getElementById("Star-button")
                    button.setAttribute("class","liked")
                    button.innerHTML = "Starred"
                    octokit.activity.starRepo({
                        owner: user,
                        repo: repoName
                    })
                })
            })
        })
    }
    /** 
     * Fork a project on github by unique ID.
     */
    this.forkByID = function(id){
        
        //Authenticate - Initialize with OAuth.io app public key
        OAuth.initialize('BYP9iFpD7aTV9SDhnalvhZ4fwD8')
        // Use popup for oauth
        OAuth.popup('github').then(github => {

            octokit = new Octokit({
                auth: github.access_token
            })
            
            octokit.request('GET /repositories/:id', {id}).then(result => {
                //Find out the information of who owns the project we are trying to fork
                var user     = result.data.owner.login
                var repoName = result.data.name
                
                octokit.repos.listTopics({
                    owner: user, 
                    repo: repoName,
                    headers: {
                        accept: 'application/vnd.github.mercy-preview+json'
                    }
                }).then(result => {
                    var topics = result.data.names
                    
                    //Create a fork of the project with the found user name and repo name under your account
                    octokit.repos.createFork({
                        owner: user, 
                        repo: repoName,
                        headers: {
                            accept: 'application/vnd.github.mercy-preview+json'
                        }
                    }).then(result => {
                        var repoName = result.data.name
                        //Manually copy over the topics which are lost in forking
                        octokit.repos.replaceTopics({
                            owner: result.data.owner.login,
                            repo: result.data.name,
                            names: topics,
                            headers: {
                                accept: 'application/vnd.github.mercy-preview+json'
                            }
                        }).then(() => {
                            
                            //Remove everything in the popup now
                            while (popup.firstChild) {
                                popup.removeChild(popup.firstChild)
                            }
                            
                            popup.classList.remove('off')
                            popup.setAttribute("style", "text-align: center")

                            var subButtonDiv = document.createElement('div')
                            subButtonDiv.setAttribute("class", "form")
                            
                            //Add a title
                            var title = document.createElement("H3")
                            title.appendChild(document.createTextNode("A copy of the project '" + repoName + "' has been copied and added to your projects. You can view it by clicking the button below."))
                            subButtonDiv.setAttribute('style','color:white;')
                            subButtonDiv.appendChild(title)
                            subButtonDiv.appendChild(document.createElement("br"))
                            
                            var form = document.createElement("form")
                            subButtonDiv.appendChild(form)
                            var button = document.createElement("button")
                            button.setAttribute("type", "button")
                            button.setAttribute("style", "border:2px solid white;")
                            button.appendChild(document.createTextNode("View Projects"))
                            button.addEventListener("click", () => {
                                window.location.href = '/'
                            })
                            form.appendChild(button)
                            popup.appendChild(subButtonDiv)
                        })
                    })
                })
            })
        })
    }
    
    /** 
     * Upload or remove files from github. Files with null content will be deleted.
     * @param {object} files A dictionary with paths as keys and the content as the answer.
     */
    this.uploadAFile = async function(files){
        
        await this.createCommit(octokit,{
            owner: currentUser,
            repo: currentRepoName,
            changes: {
                files: files,
                commit: 'Upload file'
            }
        })
    }
    
    /** 
     * Get a file from github. Calback is called after the retrieved.
     */
    this.getAFile = async function(filePath){
        
        const result = await octokit.repos.getContent({
            owner: currentUser,
            repo: currentRepoName,
            path: filePath
        })
        
        //content will be base64 encoded
        let rawFile = atob(result.data.content)
        return rawFile
    }
    
    /** 
     * Get a link to the raw version of a file on GitHub
     */
    this.getAFileRawPath = function(filePath){
        const rawPath = "https://raw.githubusercontent.com/" + currentUser + "/" + currentRepoName + "/main/" + filePath
        return rawPath
    }
    
}