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:
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user