Refactor Vue components using Composition API #230
- Migrate `StatefulVue`:
- Introduce `UseCollectionState` that replaces its behavior and acts
as a shared state store.
- Add more encapsulated, granular functions based on read or write
access to state in CollectionState.
- Some linting rules get activates due to new code-base compability to
modern parses, fix linting errors.
- Rename Dialog to ModalDialog as after refactoring,
eslintvue/no-reserved-component-names does not allow name Dialog.
- To comply with `vue/multi-word-component-names`, rename:
- `Code` -> `CodeInstruction`
- `Handle` -> `SliderHandle`
- `Documentable` -> `DocumentableNode`
- `Node` -> `NodeContent`
- `INode` -> `INodeContent`
- `Responsive` -> `SizeObserver`
- Remove `vue-property-decorator` and `vue-class-component`
dependencies.
- Refactor `watch` with computed properties when possible for cleaner
code.
- Introduce `UseApplication` to reduce repeated code in new components
that use `computed` more heavily than before.
- Change TypeScript target to `es2017` to allow top level async calls
for getting application context/state/instance to simplify the code by
removing async calls. However, mocha (unit and integration) tests do
not run with top level awaits, so a workaround is used.
This commit is contained in:
@@ -14,20 +14,36 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {
|
||||
Component, Prop, Emit, Vue,
|
||||
} from 'vue-property-decorator';
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
@Component
|
||||
export default class IconButton extends Vue {
|
||||
@Prop() public text!: number;
|
||||
export default defineComponent({
|
||||
props: {
|
||||
text: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
iconPrefix: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
iconName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: [
|
||||
'click',
|
||||
],
|
||||
setup(_, { emit }) {
|
||||
function onClicked() {
|
||||
emit('click');
|
||||
}
|
||||
|
||||
@Prop() public iconPrefix!: string;
|
||||
|
||||
@Prop() public iconName!: string;
|
||||
|
||||
@Emit('click') public onClicked() { /* do nothing except firing event */ }
|
||||
}
|
||||
return {
|
||||
onClicked,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -12,16 +12,23 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
import { defineComponent, useSlots } from 'vue';
|
||||
import { Clipboard } from '@/infrastructure/Clipboard';
|
||||
|
||||
@Component
|
||||
export default class Code extends Vue {
|
||||
public copyCode(): void {
|
||||
const code = this.$slots.default[0].text;
|
||||
Clipboard.copyText(code);
|
||||
}
|
||||
}
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
const slots = useSlots();
|
||||
|
||||
function copyCode() {
|
||||
const code = slots.default()[0].text;
|
||||
Clipboard.copyText(code);
|
||||
}
|
||||
|
||||
return {
|
||||
copyCode,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -6,8 +6,8 @@
|
||||
<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
|
||||
<a :href="macOsDownloadUrl">downloading desktop version</a> of {{ appName }} on the
|
||||
{{ osName }} system you wish to configure, and then click on the Run button. This is
|
||||
recommended for most users.
|
||||
</p>
|
||||
<hr />
|
||||
@@ -20,7 +20,7 @@
|
||||
<p>
|
||||
<ol>
|
||||
<li
|
||||
v-for='(step, index) in this.data.steps'
|
||||
v-for='(step, index) in data.steps'
|
||||
v-bind:key="index"
|
||||
class="step"
|
||||
>
|
||||
@@ -34,7 +34,7 @@
|
||||
/>
|
||||
</div>
|
||||
<div v-if="step.code" class="step__code">
|
||||
<Code>{{ step.code.instruction }}</Code>
|
||||
<CodeInstruction>{{ step.code.instruction }}</CodeInstruction>
|
||||
<font-awesome-icon
|
||||
v-if="step.code.details"
|
||||
class="explanation"
|
||||
@@ -49,36 +49,47 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
import {
|
||||
defineComponent, PropType, computed,
|
||||
} from 'vue';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { ApplicationFactory } from '@/application/ApplicationFactory';
|
||||
import Code from './Code.vue';
|
||||
import { useApplication } from '@/presentation/components/Shared/Hooks/UseApplication';
|
||||
import CodeInstruction from './CodeInstruction.vue';
|
||||
import { IInstructionListData } from './InstructionListData';
|
||||
|
||||
@Component({
|
||||
export default defineComponent({
|
||||
components: {
|
||||
Code,
|
||||
CodeInstruction,
|
||||
},
|
||||
})
|
||||
export default class InstructionList extends Vue {
|
||||
public appName = '';
|
||||
props: {
|
||||
data: {
|
||||
type: Object as PropType<IInstructionListData>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const { info } = useApplication();
|
||||
|
||||
public macOsDownloadUrl = '';
|
||||
const appName = computed<string>(() => info.name);
|
||||
|
||||
public osName = '';
|
||||
const macOsDownloadUrl = computed<string>(
|
||||
() => info.getDownloadUrl(OperatingSystem.macOS),
|
||||
);
|
||||
|
||||
@Prop() public data: IInstructionListData;
|
||||
const osName = computed<string>(() => {
|
||||
if (!props.data) {
|
||||
throw new Error('missing data');
|
||||
}
|
||||
return renderOsName(props.data.operatingSystem);
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
return {
|
||||
appName,
|
||||
macOsDownloadUrl,
|
||||
osName,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
function renderOsName(os: OperatingSystem): string {
|
||||
switch (os) {
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
<template>
|
||||
<div class="container" v-if="hasCode">
|
||||
<IconButton
|
||||
v-if="this.canRun"
|
||||
v-if="canRun"
|
||||
text="Run"
|
||||
v-on:click="executeCode"
|
||||
icon-prefix="fas"
|
||||
icon-name="play"
|
||||
/>
|
||||
<IconButton
|
||||
:text="this.isDesktopVersion ? 'Save' : 'Download'"
|
||||
:text="isDesktopVersion ? 'Save' : 'Download'"
|
||||
v-on:click="saveCode"
|
||||
icon-prefix="fas"
|
||||
:icon-name="this.isDesktopVersion ? 'save' : 'file-download'"
|
||||
:icon-name="isDesktopVersion ? 'save' : 'file-download'"
|
||||
/>
|
||||
<IconButton
|
||||
text="Copy"
|
||||
@@ -19,25 +19,24 @@
|
||||
icon-prefix="fas"
|
||||
icon-name="copy"
|
||||
/>
|
||||
<Dialog v-if="this.hasInstructions" ref="instructionsDialog">
|
||||
<InstructionList :data="this.instructions" />
|
||||
</Dialog>
|
||||
<ModalDialog v-if="instructions" ref="instructionsDialog">
|
||||
<InstructionList :data="instructions" />
|
||||
</ModalDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component } from 'vue-property-decorator';
|
||||
import { StatefulVue } from '@/presentation/components/Shared/StatefulVue';
|
||||
import { defineComponent, ref, computed } from 'vue';
|
||||
import { useCollectionState } from '@/presentation/components/Shared/Hooks/UseCollectionState';
|
||||
import { SaveFileDialog, FileType } from '@/infrastructure/SaveFileDialog';
|
||||
import { Clipboard } from '@/infrastructure/Clipboard';
|
||||
import Dialog from '@/presentation/components/Shared/Dialog.vue';
|
||||
import ModalDialog from '@/presentation/components/Shared/ModalDialog.vue';
|
||||
import { Environment } from '@/application/Environment/Environment';
|
||||
import { IReadOnlyCategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||
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 InstructionList from './Instructions/InstructionList.vue';
|
||||
@@ -45,79 +44,89 @@ import IconButton from './IconButton.vue';
|
||||
import { IInstructionListData } from './Instructions/InstructionListData';
|
||||
import { getInstructions, hasInstructions } from './Instructions/InstructionListDataFactory';
|
||||
|
||||
@Component({
|
||||
const isDesktopVersion = Environment.CurrentEnvironment.isDesktop;
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
IconButton,
|
||||
InstructionList,
|
||||
Dialog,
|
||||
ModalDialog,
|
||||
},
|
||||
})
|
||||
export default class TheCodeButtons extends StatefulVue {
|
||||
public readonly isDesktopVersion = Environment.CurrentEnvironment.isDesktop;
|
||||
setup() {
|
||||
const {
|
||||
currentState, currentContext, onStateChange, events,
|
||||
} = useCollectionState();
|
||||
|
||||
public canRun = false;
|
||||
const instructionsDialog = ref<typeof ModalDialog>();
|
||||
const canRun = computed<boolean>(() => getCanRunState(currentState.value.os));
|
||||
const fileName = computed<string>(() => buildFileName(currentState.value.collection.scripting));
|
||||
const hasCode = ref(false);
|
||||
const instructions = computed<IInstructionListData | undefined>(() => getDownloadInstructions(
|
||||
currentState.value.collection.os,
|
||||
fileName.value,
|
||||
));
|
||||
|
||||
public hasCode = false;
|
||||
|
||||
public instructions: IInstructionListData | undefined;
|
||||
|
||||
public hasInstructions = false;
|
||||
|
||||
public fileName = '';
|
||||
|
||||
public async copyCode() {
|
||||
const code = await this.getCurrentCode();
|
||||
Clipboard.copyText(code.current);
|
||||
}
|
||||
|
||||
public async saveCode() {
|
||||
const context = await this.getCurrentContext();
|
||||
saveCode(this.fileName, context.state);
|
||||
if (this.hasInstructions) {
|
||||
(this.$refs.instructionsDialog as Dialog).show();
|
||||
async function copyCode() {
|
||||
const code = await getCurrentCode();
|
||||
Clipboard.copyText(code.current);
|
||||
}
|
||||
}
|
||||
|
||||
public async executeCode() {
|
||||
const context = await this.getCurrentContext();
|
||||
await executeCode(context);
|
||||
}
|
||||
|
||||
protected handleCollectionState(newState: IReadOnlyCategoryCollectionState): void {
|
||||
this.updateRunState(newState.os);
|
||||
this.updateDownloadState(newState.collection);
|
||||
this.updateCodeState(newState.code);
|
||||
}
|
||||
|
||||
private async getCurrentCode(): Promise<IApplicationCode> {
|
||||
const context = await this.getCurrentContext();
|
||||
const { code } = context.state;
|
||||
return code;
|
||||
}
|
||||
|
||||
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);
|
||||
function saveCode() {
|
||||
saveCodeToDisk(fileName.value, currentState.value);
|
||||
instructionsDialog.value?.show();
|
||||
}
|
||||
}
|
||||
|
||||
private updateCodeState(code: IApplicationCode) {
|
||||
this.hasCode = code.current && code.current.length > 0;
|
||||
this.events.unsubscribeAll();
|
||||
this.events.register(code.changed.on((newCode) => {
|
||||
this.hasCode = newCode && newCode.code.length > 0;
|
||||
}));
|
||||
async function executeCode() {
|
||||
await runCode(currentContext);
|
||||
}
|
||||
|
||||
onStateChange((newState) => {
|
||||
subscribeToCodeChanges(newState.code);
|
||||
}, { immediate: true });
|
||||
|
||||
function subscribeToCodeChanges(code: IApplicationCode) {
|
||||
hasCode.value = code.current && code.current.length > 0;
|
||||
events.unsubscribeAll();
|
||||
events.register(code.changed.on((newCode) => {
|
||||
hasCode.value = newCode && newCode.code.length > 0;
|
||||
}));
|
||||
}
|
||||
|
||||
async function getCurrentCode(): Promise<IApplicationCode> {
|
||||
const { code } = currentContext.state;
|
||||
return code;
|
||||
}
|
||||
|
||||
return {
|
||||
isDesktopVersion,
|
||||
canRun,
|
||||
hasCode,
|
||||
instructions,
|
||||
fileName,
|
||||
instructionsDialog,
|
||||
copyCode,
|
||||
saveCode,
|
||||
executeCode,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
function getDownloadInstructions(
|
||||
os: OperatingSystem,
|
||||
fileName: string,
|
||||
): IInstructionListData | undefined {
|
||||
if (!hasInstructions(os)) {
|
||||
return undefined;
|
||||
}
|
||||
return getInstructions(os, fileName);
|
||||
}
|
||||
|
||||
function saveCode(fileName: string, state: IReadOnlyCategoryCollectionState) {
|
||||
function getCanRunState(selectedOs: OperatingSystem): boolean {
|
||||
const isRunningOnSelectedOs = selectedOs === Environment.CurrentEnvironment.os;
|
||||
return isDesktopVersion && isRunningOnSelectedOs;
|
||||
}
|
||||
|
||||
function saveCodeToDisk(fileName: string, state: IReadOnlyCategoryCollectionState) {
|
||||
const content = state.code.current;
|
||||
const type = getType(state.collection.scripting.language);
|
||||
SaveFileDialog.saveFile(content, fileName, type);
|
||||
@@ -141,7 +150,7 @@ function buildFileName(scripting: IScriptingDefinition) {
|
||||
return fileName;
|
||||
}
|
||||
|
||||
async function executeCode(context: IReadOnlyApplicationContext) {
|
||||
async function runCode(context: IReadOnlyApplicationContext) {
|
||||
const runner = new CodeRunner();
|
||||
await runner.runCode(
|
||||
/* code: */ context.state.code.current,
|
||||
|
||||
Reference in New Issue
Block a user