Skip to content

Commit

Permalink
Merge pull request #16 from salesforce/init_project
Browse files Browse the repository at this point in the history
Project management updates
  • Loading branch information
khawkins committed Jun 16, 2023
2 parents 298578f + 0ecbee3 commit 63698ff
Show file tree
Hide file tree
Showing 6 changed files with 500 additions and 56 deletions.
302 changes: 252 additions & 50 deletions src/commands/configureProjectCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,73 +5,275 @@
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
*/

import { QuickPickItem, QuickPickOptions, commands, window } from 'vscode';
import { Uri, WebviewPanel, commands, l10n, window } from 'vscode';
import * as process from 'process';
import { CommonUtils } from '@salesforce/lwc-dev-mobile-core/lib/common/CommonUtils';
import { InstructionsWebviewProvider } from '../webviews';

export class ConfigureProjectCommand {
static async configureProject(
fromWizard: boolean = false
): Promise<string> {
return new Promise(async (resolve, reject) => {
const header: QuickPickOptions = {
placeHolder: 'Create a new project, or open an existing project'
};
const items: QuickPickItem[] = [
{
label: 'Create New Project...',
description:
'Creates a new local project configured with the Offline Starter Kit'
},
{
label: 'Open Existing Project...',
description:
'Opens an existing local project configured with the Offline Starter Kit'
}
];
const selected = await window.showQuickPick(items, header);
if (!selected) {
return resolve('');
}
export type ProjectManagementChoiceAction = (panel?: WebviewPanel) => void;

export interface ProjectConfigurationProcessor {
getProjectManagementChoice(
createChoice: ProjectManagementChoiceAction,
openChoice: ProjectManagementChoiceAction
): void;
getProjectFolderPath(): Promise<Uri[] | undefined>;
preActionUserAcknowledgment(): Promise<void>;
}

if (selected.label === 'Create New Project...') {
const folderUri = await window.showOpenDialog({
openLabel: 'Select project folder',
class DefaultProjectConfigurationProcessor
implements ProjectConfigurationProcessor
{
extensionUri: Uri;
constructor(extensionUri: Uri) {
this.extensionUri = extensionUri;
}

async preActionUserAcknowledgment(): Promise<void> {
return new Promise((resolve) => {
new InstructionsWebviewProvider(
this.extensionUri
).showInstructionWebview(
l10n.t('Offline Starter Kit: Follow the Prompts'),
'src/instructions/projectBootstrapAcknowledgment.html',
[
{
buttonId: 'okButton',
action: async (panel) => {
panel.dispose();
return resolve();
}
}
]
);
});
}

async getProjectFolderPath(): Promise<Uri[] | undefined> {
return new Promise((resolve) => {
window
.showOpenDialog({
openLabel: l10n.t('Select project folder'),
canSelectFolders: true,
canSelectFiles: false,
canSelectMany: false
})
.then((result) => {
return resolve(result);
});
if (!folderUri || folderUri.length === 0) {
return resolve('');
});
}

getProjectManagementChoice(
createChoice: ProjectManagementChoiceAction,
openChoice: ProjectManagementChoiceAction
): void {
new InstructionsWebviewProvider(
this.extensionUri
).showInstructionWebview(
l10n.t('Offline Starter Kit: Create or Open Project'),
'src/instructions/projectBootstrapChoice.html',
[
{
buttonId: 'createNewButton',
action: (panel) => {
createChoice(panel);
}
},
{
buttonId: 'openExistingButton',
action: (panel) => {
openChoice(panel);
}
}
]
);
}
}

export class ConfigureProjectCommand {
static readonly STARTER_KIT_INITIAL_COMMIT =
'99b1fa9377694beb7918580aab445a2e9981f611';
static readonly STARTER_KIT_REPO_URI =
'https://github.com/salesforce/offline-app-developer-starter-kit.git';

extensionUri: Uri;
projectConfigurationProcessor: ProjectConfigurationProcessor;

constructor(
extensionUri: Uri,
projectConfigurationProcessor?: ProjectConfigurationProcessor
) {
this.extensionUri = extensionUri;
this.projectConfigurationProcessor =
projectConfigurationProcessor ??
new DefaultProjectConfigurationProcessor(extensionUri);
}

let infoMessage =
'Follow the prompts to configure the project.';
if (fromWizard) {
infoMessage +=
' NOTE: after the project is loaded, please be patient while the wizard resumes.';
async configureProject(): Promise<string | undefined> {
return new Promise(async (resolve) => {
this.projectConfigurationProcessor.getProjectManagementChoice(
(panel) => {
// It's actually important to run this async, because
// createNewProject() will not resolve its Promise
// until a path is selected, allowing the user to
// cancel the open dialog and re-initiate it as many
// times as they want.
this.createNewProject(panel).then((path) => {
return resolve(path);
});
},
(panel) => {
// See above for rationale for running this async.
this.openExistingProject(panel).then((path) => {
return resolve(path);
});
}
await window.showInformationMessage(infoMessage, {
title: 'OK'
);
});
}

async createNewProject(panel?: WebviewPanel): Promise<string> {
return new Promise(async (resolve, reject) => {
const folderUri =
await this.projectConfigurationProcessor.getProjectFolderPath();
if (!folderUri || folderUri.length === 0) {
// We explicitly do not want to resolve the Promise here, since the
// user "canceled", but could retry with the action request dialog
// that's still open. Only resolve the Promise when the user makes
// a choice.
return;
}

if (panel) {
panel.dispose();
}

this.projectConfigurationProcessor
.preActionUserAcknowledgment()
.then(async () => {
try {
const path = await this.executeProjectCreation(
folderUri[0]
);
return resolve(path);
} catch (error) {
return reject(error);
}
});
const githubRepoUri: string =
'https://github.com/salesforce/offline-app-developer-starter-kit.git';
try {
});
}

async openExistingProject(panel?: WebviewPanel): Promise<string> {
return new Promise(async (resolve) => {
const folderUri =
await this.projectConfigurationProcessor.getProjectFolderPath();
if (!folderUri || folderUri.length === 0) {
// We explicitly do not want to resolve the Promise here, since the
// user "canceled", but could retry with the action request dialog
// that's still open. Only resolve the Promise when the user makes
// a choice.
return;
}

try {
await this.validateProjectFolder(folderUri[0]);
} catch (error) {
window.showErrorMessage((error as Error).message);
// Same as above. If they chose an invalid folder, "soft"-error
// and allow them to pick a different choice.
return;
}

if (panel) {
panel.dispose();
}

this.projectConfigurationProcessor
.preActionUserAcknowledgment()
.then(async () => {
await commands.executeCommand(
'git.clone',
githubRepoUri,
folderUri[0].fsPath
'vscode.openFolder',
folderUri[0],
{ forceReuseWindow: true }
);
return resolve(folderUri[0].fsPath);
});
});
}

async executeProjectCreation(folderUri: Uri): Promise<string> {
return new Promise(async (resolve) => {
await commands.executeCommand(
'git.clone',
ConfigureProjectCommand.STARTER_KIT_REPO_URI,
folderUri.fsPath
);
return resolve(folderUri.fsPath);
});
}

async validateProjectFolder(projectFolderUri: Uri): Promise<void> {
return new Promise(async (resolve, reject) => {
const origWorkingDir = process.cwd();
try {
// Can we chdir to the selected folder?
try {
process.chdir(projectFolderUri.fsPath);
} catch (error) {
return reject(
new Error(
l10n.t(
"Could not access the project folder at '{0}'.",
projectFolderUri.fsPath
)
)
);
}

// Is git installed?
try {
// TODO: There are a number of complexities to solving
// for this in the general platform case.
// Cf. https://github.com/microsoft/vscode/blob/89ec834df20d597ff96f7d303e7e0f2f055d2a4e/extensions/git/src/git.ts#L145-L165
await CommonUtils.executeCommandAsync('git --version');
} catch (error) {
return reject(new Error(l10n.t('git is not installed.')));
}

// Is this a git repo?
try {
await CommonUtils.executeCommandAsync('git status');
} catch (error) {
window.showErrorMessage(`Failed to clone: ${error}`);
return reject(error);
return reject(
new Error(
l10n.t(
"Folder '{0}' does not contain a git repository.",
projectFolderUri.fsPath
)
)
);
}
} else if (selected.label === 'Open Existing Project...') {
console.log('Open existing project');
return resolve('');
} else {
return resolve('');

// Is this the Offline Starter Kit repo?
try {
await CommonUtils.executeCommandAsync(
`git merge-base HEAD ${ConfigureProjectCommand.STARTER_KIT_INITIAL_COMMIT}`
);
} catch (error) {
return reject(
new Error(
l10n.t(
"The git repository at '{0}' does not share history with the Offline Starter Kit.",
projectFolderUri.fsPath
)
)
);
}

return resolve();
} finally {
process.chdir(origWorkingDir);
}
});
}
Expand Down
7 changes: 4 additions & 3 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,10 @@ export function activate(context: vscode.ExtensionContext) {
'src/instructions/salesforcemobileapp.html'
);
} else {
const projectDir =
await ConfigureProjectCommand.configureProject(true);
if (projectDir === '') {
const projectDir = await new ConfigureProjectCommand(
context.extensionUri
).configureProject();
if (!projectDir) {
// No directory selected.
return Promise.resolve();
}
Expand Down
18 changes: 18 additions & 0 deletions src/instructions/projectBootstrapAcknowledgment.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Offline Starter Kit Project: Follow the Prompts</title>
</head>
<body>
<p>Follow the prompts to configure the project.</p>
<p>
<strong>NOTE:</strong>
after the project is loaded, please be patient while the wizard
resumes.
</p>
<button id="okButton">OK</button>
<script src="--- MESSAGING_SCRIPT_SRC ---"></script>
</body>
</html>
19 changes: 19 additions & 0 deletions src/instructions/projectBootstrapChoice.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Offline Starter Kit: Create or Open Project</title>
</head>
<body>
<h1>Welcome to the Offline Starter Kit Onboarding Wizard</h1>
<p>
In order to get started, please choose whether you would like to
create a new instance of the Offline Starter Kit project, or open an
existing instance of the project.
</p>
<button id="createNewButton">Create New Project</button>
<button id="openExistingButton">Open Existing Project</button>
<script src="--- MESSAGING_SCRIPT_SRC ---"></script>
</body>
</html>
Loading

0 comments on commit 63698ff

Please sign in to comment.