Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Project management updates #16

Merged
merged 7 commits into from
Jun 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

// 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;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reject() here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We explicitly don't want to reject. This is what allows for the "recoverable" case: the Promise will not be fulfilled one way or another, until a proper value is returned, allowing the user to cancel the folder picker any number of times and still continue the workflow.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm adding comments to that effect in the relevant areas. It's a good place to add clarity.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, awesome! That makes sense now. Thanks!

}

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;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reject()?

}

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>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use tags here? ;)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tags?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ugggh, I was making a joke about using blink tags, but git removed the <blink/>. Sorry!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haha you know I had a feeling that's what you meant! But I was like, "But tags? ¯\(ツ)/¯ " We were on the same page all along.

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