Refactor to enforce strictNullChecks
This commit applies `strictNullChecks` to the entire codebase to improve maintainability and type safety. Key changes include: - Remove some explicit null-checks where unnecessary. - Add necessary null-checks. - Refactor static factory functions for a more functional approach. - Improve some test names and contexts for better debugging. - Add unit tests for any additional logic introduced. - Refactor `createPositionFromRegexFullMatch` to its own function as the logic is reused. - Prefer `find` prefix on functions that may return `undefined` and `get` prefix for those that always return a value.
This commit is contained in:
@@ -7,22 +7,18 @@
|
||||
<TheCodeButtons class="app__row app__code-buttons" />
|
||||
<TheFooter />
|
||||
</div>
|
||||
<OptionalDevToolkit />
|
||||
<component v-if="devToolkitComponent" :is="devToolkitComponent" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineAsyncComponent, defineComponent } from 'vue';
|
||||
import { defineAsyncComponent, defineComponent, Component } from 'vue';
|
||||
import TheHeader from '@/presentation/components/TheHeader.vue';
|
||||
import TheFooter from '@/presentation/components/TheFooter/TheFooter.vue';
|
||||
import TheCodeButtons from '@/presentation/components/Code/CodeButtons/TheCodeButtons.vue';
|
||||
import TheScriptArea from '@/presentation/components/Scripts/TheScriptArea.vue';
|
||||
import TheSearchBar from '@/presentation/components/TheSearchBar.vue';
|
||||
|
||||
const OptionalDevToolkit = process.env.NODE_ENV !== 'production'
|
||||
? defineAsyncComponent(() => import('@/presentation/components/DevToolkit/DevToolkit.vue'))
|
||||
: null;
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
TheHeader,
|
||||
@@ -30,10 +26,23 @@ export default defineComponent({
|
||||
TheScriptArea,
|
||||
TheSearchBar,
|
||||
TheFooter,
|
||||
OptionalDevToolkit,
|
||||
},
|
||||
setup() { },
|
||||
setup() {
|
||||
const devToolkitComponent = getOptionalDevToolkitComponent();
|
||||
|
||||
return {
|
||||
devToolkitComponent,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
function getOptionalDevToolkitComponent(): Component | undefined {
|
||||
const isDevelopment = process.env.NODE_ENV !== 'production';
|
||||
if (!isDevelopment) {
|
||||
return undefined;
|
||||
}
|
||||
return defineAsyncComponent(() => import('@/presentation/components/DevToolkit/DevToolkit.vue'));
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -40,7 +40,7 @@ export default defineComponent({
|
||||
function getCanRunState(
|
||||
selectedOs: OperatingSystem,
|
||||
isDesktopVersion: boolean,
|
||||
hostOs: OperatingSystem,
|
||||
hostOs: OperatingSystem | undefined,
|
||||
): boolean {
|
||||
const isRunningOnSelectedOs = selectedOs === hostOs;
|
||||
return isDesktopVersion && isRunningOnSelectedOs;
|
||||
|
||||
@@ -21,11 +21,10 @@ import ModalDialog from '@/presentation/components/Shared/Modal/ModalDialog.vue'
|
||||
import { IReadOnlyCategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import IconButton from '../IconButton.vue';
|
||||
import InstructionList from './Instructions/InstructionList.vue';
|
||||
import { IInstructionListData } from './Instructions/InstructionListData';
|
||||
import { getInstructions, hasInstructions } from './Instructions/InstructionListDataFactory';
|
||||
import { getInstructions } from './Instructions/InstructionListDataFactory';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
@@ -39,7 +38,7 @@ export default defineComponent({
|
||||
|
||||
const areInstructionsVisible = ref(false);
|
||||
const fileName = computed<string>(() => buildFileName(currentState.value.collection.scripting));
|
||||
const instructions = computed<IInstructionListData | undefined>(() => getDownloadInstructions(
|
||||
const instructions = computed<IInstructionListData | undefined>(() => getInstructions(
|
||||
currentState.value.collection.os,
|
||||
fileName.value,
|
||||
));
|
||||
@@ -59,16 +58,6 @@ export default defineComponent({
|
||||
},
|
||||
});
|
||||
|
||||
function getDownloadInstructions(
|
||||
os: OperatingSystem,
|
||||
fileName: string,
|
||||
): IInstructionListData | undefined {
|
||||
if (!hasInstructions(os)) {
|
||||
return undefined;
|
||||
}
|
||||
return getInstructions(os, fileName);
|
||||
}
|
||||
|
||||
function saveCodeToDisk(fileName: string, state: IReadOnlyCategoryCollectionState) {
|
||||
const content = state.code.current;
|
||||
const type = getType(state.collection.scripting.language);
|
||||
|
||||
@@ -15,13 +15,11 @@ export class InstructionsBuilder {
|
||||
}
|
||||
|
||||
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)),
|
||||
|
||||
@@ -86,12 +86,9 @@ export default defineComponent({
|
||||
() => info.getDownloadUrl(OperatingSystem.macOS),
|
||||
);
|
||||
|
||||
const osName = computed<string>(() => {
|
||||
if (!props.data) {
|
||||
throw new Error('missing data');
|
||||
}
|
||||
return renderOsName(props.data.operatingSystem);
|
||||
});
|
||||
const osName = computed<string>(
|
||||
() => renderOsName(props.data.operatingSystem),
|
||||
);
|
||||
|
||||
return {
|
||||
appName,
|
||||
|
||||
@@ -9,15 +9,11 @@ const builders = new Map<OperatingSystem, InstructionsBuilder>([
|
||||
[OperatingSystem.Linux, new LinuxInstructionsBuilder()],
|
||||
]);
|
||||
|
||||
export function hasInstructions(os: OperatingSystem) {
|
||||
return builders.has(os);
|
||||
}
|
||||
|
||||
export function getInstructions(
|
||||
os: OperatingSystem,
|
||||
fileName: string,
|
||||
): IInstructionListData {
|
||||
): IInstructionListData | undefined {
|
||||
return builders
|
||||
.get(os)
|
||||
.build({ fileName });
|
||||
?.build({ fileName });
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ export default defineComponent({
|
||||
|
||||
function updateCode(code: string, language: ScriptingLanguage) {
|
||||
const innerCode = code || getDefaultCode(language);
|
||||
editor.setValue(innerCode, 1);
|
||||
editor?.setValue(innerCode, 1);
|
||||
}
|
||||
|
||||
function handleCodeChange(event: ICodeChangedEvent) {
|
||||
@@ -96,7 +96,7 @@ export default defineComponent({
|
||||
if (!currentMarkerId) {
|
||||
return;
|
||||
}
|
||||
editor.session.removeMarker(currentMarkerId);
|
||||
editor?.session.removeMarker(currentMarkerId);
|
||||
currentMarkerId = undefined;
|
||||
}
|
||||
|
||||
@@ -115,7 +115,7 @@ export default defineComponent({
|
||||
|
||||
function highlight(startRow: number, endRow: number) {
|
||||
const AceRange = ace.require('ace/range').Range;
|
||||
currentMarkerId = editor.session.addMarker(
|
||||
currentMarkerId = editor?.session.addMarker(
|
||||
new AceRange(startRow, 0, endRow, 0),
|
||||
'code-area__highlight',
|
||||
'fullLine',
|
||||
@@ -123,8 +123,11 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
function scrollToLine(row: number) {
|
||||
const column = editor.session.getLine(row).length;
|
||||
editor.gotoLine(row, column, true);
|
||||
const column = editor?.session.getLine(row).length;
|
||||
if (column === undefined) {
|
||||
return;
|
||||
}
|
||||
editor?.gotoLine(row, column, true);
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -15,9 +15,12 @@ export enum SelectionType {
|
||||
|
||||
export function setCurrentSelectionType(type: SelectionType, context: SelectionMutationContext) {
|
||||
if (type === SelectionType.Custom) {
|
||||
throw new Error('cannot select custom type');
|
||||
throw new Error('Cannot select custom type.');
|
||||
}
|
||||
const selector = selectors.get(type);
|
||||
if (!selector) {
|
||||
throw new Error(`Cannot handle the type: ${SelectionType[type]}`);
|
||||
}
|
||||
selector.select(context);
|
||||
}
|
||||
|
||||
|
||||
@@ -31,14 +31,14 @@ export default defineComponent({
|
||||
const { modifyCurrentContext, currentState } = injectKey((keys) => keys.useCollectionState);
|
||||
const { application } = injectKey((keys) => keys.useApplication);
|
||||
|
||||
const allOses = computed<ReadonlyArray<IOsViewModel>>(() => (
|
||||
application.getSupportedOsList() ?? [])
|
||||
.map((os) : IOsViewModel => (
|
||||
{
|
||||
const allOses = computed<ReadonlyArray<IOsViewModel>>(
|
||||
() => application
|
||||
.getSupportedOsList()
|
||||
.map((os) : IOsViewModel => ({
|
||||
os,
|
||||
name: renderOsName(os),
|
||||
}
|
||||
)));
|
||||
})),
|
||||
);
|
||||
|
||||
const currentOs = computed<OperatingSystem>(() => {
|
||||
return currentState.value.os;
|
||||
|
||||
@@ -48,8 +48,12 @@ export default defineComponent({
|
||||
const firstElement = shallowRef<HTMLElement>();
|
||||
|
||||
function onResize(displacementX: number): void {
|
||||
const leftWidth = firstElement.value.offsetWidth + displacementX;
|
||||
firstElement.value.style.width = `${leftWidth}px`;
|
||||
const element = firstElement.value;
|
||||
if (!element) {
|
||||
throw new Error('The element reference ref is not correctly assigned to a DOM element.');
|
||||
}
|
||||
const leftWidth = element.offsetWidth + displacementX;
|
||||
element.style.width = `${leftWidth}px`;
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -29,7 +29,10 @@ export default defineComponent({
|
||||
const cursorCssValue = 'ew-resize';
|
||||
let initialX: number | undefined;
|
||||
|
||||
const resize = (event) => {
|
||||
const resize = (event: MouseEvent) => {
|
||||
if (initialX === undefined) {
|
||||
throw new Error('Resize action started without an initial X coordinate.');
|
||||
}
|
||||
const displacementX = event.clientX - initialX;
|
||||
emit('resized', displacementX);
|
||||
initialX = event.clientX;
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
</div>
|
||||
-->
|
||||
<div
|
||||
v-if="categoryIds != null && categoryIds.length > 0"
|
||||
v-if="categoryIds.length > 0"
|
||||
class="cards"
|
||||
>
|
||||
<CardListItem
|
||||
@@ -50,8 +50,9 @@ export default defineComponent({
|
||||
const { currentState, onStateChange } = injectKey((keys) => keys.useCollectionState);
|
||||
|
||||
const width = ref<number>(0);
|
||||
const categoryIds = computed<ReadonlyArray<number>>(() => currentState
|
||||
.value.collection.actions.map((category) => category.id));
|
||||
const categoryIds = computed<readonly number[]>(
|
||||
() => currentState.value.collection.actions.map((category) => category.id),
|
||||
);
|
||||
const activeCategoryId = ref<number | undefined>(undefined);
|
||||
|
||||
function onSelected(categoryId: number, isExpanded: boolean) {
|
||||
|
||||
@@ -89,12 +89,9 @@ export default defineComponent({
|
||||
|
||||
const cardElement = shallowRef<HTMLElement>();
|
||||
|
||||
const cardTitle = computed<string | undefined>(() => {
|
||||
if (!props.categoryId || !currentState.value) {
|
||||
return undefined;
|
||||
}
|
||||
const category = currentState.value.collection.findCategory(props.categoryId);
|
||||
return category?.name;
|
||||
const cardTitle = computed<string>(() => {
|
||||
const category = currentState.value.collection.getCategory(props.categoryId);
|
||||
return category.name;
|
||||
});
|
||||
|
||||
function collapse() {
|
||||
@@ -102,8 +99,12 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
async function scrollToCard() {
|
||||
const card = cardElement.value;
|
||||
if (!card) {
|
||||
throw new Error('Card is not found');
|
||||
}
|
||||
await sleep(400); // wait a bit to allow GUI to render the expanded card
|
||||
cardElement.value.scrollIntoView({ behavior: 'smooth' });
|
||||
card.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -34,7 +34,7 @@ export default defineComponent({
|
||||
const currentCollection = computed<ICategoryCollection>(() => currentState.value.collection);
|
||||
|
||||
const currentCategory = computed<ICategory>(
|
||||
() => currentCollection.value.findCategory(props.categoryId),
|
||||
() => currentCollection.value.getCategory(props.categoryId),
|
||||
);
|
||||
|
||||
const isAnyChildSelected = computed<boolean>(
|
||||
|
||||
@@ -71,6 +71,9 @@ export default defineComponent({
|
||||
const searchHasMatches = ref(false);
|
||||
const trimmedSearchQuery = computed(() => {
|
||||
const query = searchQuery.value;
|
||||
if (!query) {
|
||||
return '';
|
||||
}
|
||||
const threshold = 30;
|
||||
if (query.length <= threshold - 3) {
|
||||
return query;
|
||||
@@ -94,7 +97,7 @@ export default defineComponent({
|
||||
|
||||
function updateFromInitialFilter(filter?: IFilterResult) {
|
||||
searchQuery.value = filter?.query;
|
||||
searchHasMatches.value = filter?.hasAnyMatches();
|
||||
searchHasMatches.value = filter?.hasAnyMatches() ?? false;
|
||||
}
|
||||
|
||||
function subscribeToFilterChanges(filter: IReadOnlyUserFilter) {
|
||||
|
||||
@@ -42,7 +42,7 @@ function renderText(docs: readonly string[] | undefined): string {
|
||||
}
|
||||
|
||||
function renderAsMarkdownListItem(content: string): string {
|
||||
if (!content || content.length === 0) {
|
||||
if (content.length === 0) {
|
||||
throw new Error('missing content');
|
||||
}
|
||||
const lines = content.split(/\r\n|\r|\n/);
|
||||
|
||||
@@ -85,7 +85,8 @@ function removeTrailingExtension(value: string): string {
|
||||
if (parts.length === 1) {
|
||||
return value;
|
||||
}
|
||||
if (parts.at(-1).length > 9) {
|
||||
const extension = parts[parts.length - 1];
|
||||
if (extension.length > 9) {
|
||||
return value; // Heuristically web file extension is no longer than 9 chars (e.g. "html")
|
||||
}
|
||||
return parts.slice(0, -1).join('.');
|
||||
@@ -115,9 +116,8 @@ function selectMostDescriptiveName(parts: readonly string[]): string | undefined
|
||||
}
|
||||
|
||||
function isGoodPathPart(part: string): boolean {
|
||||
return part
|
||||
return part.length > 2 // This is often non-human categories like T5 etc.
|
||||
&& !isDigit(part) // E.g. article numbers, issue numbers
|
||||
&& part.length > 2 // This is often non-human categories like T5 etc.
|
||||
&& !/^index(?:\.\S{0,10}$|$)/.test(part) // E.g. index.html
|
||||
&& !/^[A-Za-z]{2,4}([_-][A-Za-z]{4})?([_-]([A-Za-z]{2}|[0-9]{3}))$/.test(part) // Locale string e.g. fr-FR
|
||||
&& !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(part) // GUID
|
||||
|
||||
@@ -8,6 +8,6 @@ export interface NodeMetadata {
|
||||
readonly text: string;
|
||||
readonly isReversible: boolean;
|
||||
readonly docs: ReadonlyArray<string>;
|
||||
readonly children?: ReadonlyArray<NodeMetadata>;
|
||||
readonly children: ReadonlyArray<NodeMetadata>;
|
||||
readonly type: NodeType;
|
||||
}
|
||||
|
||||
@@ -25,10 +25,7 @@ export class CategoryReverter implements IReverter {
|
||||
}
|
||||
|
||||
function getAllSubScriptReverters(categoryId: number, collection: ICategoryCollection) {
|
||||
const category = collection.findCategory(categoryId);
|
||||
if (!category) {
|
||||
throw new Error(`Category with id "${categoryId}" does not exist`);
|
||||
}
|
||||
const category = collection.getCategory(categoryId);
|
||||
const scripts = category.getAllScriptsRecursively();
|
||||
return scripts.map((script) => new ScriptReverter(script.id));
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ import { useSelectedScriptNodeIds } from './TreeViewAdapter/UseSelectedScriptNod
|
||||
export default defineComponent({
|
||||
props: {
|
||||
categoryId: {
|
||||
type: [Number, undefined],
|
||||
type: [Number],
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import type { ReadOnlyTreeNode } from '../Node/TreeNode';
|
||||
|
||||
export interface TreeViewFilterEvent {
|
||||
readonly action: TreeViewFilterAction;
|
||||
readonly predicate?: TreeViewFilterPredicate;
|
||||
}
|
||||
type TreeViewFilterTriggeredEvent = {
|
||||
readonly action: TreeViewFilterAction.Triggered;
|
||||
readonly predicate: TreeViewFilterPredicate;
|
||||
};
|
||||
|
||||
type TreeViewFilterRemovedEvent = {
|
||||
readonly action: TreeViewFilterAction.Removed;
|
||||
};
|
||||
|
||||
export type TreeViewFilterEvent = TreeViewFilterTriggeredEvent | TreeViewFilterRemovedEvent;
|
||||
|
||||
export enum TreeViewFilterAction {
|
||||
Triggered,
|
||||
@@ -14,14 +20,14 @@ export type TreeViewFilterPredicate = (node: ReadOnlyTreeNode) => boolean;
|
||||
|
||||
export function createFilterTriggeredEvent(
|
||||
predicate: TreeViewFilterPredicate,
|
||||
): TreeViewFilterEvent {
|
||||
): TreeViewFilterTriggeredEvent {
|
||||
return {
|
||||
action: TreeViewFilterAction.Triggered,
|
||||
predicate,
|
||||
};
|
||||
}
|
||||
|
||||
export function createFilterRemovedEvent(): TreeViewFilterEvent {
|
||||
export function createFilterRemovedEvent(): TreeViewFilterRemovedEvent {
|
||||
return {
|
||||
action: TreeViewFilterAction.Removed,
|
||||
};
|
||||
|
||||
@@ -25,7 +25,7 @@ export class TreeNodeHierarchy implements HierarchyAccess {
|
||||
this.children = children;
|
||||
}
|
||||
|
||||
public setParent(parent: TreeNode): void {
|
||||
public setParent(parent: TreeNode | undefined): void {
|
||||
this.parent = parent;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,6 @@ import { TreeNodeManager } from '../../Node/TreeNodeManager';
|
||||
export function parseTreeInput(
|
||||
input: readonly TreeInputNodeData[],
|
||||
): TreeNode[] {
|
||||
if (!input) {
|
||||
throw new Error('missing input');
|
||||
}
|
||||
if (!Array.isArray(input)) {
|
||||
throw new Error('input data must be an array');
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ export class TreeNodeInitializerAndUpdater implements TreeNodeCollection {
|
||||
public nodesUpdated = new EventSource<QueryableNodes>();
|
||||
|
||||
public updateRootNodes(rootNodesData: readonly TreeInputNodeData[]): void {
|
||||
if (!rootNodesData?.length) {
|
||||
if (!rootNodesData.length) {
|
||||
throw new Error('missing data');
|
||||
}
|
||||
const rootNodes = this.treeNodeParser(rootNodesData);
|
||||
|
||||
@@ -71,8 +71,12 @@ TreeNavigationKeyCodes,
|
||||
};
|
||||
|
||||
function focusPreviousVisibleNode(context: TreeNavigationContext): void {
|
||||
const focusedNode = context.focus.currentSingleFocusedNode;
|
||||
if (!focusedNode) {
|
||||
return;
|
||||
}
|
||||
const previousVisibleNode = findPreviousVisibleNode(
|
||||
context.focus.currentSingleFocusedNode,
|
||||
focusedNode,
|
||||
context.nodes,
|
||||
);
|
||||
if (!previousVisibleNode) {
|
||||
@@ -83,6 +87,9 @@ function focusPreviousVisibleNode(context: TreeNavigationContext): void {
|
||||
|
||||
function focusNextVisibleNode(context: TreeNavigationContext): void {
|
||||
const focusedNode = context.focus.currentSingleFocusedNode;
|
||||
if (!focusedNode) {
|
||||
return;
|
||||
}
|
||||
const nextVisibleNode = findNextVisibleNode(focusedNode, context.nodes);
|
||||
if (!nextVisibleNode) {
|
||||
return;
|
||||
@@ -92,6 +99,9 @@ function focusNextVisibleNode(context: TreeNavigationContext): void {
|
||||
|
||||
function toggleTreeNodeCheckStatus(context: TreeNavigationContext): void {
|
||||
const focusedNode = context.focus.currentSingleFocusedNode;
|
||||
if (!focusedNode) {
|
||||
return;
|
||||
}
|
||||
const nodeState = focusedNode.state;
|
||||
let transaction = nodeState.beginTransaction();
|
||||
if (nodeState.current.checkState === TreeNodeCheckState.Checked) {
|
||||
@@ -104,19 +114,28 @@ function toggleTreeNodeCheckStatus(context: TreeNavigationContext): void {
|
||||
|
||||
function collapseNodeOrFocusParent(context: TreeNavigationContext): void {
|
||||
const focusedNode = context.focus.currentSingleFocusedNode;
|
||||
if (!focusedNode) {
|
||||
return;
|
||||
}
|
||||
const nodeState = focusedNode.state;
|
||||
const parentNode = focusedNode.hierarchy.parent;
|
||||
if (focusedNode.hierarchy.isBranchNode && nodeState.current.isExpanded) {
|
||||
nodeState.commitTransaction(
|
||||
nodeState.beginTransaction().withExpansionState(false),
|
||||
);
|
||||
} else {
|
||||
const parentNode = focusedNode.hierarchy.parent;
|
||||
if (!parentNode) {
|
||||
return;
|
||||
}
|
||||
context.focus.setSingleFocus(parentNode);
|
||||
}
|
||||
}
|
||||
|
||||
function expandNodeOrFocusFirstChild(context: TreeNavigationContext): void {
|
||||
const focusedNode = context.focus.currentSingleFocusedNode;
|
||||
if (!focusedNode) {
|
||||
return;
|
||||
}
|
||||
const nodeState = focusedNode.state;
|
||||
if (focusedNode.hierarchy.isBranchNode && !nodeState.current.isExpanded) {
|
||||
nodeState.commitTransaction(
|
||||
@@ -151,7 +170,10 @@ function findNextNode(node: TreeNode, nodes: QueryableNodes): TreeNode | undefin
|
||||
return nodes.flattenedNodes[index + 1] || undefined;
|
||||
}
|
||||
|
||||
function findPreviousVisibleNode(node: TreeNode, nodes: QueryableNodes): TreeNode | undefined {
|
||||
function findPreviousVisibleNode(
|
||||
node: TreeNode,
|
||||
nodes: QueryableNodes,
|
||||
): TreeNode | undefined {
|
||||
const previousNode = findPreviousNode(node, nodes);
|
||||
if (!previousNode) {
|
||||
return node.hierarchy.parent;
|
||||
|
||||
@@ -14,6 +14,7 @@ export function useTreeQueryFilter(
|
||||
const { nodes } = useCurrentTreeNodes(treeRootRef);
|
||||
|
||||
let isFiltering = false;
|
||||
|
||||
const statesBeforeFiltering = new NodeStateRestorer();
|
||||
statesBeforeFiltering.saveStateBeforeFilter(nodes.value);
|
||||
|
||||
@@ -35,7 +36,7 @@ export function useTreeQueryFilter(
|
||||
currentNodes.flattenedNodes.forEach((node: TreeNode) => {
|
||||
let transaction = node.state.beginTransaction()
|
||||
.withMatchState(false);
|
||||
transaction = statesBeforeFiltering.applyOriginalState(node, transaction);
|
||||
transaction = statesBeforeFiltering.applyStateBeforeFilter(node, transaction);
|
||||
node.state.commitTransaction(transaction);
|
||||
});
|
||||
statesBeforeFiltering.clear();
|
||||
@@ -150,18 +151,20 @@ class NodeStateRestorer {
|
||||
});
|
||||
}
|
||||
|
||||
public applyOriginalState(
|
||||
public applyStateBeforeFilter(
|
||||
node: TreeNode,
|
||||
transaction: TreeNodeStateTransaction,
|
||||
): TreeNodeStateTransaction {
|
||||
if (!this.originalStates.has(node)) {
|
||||
const originalState = this.originalStates.get(node);
|
||||
if (!originalState) {
|
||||
return transaction;
|
||||
}
|
||||
const originalState = this.originalStates.get(node);
|
||||
if (originalState.isExpanded !== undefined) {
|
||||
transaction = transaction.withExpansionState(originalState.isExpanded);
|
||||
}
|
||||
transaction = transaction.withVisibilityState(originalState.isVisible);
|
||||
if (originalState.isVisible !== undefined) {
|
||||
transaction = transaction.withVisibilityState(originalState.isVisible);
|
||||
}
|
||||
return transaction;
|
||||
}
|
||||
|
||||
@@ -185,18 +188,13 @@ function setupWatchers(options: {
|
||||
options.nodesRef,
|
||||
],
|
||||
([filterEvent, nodes]) => {
|
||||
if (!filterEvent) {
|
||||
if (filterEvent === undefined) {
|
||||
return;
|
||||
}
|
||||
switch (filterEvent.action) {
|
||||
case TreeViewFilterAction.Triggered:
|
||||
options.onFilterTrigger(filterEvent.predicate, nodes);
|
||||
break;
|
||||
case TreeViewFilterAction.Removed:
|
||||
options.onFilterReset();
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown action: ${TreeViewFilterAction[filterEvent.action]}`);
|
||||
if (filterEvent.action === TreeViewFilterAction.Triggered) {
|
||||
options.onFilterTrigger(filterEvent.predicate, nodes);
|
||||
} else {
|
||||
options.onFilterReset();
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
|
||||
@@ -2,18 +2,15 @@ import { ICategory, IScript } from '@/domain/ICategory';
|
||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
import { NodeMetadata, NodeType } from '../NodeContent/NodeMetadata';
|
||||
|
||||
export function parseAllCategories(collection: ICategoryCollection): NodeMetadata[] | undefined {
|
||||
export function parseAllCategories(collection: ICategoryCollection): NodeMetadata[] {
|
||||
return createCategoryNodes(collection.actions);
|
||||
}
|
||||
|
||||
export function parseSingleCategory(
|
||||
categoryId: number,
|
||||
collection: ICategoryCollection,
|
||||
): NodeMetadata[] | undefined {
|
||||
const category = collection.findCategory(categoryId);
|
||||
if (!category) {
|
||||
throw new Error(`Category with id ${categoryId} does not exist`);
|
||||
}
|
||||
): NodeMetadata[] {
|
||||
const category = collection.getCategory(categoryId);
|
||||
const tree = parseCategoryRecursively(category);
|
||||
return tree;
|
||||
}
|
||||
@@ -73,7 +70,7 @@ function convertScriptToNode(script: IScript): NodeMetadata {
|
||||
id: getScriptNodeId(script),
|
||||
type: NodeType.Script,
|
||||
text: script.name,
|
||||
children: undefined,
|
||||
children: [],
|
||||
docs: script.docs,
|
||||
isReversible: script.canRevert(),
|
||||
};
|
||||
|
||||
@@ -5,7 +5,6 @@ import { TreeInputNodeData } from '../TreeView/Bindings/TreeInputNodeData';
|
||||
export function getNodeMetadata(
|
||||
treeNode: ReadOnlyTreeNode,
|
||||
): NodeMetadata {
|
||||
if (!treeNode) { throw new Error('missing tree node'); }
|
||||
const data = treeNode.metadata as NodeMetadata;
|
||||
if (!data) {
|
||||
throw new Error('Provided node does not contain the expected metadata.');
|
||||
@@ -14,7 +13,6 @@ export function getNodeMetadata(
|
||||
}
|
||||
|
||||
export function convertToNodeInput(metadata: NodeMetadata): TreeInputNodeData {
|
||||
if (!metadata) { throw new Error('missing metadata'); }
|
||||
return {
|
||||
id: metadata.id,
|
||||
children: convertChildren(metadata.children, convertToNodeInput),
|
||||
@@ -23,7 +21,7 @@ export function convertToNodeInput(metadata: NodeMetadata): TreeInputNodeData {
|
||||
}
|
||||
|
||||
function convertChildren<TOldNode, TNewNode>(
|
||||
oldChildren: readonly TOldNode[],
|
||||
oldChildren: readonly TOldNode[] | undefined,
|
||||
callback: (value: TOldNode) => TNewNode,
|
||||
): TNewNode[] {
|
||||
if (!oldChildren || oldChildren.length === 0) {
|
||||
|
||||
@@ -42,7 +42,7 @@ export function useTreeViewFilterEvent() {
|
||||
|
||||
function subscribeToFilterChanges(
|
||||
filter: IReadOnlyUserFilter,
|
||||
latestFilterEvent: Ref<TreeViewFilterEvent>,
|
||||
latestFilterEvent: Ref<TreeViewFilterEvent | undefined>,
|
||||
filterPredicate: TreeNodeFilterResultPredicate,
|
||||
) {
|
||||
return filter.filterChanged.on((event) => {
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { IApplication } from '@/domain/IApplication';
|
||||
|
||||
export function useApplication(application: IApplication) {
|
||||
if (!application) {
|
||||
throw new Error('missing application');
|
||||
}
|
||||
return {
|
||||
application,
|
||||
info: application.info,
|
||||
|
||||
@@ -7,13 +7,6 @@ export function useCollectionState(
|
||||
context: IApplicationContext,
|
||||
events: IEventSubscriptionCollection,
|
||||
) {
|
||||
if (!context) {
|
||||
throw new Error('missing context');
|
||||
}
|
||||
if (!events) {
|
||||
throw new Error('missing events');
|
||||
}
|
||||
|
||||
const currentState = shallowRef<IReadOnlyCategoryCollectionState>(context.state);
|
||||
events.register([
|
||||
context.contextChanged.on((event) => {
|
||||
@@ -28,9 +21,6 @@ export function useCollectionState(
|
||||
handler: NewStateEventHandler,
|
||||
settings: Partial<IStateCallbackSettings> = defaultSettings,
|
||||
) {
|
||||
if (!handler) {
|
||||
throw new Error('missing state handler');
|
||||
}
|
||||
events.register([
|
||||
context.contextChanged.on((event) => {
|
||||
handler(event.newState, event.oldState);
|
||||
@@ -46,16 +36,10 @@ export function useCollectionState(
|
||||
}
|
||||
|
||||
function modifyCurrentState(mutator: StateModifier) {
|
||||
if (!mutator) {
|
||||
throw new Error('missing state mutator');
|
||||
}
|
||||
mutator(context.state);
|
||||
}
|
||||
|
||||
function modifyCurrentContext(mutator: ContextModifier) {
|
||||
if (!mutator) {
|
||||
throw new Error('missing context mutator');
|
||||
}
|
||||
mutator(context);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { IRuntimeEnvironment } from '@/infrastructure/RuntimeEnvironment/IRuntimeEnvironment';
|
||||
|
||||
export function useRuntimeEnvironment(environment: IRuntimeEnvironment) {
|
||||
if (!environment) {
|
||||
throw new Error('missing environment');
|
||||
}
|
||||
return environment;
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ const RawSvgLoaders = import.meta.glob('@/presentation/assets/icons/**/*.svg', {
|
||||
});
|
||||
|
||||
function modifySvg(svgSource: string): string {
|
||||
const parser = new window.DOMParser();
|
||||
const parser = new globalThis.window.DOMParser();
|
||||
const doc = parser.parseFromString(svgSource, 'image/svg+xml');
|
||||
let svgRoot = doc.documentElement;
|
||||
svgRoot = removeSvgComments(svgRoot);
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Ref, watchEffect } from 'vue';
|
||||
* Manages focus transitions, ensuring good usability and accessibility.
|
||||
*/
|
||||
export function useCurrentFocusToggle(shouldDisableFocus: Ref<boolean>) {
|
||||
let previouslyFocusedElement: HTMLElement | undefined;
|
||||
let previouslyFocusedElement: HTMLElement | null;
|
||||
|
||||
watchEffect(() => {
|
||||
if (shouldDisableFocus.value) {
|
||||
@@ -17,7 +17,7 @@ export function useCurrentFocusToggle(shouldDisableFocus: Ref<boolean>) {
|
||||
return;
|
||||
}
|
||||
previouslyFocusedElement.focus();
|
||||
previouslyFocusedElement = undefined;
|
||||
previouslyFocusedElement = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import {
|
||||
defineComponent, shallowRef, onMounted, onBeforeUnmount,
|
||||
defineComponent, shallowRef, onMounted, onBeforeUnmount, watch,
|
||||
} from 'vue';
|
||||
import { useResizeObserverPolyfill } from '@/presentation/components/Shared/Hooks/UseResizeObserverPolyfill';
|
||||
|
||||
@@ -25,61 +25,63 @@ export default defineComponent({
|
||||
|
||||
let width = 0;
|
||||
let height = 0;
|
||||
let observer: ResizeObserver;
|
||||
let observer: ResizeObserver | undefined;
|
||||
|
||||
onMounted(() => {
|
||||
width = containerElement.value.offsetWidth;
|
||||
height = containerElement.value.offsetHeight;
|
||||
|
||||
resizeObserverReady.then(() => {
|
||||
observer = new ResizeObserver(updateSize);
|
||||
observer.observe(containerElement.value);
|
||||
});
|
||||
|
||||
fireChangeEvents();
|
||||
watch(() => containerElement.value, async (element) => {
|
||||
if (!element) {
|
||||
disposeObserver();
|
||||
return;
|
||||
}
|
||||
resizeObserverReady.then(() => {
|
||||
observer = new ResizeObserver(updateSize);
|
||||
observer.observe(element);
|
||||
});
|
||||
updateSize();
|
||||
}, { immediate: true });
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
observer?.disconnect();
|
||||
disposeObserver();
|
||||
});
|
||||
|
||||
function updateSize() {
|
||||
let sizeChanged = false;
|
||||
if (isWidthChanged()) {
|
||||
updateWidth(containerElement.value.offsetWidth);
|
||||
sizeChanged = true;
|
||||
}
|
||||
if (isHeightChanged()) {
|
||||
updateHeight(containerElement.value.offsetHeight);
|
||||
sizeChanged = true;
|
||||
}
|
||||
if (sizeChanged) {
|
||||
const changes = [
|
||||
updateWidth(),
|
||||
updateHeight(),
|
||||
];
|
||||
if (changes.some((c) => c.isChanged)) {
|
||||
emit('sizeChanged');
|
||||
}
|
||||
}
|
||||
|
||||
function updateWidth(newWidth: number) {
|
||||
function updateWidth(): {
|
||||
readonly isChanged: boolean;
|
||||
} {
|
||||
const newWidth = containerElement.value?.offsetWidth ?? 0;
|
||||
if (newWidth === width) {
|
||||
return { isChanged: false };
|
||||
}
|
||||
width = newWidth;
|
||||
emit('widthChanged', newWidth);
|
||||
return { isChanged: true };
|
||||
}
|
||||
|
||||
function updateHeight(newHeight: number) {
|
||||
function updateHeight(): {
|
||||
readonly isChanged: boolean;
|
||||
} {
|
||||
const newHeight = containerElement.value?.offsetHeight ?? 0;
|
||||
if (newHeight === height) {
|
||||
return { isChanged: false };
|
||||
}
|
||||
height = newHeight;
|
||||
emit('heightChanged', newHeight);
|
||||
return { isChanged: true };
|
||||
}
|
||||
|
||||
function fireChangeEvents() {
|
||||
updateWidth(containerElement.value.offsetWidth);
|
||||
updateHeight(containerElement.value.offsetHeight);
|
||||
emit('sizeChanged');
|
||||
}
|
||||
|
||||
function isWidthChanged(): boolean {
|
||||
return width !== containerElement.value.offsetWidth;
|
||||
}
|
||||
|
||||
function isHeightChanged(): boolean {
|
||||
return height !== containerElement.value.offsetHeight;
|
||||
function disposeObserver() {
|
||||
observer?.disconnect();
|
||||
observer = undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -10,11 +10,11 @@ export function throttle(
|
||||
}
|
||||
|
||||
// Allows aligning with both NodeJs (NodeJs.Timeout) and Window type (number)
|
||||
export type TimeoutType = ReturnType<typeof setTimeout>;
|
||||
export type Timeout = ReturnType<typeof setTimeout>;
|
||||
|
||||
export interface ITimer {
|
||||
setTimeout: (callback: () => void, ms: number) => TimeoutType;
|
||||
clearTimeout: (timeoutId: TimeoutType) => void;
|
||||
setTimeout: (callback: () => void, ms: number) => Timeout;
|
||||
clearTimeout: (timeoutId: Timeout) => void;
|
||||
dateNow(): number;
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ interface IThrottler {
|
||||
}
|
||||
|
||||
class Throttler implements IThrottler {
|
||||
private queuedExecutionId: TimeoutType;
|
||||
private queuedExecutionId: Timeout | undefined;
|
||||
|
||||
private previouslyRun: number;
|
||||
|
||||
@@ -38,10 +38,8 @@ class Throttler implements IThrottler {
|
||||
private readonly waitInMs: number,
|
||||
private readonly callback: CallbackType,
|
||||
) {
|
||||
if (!timer) { throw new Error('missing timer'); }
|
||||
if (!waitInMs) { throw new Error('missing delay'); }
|
||||
if (waitInMs < 0) { throw new Error('negative delay'); }
|
||||
if (!callback) { throw new Error('missing callback'); }
|
||||
}
|
||||
|
||||
public invoke(...args: unknown[]): void {
|
||||
|
||||
@@ -41,7 +41,9 @@ export default defineComponent({
|
||||
...supportedOperativeSystems,
|
||||
].sort((os) => (os === currentOs ? 0 : 1));
|
||||
|
||||
const hasCurrentOsDesktopVersion = supportedOperativeSystems.includes(currentOs);
|
||||
const hasCurrentOsDesktopVersion = currentOs === undefined
|
||||
? false
|
||||
: supportedOperativeSystems.includes(currentOs);
|
||||
|
||||
return {
|
||||
supportedDesktops,
|
||||
|
||||
@@ -39,7 +39,7 @@ export default defineComponent({
|
||||
return hasDesktopVersion(props.operatingSystem);
|
||||
});
|
||||
|
||||
const downloadUrl = computed<string | undefined>(() => {
|
||||
const downloadUrl = computed<string>(() => {
|
||||
return info.getDownloadUrl(props.operatingSystem);
|
||||
});
|
||||
|
||||
|
||||
@@ -38,11 +38,12 @@ export default defineComponent({
|
||||
const { totalScripts } = currentState.value.collection;
|
||||
return `Search in ${totalScripts} scripts`;
|
||||
});
|
||||
const searchQuery = ref<string>();
|
||||
|
||||
const searchQuery = ref<string | undefined>();
|
||||
|
||||
watch(searchQuery, (newFilter) => updateFilter(newFilter));
|
||||
|
||||
function updateFilter(newFilter: string) {
|
||||
function updateFilter(newFilter: string | undefined) {
|
||||
modifyCurrentState((state) => {
|
||||
const { filter } = state;
|
||||
if (!newFilter) {
|
||||
@@ -61,7 +62,7 @@ export default defineComponent({
|
||||
}, { immediate: true });
|
||||
|
||||
function updateFromInitialFilter(filter?: IFilterResult) {
|
||||
searchQuery.value = filter?.query || '';
|
||||
searchQuery.value = filter?.query;
|
||||
}
|
||||
|
||||
function subscribeToFilterChanges(
|
||||
|
||||
Reference in New Issue
Block a user