diff --git a/src/assets/new-project/assets/css/style.css b/src/assets/new-project/assets/css/style.css index 689fac19fc..b1a94ca10b 100644 --- a/src/assets/new-project/assets/css/style.css +++ b/src/assets/new-project/assets/css/style.css @@ -909,6 +909,12 @@ img { margin-bottom: 30px; } +.project-bottom { + border-top: 3px solid rgba(255, 255, 255, 0.1); + padding-top: 20px; + margin-top: auto; +} + .new-project-content { overflow-y: auto; } diff --git a/src/assets/new-project/assets/js/code-editor.js b/src/assets/new-project/assets/js/code-editor.js index 743daf8899..7629cea98d 100644 --- a/src/assets/new-project/assets/js/code-editor.js +++ b/src/assets/new-project/assets/js/code-editor.js @@ -204,10 +204,6 @@ function initCodeEditor() { _openURLInTauri(document.getElementById(iconID).getAttribute('href')); }; } - if(window.top.__TAURI__) { - // in desktop, we don't show github project option till we have git extension integrated. - document.getElementById("newGitHubProject").classList.add("forced-hidden"); - } document.getElementById("newGitHubProject").onclick = function() { Metrics.countEvent(Metrics.EVENT_TYPE.NEW_PROJECT, "main.Click", "github-project"); window.location.href = 'new-project-github.html'; diff --git a/src/assets/new-project/assets/js/new-git-project.js b/src/assets/new-project/assets/js/new-git-project.js new file mode 100644 index 0000000000..76d51f91cf --- /dev/null +++ b/src/assets/new-project/assets/js/new-git-project.js @@ -0,0 +1,118 @@ +/* + * GNU AGPL-3.0 License + * + * Copyright (c) 2021 - present core.ai . All rights reserved. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License + * for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://opensource.org/licenses/AGPL-3.0. + * + */ + +/*global newProjectExtension, Strings, Metrics*/ +/*eslint no-console: 0*/ +/*eslint strict: ["error", "global"]*/ +/* jshint ignore:start */ + +function desktopInit() { + const LAST_GIT_CLONE_BASE_DIR = "PH_LAST_GIT_CLONE_BASE_DIR"; + let createProjectBtn, websiteURLInput, locationInput; + + function _validateGitURL(errors) { + let gitURL = websiteURLInput.value; + if(gitURL){ + $(websiteURLInput).removeClass("error-border"); + return true; + } + $(websiteURLInput).addClass("error-border"); + errors.push(`  ${Strings.ERROR_GIT_URL_INVALID}`); + return false; + } + + function _validateProjectLocation(errors) { + let location = locationInput.value; + if( location === Strings.PLEASE_SELECT_A_FOLDER){ + $(locationInput).addClass("error-border"); + return false; + } + if(locationInput.error){ + errors.push(`  ${locationInput.error}`); + $(locationInput).addClass("error-border"); + return false; + } + $(locationInput).removeClass("error-border"); + return true; + } + + function _validate() { + const errors = []; + let isValid = _validateGitURL(errors); + isValid = _validateProjectLocation(errors) && isValid; + $(createProjectBtn).prop('disabled', !isValid); + const $messageDisplay = $("#messageDisplay"); + $messageDisplay.html(""); + if(!isValid) { + $messageDisplay.html(errors.join("
")); + } + return isValid; + } + + async function _deduceClonePath(newPath) { + if(!newPath){ + newPath = locationInput.originalPath; + } + if(!newPath){ + return; + } + const {clonePath, error} = await newProjectExtension.getGitCloneDir(newPath, websiteURLInput.value); + locationInput.clonePath = clonePath; + locationInput.value = window.top.Phoenix.fs.getTauriPlatformPath(clonePath); + locationInput.error = error; + locationInput.originalPath = newPath; + } + + function _selectFolder() { + newProjectExtension.showFolderSelect(locationInput.originalPath || "") + .then((newPath)=>{ + _deduceClonePath(newPath).then(_validate); + }).catch((err)=>{ + console.error("user cancelled or error", err); + }); + } + + function _createProjectClicked() { + localStorage.setItem(LAST_GIT_CLONE_BASE_DIR, locationInput.originalPath); + newProjectExtension.gitClone(websiteURLInput.value, locationInput.clonePath); + Metrics.countEvent(Metrics.EVENT_TYPE.NEW_PROJECT, "git.Click", "create"); + newProjectExtension.closeDialogue(); + } + + function initGitProject() { + $(".label-clone").text(Strings.GIT_CLONE_URL); + createProjectBtn = document.getElementById("createProjectBtn"); + websiteURLInput = document.getElementById("websiteURLInput"); + locationInput = document.getElementById("locationInput"); + createProjectBtn.onclick = _createProjectClicked; + $(websiteURLInput).keyup(()=>{ + _deduceClonePath().then(_validate); + }); + locationInput.value = Strings.PLEASE_SELECT_A_FOLDER; + locationInput.onclick = _selectFolder; + websiteURLInput.value = "https://github.com/phcode-dev/HTML-Starter-Templates.git"; + _deduceClonePath(localStorage.getItem(LAST_GIT_CLONE_BASE_DIR)).then(_validate); + } + window.initGitProject = initGitProject; +} + +if(window.top.Phoenix.isNativeApp){ + desktopInit(); +} diff --git a/src/assets/new-project/assets/js/new-github-project.js b/src/assets/new-project/assets/js/new-github-project.js index b0bd9e8981..c6da86fc54 100644 --- a/src/assets/new-project/assets/js/new-github-project.js +++ b/src/assets/new-project/assets/js/new-github-project.js @@ -23,115 +23,127 @@ /*eslint strict: ["error", "global"]*/ /* jshint ignore:start */ -let createProjectBtn, websiteURLInput, locationInput; -const FLATTEN_ZIP_FIRST_LEVEL_DIR = true; +function browserInit() { + let createProjectBtn, websiteURLInput, locationInput; + const FLATTEN_ZIP_FIRST_LEVEL_DIR = true; -function _isValidGitHubURL(url) { - // strip trailing slash - url = url.replace(/\/$/, ""); - let githubPrefix = "https://github.com/"; - let components = url.replace("https://github.com/", '').split('/'); - if(!url.startsWith(githubPrefix) || components.length !== 2){ - return false; + function _isValidGitHubURL(url) { + // strip trailing slash + url = url.replace(/\/$/, ""); + let githubPrefix = "https://github.com/"; + let components = url.replace("https://github.com/", '').split('/'); + if(!url.startsWith(githubPrefix) || components.length !== 2){ + return false; + } + return true; } - return true; -} -function _fixGitHubBrokenURL() { - let githubPrefix = "https://github.com/", - gitSuffix = '.git'; - let githubURL = websiteURLInput.value; - if(githubURL.startsWith("http:")){ - githubURL = githubURL.replace("http:", "https:"); - } - if(!githubURL.startsWith(githubPrefix)){ - return; - } - // strip any query string params if present - let queryParamTrimIndex = githubURL.indexOf('?') >= 0 ? githubURL.indexOf('?') : githubURL.length; - githubURL = githubURL.substring(0, queryParamTrimIndex); - // trim everything after https://github.com/orgname/repo/... to https://github.com/orgname/repo - let components = githubURL.replace("https://github.com/", '').split('/'); - // trim .git at the end of the name - if(githubURL.endsWith(gitSuffix)){ - githubURL = githubURL.replace(new RegExp(gitSuffix + '$'), ''); - } - if(components.length > 2){ - githubURL = `https://github.com/${components[0]}/${components[1]}`; + function _fixGitHubBrokenURL() { + let githubPrefix = "https://github.com/", + gitSuffix = '.git'; + let githubURL = websiteURLInput.value; + if(githubURL.startsWith("http:")){ + githubURL = githubURL.replace("http:", "https:"); + } + if(!githubURL.startsWith(githubPrefix)){ + return; + } + // strip any query string params if present + let queryParamTrimIndex = githubURL.indexOf('?') >= 0 ? githubURL.indexOf('?') : githubURL.length; + githubURL = githubURL.substring(0, queryParamTrimIndex); + // trim everything after https://github.com/orgname/repo/... to https://github.com/orgname/repo + let components = githubURL.replace("https://github.com/", '').split('/'); + // trim .git at the end of the name + if(githubURL.endsWith(gitSuffix)){ + githubURL = githubURL.replace(new RegExp(gitSuffix + '$'), ''); + } + if(components.length > 2){ + githubURL = `https://github.com/${components[0]}/${components[1]}`; + } + websiteURLInput.value = githubURL; } - websiteURLInput.value = githubURL; -} -function _validateGitHubURL() { - _fixGitHubBrokenURL(); - let githubURL = websiteURLInput.value; - if(_isValidGitHubURL(githubURL)){ - $(websiteURLInput).removeClass("error-border"); - return true; + function _validateGitHubURL() { + _fixGitHubBrokenURL(); + let githubURL = websiteURLInput.value; + const $messageDisplay = $("#messageDisplay"); + if(_isValidGitHubURL(githubURL)){ + $(websiteURLInput).removeClass("error-border"); + $messageDisplay.html(""); + return true; + } + $(websiteURLInput).addClass("error-border"); + $messageDisplay.html(`  ${Strings.ERROR_ONLY_GITHUB}`); + return false; } - $(websiteURLInput).addClass("error-border"); - return false; -} -function _validateProjectLocation() { - if(!window.showDirectoryPicker){ - // fs access apis not present, so we will give phoenix empty location to figure out a suitable location + function _validateProjectLocation() { + if(!window.showDirectoryPicker){ + // fs access apis not present, so we will give phoenix empty location to figure out a suitable location + return true; + } + let location = locationInput.value; + if( location === Strings.PLEASE_SELECT_A_FOLDER){ + $(locationInput).addClass("error-border"); + return false; + } + $(locationInput).removeClass("error-border"); return true; } - let location = locationInput.value; - if( location === Strings.PLEASE_SELECT_A_FOLDER){ - $(locationInput).addClass("error-border"); - return false; + + function _validate() { + return _validateGitHubURL() + && _validateProjectLocation(); } - $(locationInput).removeClass("error-border"); - return true; -} -function _validate() { - return _validateGitHubURL() - && _validateProjectLocation(); -} + function _selectFolder() { + newProjectExtension.showFolderSelect() + .then(file =>{ + locationInput.fullPath = file; + locationInput.value = file.replace(newProjectExtension.getMountDir(), ""); + _validateProjectLocation(); + }); + } -function _selectFolder() { - newProjectExtension.showFolderSelect() - .then(file =>{ - locationInput.fullPath = file; - locationInput.value = file.replace(newProjectExtension.getMountDir(), ""); - _validateProjectLocation(); - }); -} + function _createProjectClicked() { + if(_validate()){ + let githubURL = websiteURLInput.value; + let components = githubURL.replace("https://github.com/", '').split('/'); + let zipURL = `https://phcode.site/getGitHubZip?org=${components[0]}&repo=${components[1]}`; + let suggestedProjectName = `${components[0]}-${components[1]}`; + newProjectExtension.downloadAndOpenProject( + zipURL, + locationInput.fullPath, suggestedProjectName, FLATTEN_ZIP_FIRST_LEVEL_DIR) + .then(()=>{ + Metrics.countEvent(Metrics.EVENT_TYPE.NEW_PROJECT, "github.Click", "create.success"); + newProjectExtension.closeDialogue(); + }); + } else { + newProjectExtension.showErrorDialogue( + Strings.MISSING_FIELDS, + Strings.PLEASE_FILL_ALL_REQUIRED); + } + Metrics.countEvent(Metrics.EVENT_TYPE.NEW_PROJECT, "github.Click", "create"); + } -function _createProjectClicked() { - if(_validate()){ - let githubURL = websiteURLInput.value; - let components = githubURL.replace("https://github.com/", '').split('/'); - let zipURL = `https://phcode.site/getGitHubZip?org=${components[0]}&repo=${components[1]}`; - let suggestedProjectName = `${components[0]}-${components[1]}`; - newProjectExtension.downloadAndOpenProject( - zipURL, - locationInput.fullPath, suggestedProjectName, FLATTEN_ZIP_FIRST_LEVEL_DIR) - .then(()=>{ - Metrics.countEvent(Metrics.EVENT_TYPE.NEW_PROJECT, "github.Click", "create.success"); - newProjectExtension.closeDialogue(); - }); - } else { - newProjectExtension.showErrorDialogue( - Strings.MISSING_FIELDS, - Strings.PLEASE_FILL_ALL_REQUIRED); + function initGitProject() { + if(!window.showDirectoryPicker){ // fs access apis not present + $(document.getElementById("projectLocation")).addClass("forced-hidden"); + } + $(".label-clone").text(Strings.GIT_REPO_URL); + createProjectBtn = document.getElementById("createProjectBtn"); + websiteURLInput = document.getElementById("websiteURLInput"); + locationInput = document.getElementById("locationInput"); + createProjectBtn.onclick = _createProjectClicked; + $(websiteURLInput).keyup(_validate); + locationInput.value = Strings.PLEASE_SELECT_A_FOLDER; + locationInput.onclick = _selectFolder; + websiteURLInput.value = "https://github.com/phcode-dev/HTML-Starter-Templates"; + _validate(); } - Metrics.countEvent(Metrics.EVENT_TYPE.NEW_PROJECT, "github.Click", "create"); + window.initGitProject = initGitProject; } -function initGithubProject() { - if(!window.showDirectoryPicker){ // fs access apis not present - $(document.getElementById("projectLocation")).addClass("forced-hidden"); - } - createProjectBtn = document.getElementById("createProjectBtn"); - websiteURLInput = document.getElementById("websiteURLInput"); - locationInput = document.getElementById("locationInput"); - createProjectBtn.onclick = _createProjectClicked; - $(websiteURLInput).keyup(_validate); - locationInput.value = Strings.PLEASE_SELECT_A_FOLDER; - locationInput.onclick = _selectFolder; - _validate(); +if(!window.top.Phoenix.isNativeApp){ + browserInit(); } diff --git a/src/assets/new-project/code-editor.html b/src/assets/new-project/code-editor.html index 594ff0c001..643f6e9a99 100644 --- a/src/assets/new-project/code-editor.html +++ b/src/assets/new-project/code-editor.html @@ -124,8 +124,8 @@

{{START_PROJECT}}

  • - image - {{GITHUB_PROJECT}} + image + {{GIT_PROJECT}}
  • diff --git a/src/assets/new-project/images/git.svg b/src/assets/new-project/images/git.svg new file mode 100644 index 0000000000..f2b825c812 --- /dev/null +++ b/src/assets/new-project/images/git.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/new-project/images/tab-img7.png b/src/assets/new-project/images/tab-img7.png deleted file mode 100644 index b56518494b..0000000000 Binary files a/src/assets/new-project/images/tab-img7.png and /dev/null differ diff --git a/src/assets/new-project/new-project-github.html b/src/assets/new-project/new-project-github.html index e66be16979..72205d9722 100644 --- a/src/assets/new-project/new-project-github.html +++ b/src/assets/new-project/new-project-github.html @@ -11,9 +11,15 @@ + + - -
    + +
    @@ -24,7 +30,7 @@
    - {{NEW_PROJECT_FROM_GITHUB}} + {{NEW_PROJECT_FROM_GIT}}
    @@ -42,16 +48,22 @@
    - {{GITHUB_REPO_URL}} - - + +
    {{LOCATION}} - + +
    +
    + +
    +
    + +
    diff --git a/src/extensionsIntegrated/Phoenix/new-project.js b/src/extensionsIntegrated/Phoenix/new-project.js index e038585ec1..09a126a342 100644 --- a/src/extensionsIntegrated/Phoenix/new-project.js +++ b/src/extensionsIntegrated/Phoenix/new-project.js @@ -18,7 +18,7 @@ * */ -/*global Phoenix*/ +/*global path, jsPromise*/ define(function (require, exports, module) { const Dialogs = require("widgets/Dialogs"), @@ -376,9 +376,9 @@ define(function (require, exports, module) { }); } - function showFolderSelect() { + function showFolderSelect(initialPath = "") { return new Promise((resolve, reject)=>{ - FileSystem.showOpenDialog(false, true, Strings.CHOOSE_FOLDER, '', null, function (err, files) { + FileSystem.showOpenDialog(false, true, Strings.CHOOSE_FOLDER, initialPath, null, function (err, files) { if(err || files.length !== 1){ reject(); return; @@ -388,6 +388,86 @@ define(function (require, exports, module) { }); } + async function gitClone(url, cloneDIR) { + try{ + const cloneFolderExists = await _dirExists(cloneDIR); + if(!cloneFolderExists) { + await Phoenix.VFS.ensureExistsDirAsync(cloneDIR); + } + await jsPromise(ProjectManager.openProject(cloneDIR)); + CommandManager.execute("git-clone-url", url, cloneDIR ); + } catch (e) { + setTimeout(async ()=>{ + // we need this timeout as when user clicks clone in new project dialog, it will immediately + // close the error dialog too as it dismisses itself. + showErrorDialogue(Strings.ERROR_CLONING_TITLE, e.message || e); + }, 100); + console.error("git clone failed: ", url, cloneDIR, e); + Metrics.countEvent(Metrics.EVENT_TYPE.NEW_PROJECT, "gitClone", "fail"); + } + } + + function _getGitFolderName(gitURL) { + if (typeof gitURL !== 'string' || !gitURL.trim()) { + return ""; + } + // Remove trailing `.git` if it exists and split the URL + const parts = gitURL.replace(/\.git$/, '').split('/'); + // Return the last segment as the project folder name + return parts[parts.length - 1]; + } + + async function _dirExists(fullPath) { + try { + const {entry} = await FileSystem.resolveAsync(fullPath); + return entry.isDirectory; + } catch (e) { + return false; + } + } + + /** + * Determines which directory to use for a Git clone operation: + * 1. If the selected directory is empty, returns that directory. + * 2. Otherwise, checks/creates a child directory named after the Git project. + * - If that child directory is (or becomes) empty, returns its entry. + * - If it is not empty, returns null. + * + * @param {string} selectedDir - The full path to the user-selected directory. + * @param {string} gitURL - The Git clone URL (used to derive the child folder name). + * @returns {Promise<{error, }>} error string to show to user and the path to clone. + */ + async function getGitCloneDir(selectedDir, gitURL) { + const selectedDirExists = await _dirExists(selectedDir); + if (!selectedDirExists) { + return {error: Strings.ERROR_GIT_FOLDER_NOT_EXIST, clonePath: selectedDir}; + } + + const {entry: selectedEntry} = await FileSystem.resolveAsync(selectedDir); + if (await selectedEntry.isEmptyAsync()) { + return {clonePath: selectedDir}; + } + + // If not empty, compute the child directory path + const folderName = _getGitFolderName(gitURL); + if(!folderName){ + return {error: Strings.ERROR_GIT_FOLDER_NOT_EMPTY, clonePath: selectedDir}; + } + + const childDirPath = path.join(selectedDir, folderName); + const childDirExists = await _dirExists(childDirPath); + if (!childDirExists) { + return {clonePath: childDirPath}; + } + // The child directory exists; check if it is empty + const {entry: childEntry} = await FileSystem.resolveAsync(childDirPath); + const isChildEmpty = await childEntry.isEmptyAsync(); + if(isChildEmpty){ + return {clonePath: childDirPath}; + } + return {error: Strings.ERROR_GIT_FOLDER_NOT_EMPTY, clonePath: childDirPath}; + } + function showAboutBox() { CommandManager.execute(Commands.HELP_ABOUT); } @@ -398,6 +478,8 @@ define(function (require, exports, module) { exports.downloadAndOpenProject = downloadAndOpenProject; exports.showFolderSelect = showFolderSelect; exports.showErrorDialogue = showErrorDialogue; + exports.getGitCloneDir = getGitCloneDir; + exports.gitClone = gitClone; exports.setupExploreProject = defaultProjects.setupExploreProject; exports.setupStartupProject = defaultProjects.setupStartupProject; exports.alreadyExists = window.Phoenix.VFS.existsAsync; diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index d115b006f4..e947391465 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -1184,20 +1184,26 @@ define({ "CODE_EDITOR": "Code Editor", "BUILD_THE_WEB": "Build the web", "IMPORT_PROJECT": "Import Project", - "GITHUB_PROJECT": "GitHub Project", - "NEW_PROJECT_FROM_GITHUB": "New Project from GitHub", - "GITHUB_REPO_URL": "GitHub Repo URL :", + "GIT_PROJECT": "Get from Git", + "NEW_PROJECT_FROM_GIT": "New Project from Git", + "GIT_REPO_URL": "Git Repo URL :", + "GIT_CLONE_URL": "Git Clone URL :", "START_PROJECT": "Start Project\u2026", "DOWNLOAD_DESKTOP_APP": "Download Desktop App", "GET_DESKTOP_APP": "Get Desktop App", "CREATE_PROJECT": "Create Project", "SETTING_UP_PROJECT": "Setting Up Project", "LOCATION": "Location :", + "ERROR_ONLY_GITHUB": "The browser version of {APP_NAME} supports only GitHub URLs. To work with other Git URLs, please use the desktop app.", + "ERROR_GIT_URL_INVALID": "Please enter a valid Git clone URL.", + "ERROR_GIT_FOLDER_NOT_EMPTY": "The Selected folder cannot be used for Git clone as it is not empty or is unreadable.", + "ERROR_GIT_FOLDER_NOT_EXIST": "The Selected folder cannot be used for Git clone as it does not exist.", + "ERROR_CLONING_TITLE": "Git clone Failed", "DOWNLOADING": "Downloading...", "DOWNLOADING_FILE": "Downloading {0}...", "EXTRACTING_FILES_PROGRESS": "Extracting {0} of {1} files.", "COMPRESS_FILES_PROGRESS": "Compressing {0} of {1} files.", - "DOWNLOAD_FAILED_MESSAGE": "Make sure the download URL is a valid.
    NB: As Phoenix is in alpha, Private Repos and GitHub URLs larger than 25 MB is not allowed.", + "DOWNLOAD_FAILED_MESSAGE": "Make sure the download URL is a valid.
    NB: As Phoenix is free, Private Repos and GitHub URLs larger than 25 MB is not allowed.", "PLEASE_SELECT_A_FOLDER": "Please select a folder...", "UNZIP_IN_PROGRESS": "Unzipping files...", "UNZIP_FAILED": "Error: Unzipping failed.",