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
}
}