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:
undergroundwires
2023-08-07 13:16:39 +02:00
parent 3a594ac7fd
commit 1b9be8fe2d
67 changed files with 2135 additions and 1267 deletions

View File

@@ -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">

View File

@@ -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">

View File

@@ -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) {

View File

@@ -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,