Improve manual execution instructions

- Simplify alternatives to run the script.
- Change style of code parts to be easier to the eye:
  - Use the same font size as other texts in body.
  - Add vertical padding.
  - Align the contents (the code, copy button and dollar sign) in the
    middle.
  - Align information icon to center of context next to it.
- Fix minor typos and punctations.
- Refactor instruction list to be more generic to able to be used by
  other operating systems.
- Make dialogs scrollable so instruction list on smaller screens can be
  read until the end.
This commit is contained in:
undergroundwires
2022-10-01 19:50:45 +02:00
parent 430537f704
commit 7d3670c26d
13 changed files with 470 additions and 140 deletions

View File

@@ -28,13 +28,14 @@ export default class Code extends Vue {
@use "@/presentation/assets/styles/main" as *;
.code-wrapper {
display:flex;
white-space: nowrap;
justify-content: space-between;
font-family: $font-normal;
background-color: $color-primary-darker;
color: $color-on-primary;
padding-left: 0.3rem;
padding-right: 0.3rem;
align-items: center;
padding: 0.2rem;
.dollar {
margin-right: 0.5rem;
font-size: 0.8rem;
@@ -48,7 +49,7 @@ export default class Code extends Vue {
}
}
code {
font-size: 1.2rem;
font-size: 1rem;
}
}
</style>

View File

@@ -0,0 +1,30 @@
import { OperatingSystem } from '@/domain/OperatingSystem';
import { IInstructionListData, IInstructionListStep } from '../InstructionListData';
export interface IInstructionsBuilderData {
readonly fileName: string;
}
export type InstructionStepBuilderType = (data: IInstructionsBuilderData) => IInstructionListStep;
export class InstructionsBuilder {
private readonly stepBuilders = new Array<InstructionStepBuilderType>();
constructor(private readonly os: OperatingSystem) {
}
public withStep(stepBuilder: InstructionStepBuilderType) {
if (!stepBuilder) { throw new Error('missing stepBuilder'); }
this.stepBuilders.push(stepBuilder);
return this;
}
public build(data: IInstructionsBuilderData): IInstructionListData {
if (!data) { throw new Error('missing data'); }
return {
operatingSystem: this.os,
steps: this.stepBuilders.map((stepBuilder) => stepBuilder(data)),
};
}
}

View File

@@ -0,0 +1,70 @@
import { OperatingSystem } from '@/domain/OperatingSystem';
import { InstructionsBuilder } from './InstructionsBuilder';
export class MacOsInstructionsBuilder extends InstructionsBuilder {
constructor() {
super(OperatingSystem.macOS);
super
.withStep(() => ({
action: {
instruction: 'Download the file.',
details: 'You should have already been prompted to save the script file.'
+ '<br/>If this was not the case or you did not save the script when prompted,'
+ '<br/>please try to download your script file again.'
,
},
}))
.withStep(() => ({
action: {
instruction: 'Open terminal.',
details: 'Type Terminal into Spotlight or open it from the Applications -> Utilities folder.',
},
}))
.withStep(() => ({
action: {
instruction: 'Navigate to the folder where you downloaded the file e.g.:',
},
code: {
instruction: 'cd ~/Downloads',
details: 'Press on <code>enter/return</code> key after running the command.'
+ '<br/>If the file is not downloaded on Downloads folder,'
+ '<br/>change <code>Downloads</code> to path where the file is downloaded.'
+ '<br/>'
+ '<br/>This command means:'
+ '<ul>'
+ '<li><code>cd</code> will change the current folder.</li>'
+ '<li><code>~</code> is the user home directory.</li>'
+ '</ul>',
},
}))
.withStep((data) => ({
action: {
instruction: 'Give the file execute permissions:',
},
code: {
instruction: `chmod +x ${data.fileName}`,
details: 'Press on <code>enter/return</code> key after running the command.<br/>'
+ 'It will make the file executable.'
,
},
}))
.withStep((data) => ({
action: {
instruction: 'Execute the file:',
},
code: {
instruction: `./${data.fileName}`,
details: 'Alternatively you can locate the file in <strong>Finder</strong> and double click on it.',
},
}))
.withStep(() => ({
action: {
instruction: 'If asked, enter your administrator password.',
details: 'As you type, your password will be hidden but the keys are still registered, so keep typing.'
+ '<br/>Press on <code>enter/return</code> key after typing your password.'
+ '<br/>Administrator privileges are required to configure OS.'
,
},
}));
}
}

View File

@@ -0,0 +1,113 @@
<template>
<div class="instructions">
<p>
You have two alternatives to apply your selection.
</p>
<hr />
<p>
<strong>1. The easy alternative</strong>. Run your script without any manual steps by
<a :href="this.macOsDownloadUrl">downloading desktop version</a> of {{ this.appName }} on the
{{ this.osName }} system you wish to configure, and then click on the Run button. This is
recommended for most users.
</p>
<hr />
<p>
<strong>2. The hard (manual) alternative</strong>. This requires you to do additional manual
steps. If you are unsure how to follow the instructions, hover on information
(<font-awesome-icon :icon="['fas', 'info-circle']" />)
icons near the steps, or follow the easy alternative described above.
</p>
<p>
<ol>
<li
v-for='(step, index) in this.data.steps'
v-bind:key="index"
class="step"
>
<div class="step__action">
<span>{{ step.action.instruction }}</span>
<font-awesome-icon
v-if="step.action.details"
class="explanation"
:icon="['fas', 'info-circle']"
v-tooltip.top-center="step.action.details"
/>
</div>
<div v-if="step.code" class="step__code">
<Code>{{ step.code.instruction }}</Code>
<font-awesome-icon
v-if="step.code.details"
class="explanation"
:icon="['fas', 'info-circle']"
v-tooltip.top-center="step.code.details"
/>
</div>
</li>
</ol>
</p>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import { OperatingSystem } from '@/domain/OperatingSystem';
import { ApplicationFactory } from '@/application/ApplicationFactory';
import Code from './Code.vue';
import { IInstructionListData } from './InstructionListData';
@Component({
components: {
Code,
},
})
export default class InstructionList extends Vue {
public appName = '';
public macOsDownloadUrl = '';
public osName = '';
@Prop() public data: IInstructionListData;
public async created() {
if (!this.data) {
throw new Error('missing data');
}
const app = await ApplicationFactory.Current.getApp();
this.appName = app.info.name;
this.macOsDownloadUrl = app.info.getDownloadUrl(OperatingSystem.macOS);
this.osName = renderOsName(this.data.operatingSystem);
}
}
function renderOsName(os: OperatingSystem): string {
switch (os) {
case OperatingSystem.Windows: return 'Windows';
case OperatingSystem.macOS: return 'macOS';
case OperatingSystem.Linux: return 'Linux';
default: throw new RangeError(`Cannot render os name: ${OperatingSystem[os]}`);
}
}
</script>
<style scoped lang="scss">
@use "@/presentation/assets/styles/main" as *;
.step {
margin: 10px 0;
&__action {
display: flex;
flex-direction: row;
align-items: center;
}
&__code {
display: flex;
flex-direction: row;
align-items: center;
margin-top: 0.5em;
}
}
.explanation {
margin-left: 0.5em;
}
</style>

View File

@@ -0,0 +1,16 @@
import { OperatingSystem } from '@/domain/OperatingSystem';
export interface IInstructionListData {
readonly operatingSystem: OperatingSystem;
readonly steps: readonly IInstructionListStep[];
}
export interface IInstructionListStep {
readonly action: IInstructionInfo;
readonly code?: IInstructionInfo;
}
export interface IInstructionInfo {
readonly instruction: string;
readonly details?: string;
}

View File

@@ -0,0 +1,21 @@
import { OperatingSystem } from '@/domain/OperatingSystem';
import { InstructionsBuilder } from './Data/InstructionsBuilder';
import { MacOsInstructionsBuilder } from './Data/MacOsInstructionsBuilder';
import { IInstructionListData } from './InstructionListData';
const builders = new Map<OperatingSystem, InstructionsBuilder>([
[OperatingSystem.macOS, new MacOsInstructionsBuilder()],
]);
export function hasInstructions(os: OperatingSystem) {
return builders.has(os);
}
export function getInstructions(
os: OperatingSystem,
fileName: string,
): IInstructionListData {
return builders
.get(os)
.build({ fileName });
}

View File

@@ -1,123 +0,0 @@
<template>
<div class="instructions">
<p>
Since you're using online version of {{ this.appName }}, you will need to do additional
steps after downloading the file to execute your script on macOS:
</p>
<p>
<ol>
<li>
<span>Download the file</span>
<font-awesome-icon
class="explanation"
:icon="['fas', 'info-circle']"
v-tooltip.top-center="
'You should be prompted to save the script file now'
+ ', otherwise try to download it again'"
/>
</li>
<li>
<span>Open terminal</span>
<font-awesome-icon
class="explanation"
:icon="['fas', 'info-circle']"
v-tooltip.top-center="
'Type Terminal into Spotlight or open from the Applications -> Utilities folder'"
/>
</li>
<li>
<span>Navigate to the folder where you downloaded the file e.g.:</span>
<div>
<Code>cd ~/Downloads</Code>
<font-awesome-icon
class="explanation"
:icon="['fas', 'info-circle']"
v-tooltip.top-center="
'Press on Enter/Return key after running the command.<br/>'
+ 'If the file is not downloaded on Downloads folder, change'
+ '`Downloads` to path where the file is downloaded.<br/>'
+ ' `cd` will change the current folder.<br/>'
+ ' `~` is the user home directory.'"
/>
</div>
</li>
<li>
<span>Give the file execute permissions:</span>
<div>
<Code>chmod +x {{ this.fileName }}</Code>
<font-awesome-icon
class="explanation"
:icon="['fas', 'info-circle']"
v-tooltip.top-center="
'Press on Enter/Return key after running the command.<br/>' +
'It will make the file executable.'"
/>
</div>
</li>
<li>
<span>Execute the file:</span>
<div>
<Code>./{{ this.fileName }}</Code>
<font-awesome-icon
class="explanation"
:icon="['fas', 'info-circle']"
v-tooltip.top-center="'Alternatively you can double click on the file'"
/>
</div>
</li>
<li>
<span>If asked, enter your administrator password</span>
<font-awesome-icon
class="explanation"
:icon="['fas', 'info-circle']"
v-tooltip.top-center="
'Press on Enter/Return key after typing your password<br/>' +
'Your password will not be shown by default.<br/>' +
'Administor privileges are required to configure OS.'"
/>
</li>
</ol>
</p>
<p>
Or download the <a :href="this.macOsDownloadUrl">offline version</a> to run your scripts
directly to skip these steps.
</p>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import { OperatingSystem } from '@/domain/OperatingSystem';
import { ApplicationFactory } from '@/application/ApplicationFactory';
import Code from './Code.vue';
@Component({
components: {
Code,
},
})
export default class MacOsInstructions extends Vue {
@Prop() public fileName: string;
public appName = '';
public macOsDownloadUrl = '';
public async created() {
const app = await ApplicationFactory.Current.getApp();
this.appName = app.info.name;
this.macOsDownloadUrl = app.info.getDownloadUrl(OperatingSystem.macOS);
}
}
</script>
<style scoped lang="scss">
@use "@/presentation/assets/styles/main" as *;
li {
margin: 10px 0;
}
.explanation {
margin-left: 0.5em;
}
</style>

View File

@@ -17,8 +17,8 @@
v-on:click="copyCode"
icon-prefix="fas" icon-name="copy">
</IconButton>
<Dialog v-if="this.isMacOsCollection" ref="instructionsDialog">
<MacOsInstructions :fileName="this.fileName" />
<Dialog v-if="this.hasInstructions" ref="instructionsDialog">
<InstructionList :data="this.instructions" />
</Dialog>
</div>
</template>
@@ -35,15 +35,18 @@ import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
import { IApplicationCode } from '@/application/Context/State/Code/IApplicationCode';
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
import { OperatingSystem } from '@/domain/OperatingSystem';
import { ICategoryCollection } from '@/domain/ICategoryCollection';
import { CodeRunner } from '@/infrastructure/CodeRunner';
import { IReadOnlyApplicationContext } from '@/application/Context/IApplicationContext';
import MacOsInstructions from './MacOsInstructions.vue';
import InstructionList from './Instructions/InstructionList.vue';
import IconButton from './IconButton.vue';
import { IInstructionListData } from './Instructions/InstructionListData';
import { getInstructions, hasInstructions } from './Instructions/InstructionListDataFactory';
@Component({
components: {
IconButton,
MacOsInstructions,
InstructionList,
Dialog,
},
})
@@ -54,7 +57,9 @@ export default class TheCodeButtons extends StatefulVue {
public hasCode = false;
public isMacOsCollection = false;
public instructions: IInstructionListData | undefined;
public hasInstructions = false;
public fileName = '';
@@ -66,7 +71,7 @@ export default class TheCodeButtons extends StatefulVue {
public async saveCode() {
const context = await this.getCurrentContext();
saveCode(this.fileName, context.state);
if (this.isMacOsCollection) {
if (this.hasInstructions) {
(this.$refs.instructionsDialog as Dialog).show();
}
}
@@ -77,11 +82,9 @@ export default class TheCodeButtons extends StatefulVue {
}
protected handleCollectionState(newState: IReadOnlyCategoryCollectionState): void {
const isNewOs = (test: OperatingSystem) => newState.collection.os === test;
this.canRun = this.isDesktopVersion && isNewOs(Environment.CurrentEnvironment.os);
this.isMacOsCollection = isNewOs(OperatingSystem.macOS);
this.fileName = buildFileName(newState.collection.scripting);
this.react(newState.code);
this.updateRunState(newState.os);
this.updateDownloadState(newState.collection);
this.updateCodeState(newState.code);
}
private async getCurrentCode(): Promise<IApplicationCode> {
@@ -90,7 +93,20 @@ export default class TheCodeButtons extends StatefulVue {
return code;
}
private async react(code: IApplicationCode) {
private updateRunState(selectedOs: OperatingSystem) {
const isRunningOnSelectedOs = selectedOs === Environment.CurrentEnvironment.os;
this.canRun = this.isDesktopVersion && isRunningOnSelectedOs;
}
private updateDownloadState(collection: ICategoryCollection) {
this.fileName = buildFileName(collection.scripting);
this.hasInstructions = hasInstructions(collection.os);
if (this.hasInstructions) {
this.instructions = getInstructions(collection.os, this.fileName);
}
}
private updateCodeState(code: IApplicationCode) {
this.hasCode = code.current && code.current.length > 0;
this.events.unsubscribeAll();
this.events.register(code.changed.on((newCode) => {
@@ -131,7 +147,6 @@ async function executeCode(context: IReadOnlyApplicationContext) {
/* fileExtension: */ context.state.collection.scripting.fileExtension,
);
}
</script>
<style scoped lang="scss">