Refactor to use string IDs for executables #262

This commit unifies the concepts of executables having same ID
structure. It paves the way for more complex ID structure and using IDs
in collection files as part of new ID solution (#262). Using string IDs
also leads to more expressive test code.

This commit also refactors the rest of the code to adopt to the changes.

This commit:

- Separate concerns from entities for data access (in repositories) and
  executables. Executables use `Identifiable` meanwhile repositories use
  `RepositoryEntity`.
- Refactor unnecessary generic parameters for enttities and ids,
  enforcing string gtype everwyhere.
- Changes numeric IDs to string IDs for categories to unify the
  retrieval and construction for executables, using pseudo-ids (their
  names) just like scripts.
- Remove `BaseEntity` for simplicity.
- Simplify usage and construction of executable objects.
  Move factories responsible for creation of category/scripts to domain
  layer. Do not longer export `CollectionCategorY` and
  `CollectionScript`.
- Use named typed for string IDs for better differentation of different
  ID contexts in code.
This commit is contained in:
undergroundwires
2024-06-16 11:44:48 +02:00
parent 19ea8dbc5b
commit 48d6dbd700
96 changed files with 1417 additions and 1109 deletions

View File

@@ -1,7 +1,7 @@
import type { Script } from '@/domain/Executables/Script/Script';
import { RecommendationLevel } from '@/domain/Executables/Script/RecommendationLevel';
import { scrambledEqual } from '@/application/Common/Array';
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
import type { ReadonlyScriptSelection, ScriptSelection } from '@/application/Context/State/Selection/Script/ScriptSelection';
import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
import { RecommendationStatusType } from './RecommendationStatusType';
@@ -99,6 +99,6 @@ function areAllSelected(
if (expectedScripts.length < selectedScriptIds.length) {
return false;
}
const expectedScriptIds = expectedScripts.map((script) => script.id);
const expectedScriptIds = expectedScripts.map((script) => script.executableId);
return scrambledEqual(selectedScriptIds, expectedScriptIds);
}

View File

@@ -90,7 +90,7 @@ import {
} from 'vue';
import { injectKey } from '@/presentation/injectionSymbols';
import TooltipWrapper from '@/presentation/components/Shared/TooltipWrapper.vue';
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
import MenuOptionList from '../MenuOptionList.vue';
import MenuOptionListItem from '../MenuOptionListItem.vue';
import { setCurrentRecommendationStatus, getCurrentRecommendationStatus } from './RecommendationStatusHandler';
@@ -142,3 +142,4 @@ export default defineComponent({
},
});
</script>
@/domain/Collection/ICategoryCollection

View File

@@ -44,6 +44,7 @@ import {
} from 'vue';
import { injectKey } from '@/presentation/injectionSymbols';
import SizeObserver from '@/presentation/components/Shared/SizeObserver.vue';
import type { ExecutableId } from '@/domain/Executables/Identifiable';
import { hasDirective } from './NonCollapsingDirective';
import CardListItem from './CardListItem.vue';
@@ -58,12 +59,12 @@ export default defineComponent({
const width = ref<number | undefined>();
const categoryIds = computed<readonly number[]>(
() => currentState.value.collection.actions.map((category) => category.id),
const categoryIds = computed<readonly ExecutableId[]>(
() => currentState.value.collection.actions.map((category) => category.executableId),
);
const activeCategoryId = ref<number | undefined>(undefined);
const activeCategoryId = ref<ExecutableId | undefined>(undefined);
function onSelected(categoryId: number, isExpanded: boolean) {
function onSelected(categoryId: ExecutableId, isExpanded: boolean) {
activeCategoryId.value = isExpanded ? categoryId : undefined;
}

View File

@@ -56,12 +56,14 @@
<script lang="ts">
import {
defineComponent, computed, shallowRef,
type PropType,
} from 'vue';
import AppIcon from '@/presentation/components/Shared/Icon/AppIcon.vue';
import FlatButton from '@/presentation/components/Shared/FlatButton.vue';
import { injectKey } from '@/presentation/injectionSymbols';
import ScriptsTree from '@/presentation/components/Scripts/View/Tree/ScriptsTree.vue';
import { sleep } from '@/infrastructure/Threading/AsyncSleep';
import type { ExecutableId } from '@/domain/Executables/Identifiable';
import CardSelectionIndicator from './CardSelectionIndicator.vue';
import CardExpandTransition from './CardExpandTransition.vue';
import CardExpansionArrow from './CardExpansionArrow.vue';
@@ -77,11 +79,11 @@ export default defineComponent({
},
props: {
categoryId: {
type: Number,
type: String as PropType<ExecutableId>,
required: true,
},
activeCategoryId: {
type: Number,
type: String as PropType<ExecutableId>,
default: undefined,
},
},

View File

@@ -12,11 +12,12 @@
</template>
<script lang="ts">
import { defineComponent, computed } from 'vue';
import { defineComponent, computed, type PropType } from 'vue';
import AppIcon from '@/presentation/components/Shared/Icon/AppIcon.vue';
import { injectKey } from '@/presentation/injectionSymbols';
import type { Category } from '@/domain/Executables/Category/Category';
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
import type { ExecutableId } from '@/domain/Executables/Identifiable';
export default defineComponent({
components: {
@@ -24,7 +25,7 @@ export default defineComponent({
},
props: {
categoryId: {
type: Number,
type: String as PropType<ExecutableId>,
required: true,
},
},
@@ -60,3 +61,4 @@ export default defineComponent({
font-size: $font-size-absolute-normal;
}
</style>
@/domain/Collection/ICategoryCollection

View File

@@ -1,10 +1,12 @@
import type { ExecutableId } from '@/domain/Executables/Identifiable';
export enum NodeType {
Script,
Category,
}
export interface NodeMetadata {
readonly id: string;
readonly id: ExecutableId;
readonly text: string;
readonly isReversible: boolean;
readonly docs: ReadonlyArray<string>;

View File

@@ -12,7 +12,7 @@ import {
} from 'vue';
import { injectKey } from '@/presentation/injectionSymbols';
import type { NodeMetadata } from '@/presentation/components/Scripts/View/Tree/NodeContent/NodeMetadata';
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
import { getReverter } from './Reverter/ReverterFactory';
import ToggleSwitch from './ToggleSwitch.vue';
import type { Reverter } from './Reverter/Reverter';
@@ -64,3 +64,4 @@ export default defineComponent({
},
});
</script>
@/domain/Collection/ICategoryCollection

View File

@@ -1,17 +1,18 @@
import type { UserSelection } from '@/application/Context/State/Selection/UserSelection';
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
import { getCategoryId } from '../../TreeViewAdapter/CategoryNodeMetadataConverter';
import type { ExecutableId } from '@/domain/Executables/Identifiable';
import { createExecutableIdFromNodeId } from '../../TreeViewAdapter/CategoryNodeMetadataConverter';
import { ScriptReverter } from './ScriptReverter';
import type { Reverter } from './Reverter';
export class CategoryReverter implements Reverter {
private readonly categoryId: number;
private readonly categoryId: ExecutableId;
private readonly scriptReverters: ReadonlyArray<ScriptReverter>;
constructor(nodeId: string, collection: ICategoryCollection) {
this.categoryId = getCategoryId(nodeId);
this.categoryId = createExecutableIdFromNodeId(nodeId);
this.scriptReverters = createScriptReverters(this.categoryId, collection);
}
@@ -37,12 +38,12 @@ export class CategoryReverter implements Reverter {
}
function createScriptReverters(
categoryId: number,
categoryId: ExecutableId,
collection: ICategoryCollection,
): ScriptReverter[] {
const category = collection.getCategory(categoryId);
const scripts = category
.getAllScriptsRecursively()
.filter((script) => script.canRevert());
return scripts.map((script) => new ScriptReverter(script.id));
return scripts.map((script) => new ScriptReverter(script.executableId));
}

View File

@@ -1,4 +1,4 @@
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
import { type NodeMetadata, NodeType } from '../NodeMetadata';
import { ScriptReverter } from './ScriptReverter';
import { CategoryReverter } from './CategoryReverter';

View File

@@ -1,13 +1,13 @@
import type { UserSelection } from '@/application/Context/State/Selection/UserSelection';
import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
import { getScriptId } from '../../TreeViewAdapter/CategoryNodeMetadataConverter';
import { createExecutableIdFromNodeId } from '../../TreeViewAdapter/CategoryNodeMetadataConverter';
import type { Reverter } from './Reverter';
export class ScriptReverter implements Reverter {
private readonly scriptId: string;
constructor(nodeId: string) {
this.scriptId = getScriptId(nodeId);
this.scriptId = createExecutableIdFromNodeId(nodeId);
}
public getState(selectedScripts: ReadonlyArray<SelectedScript>): boolean {

View File

@@ -24,8 +24,9 @@
</template>
<script lang="ts">
import { defineComponent, toRef } from 'vue';
import { defineComponent, toRef, type PropType } from 'vue';
import { injectKey } from '@/presentation/injectionSymbols';
import type { ExecutableId } from '@/domain/Executables/Identifiable';
import TreeView from './TreeView/TreeView.vue';
import NodeContent from './NodeContent/NodeContent.vue';
import { useTreeViewFilterEvent } from './TreeViewAdapter/UseTreeViewFilterEvent';
@@ -41,7 +42,7 @@ export default defineComponent({
},
props: {
categoryId: {
type: [Number],
type: String as PropType<ExecutableId>,
default: undefined,
},
hasTopPadding: {

View File

@@ -1,5 +1,7 @@
export type TreeInputNodeDataId = string;
export interface TreeInputNodeData {
readonly id: string;
readonly id: TreeInputNodeDataId;
readonly children?: readonly TreeInputNodeData[];
readonly parent?: TreeInputNodeData | null;
readonly data?: object;

View File

@@ -56,7 +56,7 @@ import { useNodeState } from './UseNodeState';
import LeafTreeNode from './LeafTreeNode.vue';
import InteractableNode from './InteractableNode.vue';
import type { TreeRoot } from '../TreeRoot/TreeRoot';
import type { TreeNode } from './TreeNode';
import type { TreeNode, TreeNodeId } from './TreeNode';
import type { NodeRenderingStrategy } from '../Rendering/Scheduling/NodeRenderingStrategy';
import type { PropType } from 'vue';
@@ -69,7 +69,7 @@ export default defineComponent({
},
props: {
nodeId: {
type: String,
type: String as PropType<TreeNodeId>,
required: true,
},
treeRoot: {

View File

@@ -18,13 +18,13 @@ import { useCurrentTreeNodes } from '../UseCurrentTreeNodes';
import { useNodeState } from './UseNodeState';
import { useKeyboardInteractionState } from './UseKeyboardInteractionState';
import type { TreeRoot } from '../TreeRoot/TreeRoot';
import type { TreeNode } from './TreeNode';
import type { TreeNode, TreeNodeId } from './TreeNode';
import type { PropType } from 'vue';
export default defineComponent({
props: {
nodeId: {
type: String,
type: String as PropType<TreeNodeId>,
required: true,
},
treeRoot: {

View File

@@ -28,7 +28,7 @@ import { defineComponent, computed, toRef } from 'vue';
import { useCurrentTreeNodes } from '../UseCurrentTreeNodes';
import NodeCheckbox from './NodeCheckbox.vue';
import InteractableNode from './InteractableNode.vue';
import type { TreeNode } from './TreeNode';
import type { TreeNode, TreeNodeId } from './TreeNode';
import type { TreeRoot } from '../TreeRoot/TreeRoot';
import type { PropType } from 'vue';
@@ -39,7 +39,7 @@ export default defineComponent({
},
props: {
nodeId: {
type: String,
type: String as PropType<TreeNodeId>,
required: true,
},
treeRoot: {

View File

@@ -14,13 +14,13 @@ import { useCurrentTreeNodes } from '../UseCurrentTreeNodes';
import { useNodeState } from './UseNodeState';
import { TreeNodeCheckState } from './State/CheckState';
import type { TreeRoot } from '../TreeRoot/TreeRoot';
import type { TreeNode } from './TreeNode';
import type { TreeNode, TreeNodeId } from './TreeNode';
import type { PropType } from 'vue';
export default defineComponent({
props: {
nodeId: {
type: String,
type: String as PropType<TreeNodeId>,
required: true,
},
treeRoot: {

View File

@@ -1,8 +1,10 @@
import type { HierarchyAccess, HierarchyReader } from './Hierarchy/HierarchyAccess';
import type { TreeNodeStateAccess, TreeNodeStateReader } from './State/StateAccess';
export type TreeNodeId = string;
export interface ReadOnlyTreeNode {
readonly id: string;
readonly id: TreeNodeId;
readonly state: TreeNodeStateReader;
readonly hierarchy: HierarchyReader;
readonly metadata?: object;

View File

@@ -1,6 +1,6 @@
import { TreeNodeHierarchy } from './Hierarchy/TreeNodeHierarchy';
import { TreeNodeState } from './State/TreeNodeState';
import type { TreeNode } from './TreeNode';
import type { TreeNode, TreeNodeId } from './TreeNode';
import type { TreeNodeStateAccess } from './State/StateAccess';
import type { HierarchyAccess } from './Hierarchy/HierarchyAccess';
@@ -9,7 +9,7 @@ export class TreeNodeManager implements TreeNode {
public readonly hierarchy: HierarchyAccess;
constructor(public readonly id: string, public readonly metadata?: object) {
constructor(public readonly id: TreeNodeId, public readonly metadata?: object) {
if (!id) {
throw new Error('missing id');
}

View File

@@ -22,6 +22,7 @@ import {
} from 'vue';
import HierarchicalTreeNode from '../Node/HierarchicalTreeNode.vue';
import { useCurrentTreeNodes } from '../UseCurrentTreeNodes';
import { type TreeNodeId } from '../Node/TreeNode';
import type { NodeRenderingStrategy } from '../Rendering/Scheduling/NodeRenderingStrategy';
import type { TreeRoot } from './TreeRoot';
import type { PropType } from 'vue';
@@ -43,7 +44,7 @@ export default defineComponent({
setup(props) {
const { nodes } = useCurrentTreeNodes(toRef(props, 'treeRoot'));
const renderedNodeIds = computed<string[]>(() => {
const renderedNodeIds = computed<TreeNodeId[]>(() => {
return nodes
.value
.rootNodes

View File

@@ -26,6 +26,7 @@ import { useLeafNodeCheckedStateUpdater } from './UseLeafNodeCheckedStateUpdater
import { useAutoUpdateParentCheckState } from './UseAutoUpdateParentCheckState';
import { useAutoUpdateChildrenCheckState } from './UseAutoUpdateChildrenCheckState';
import { useGradualNodeRendering, type NodeRenderingControl } from './Rendering/UseGradualNodeRendering';
import { type TreeNodeId } from './Node/TreeNode';
import type { TreeNodeStateChangedEmittedEvent } from './Bindings/TreeNodeStateChangedEmittedEvent';
import type { TreeInputNodeData } from './Bindings/TreeInputNodeData';
import type { TreeViewFilterEvent } from './Bindings/TreeInputFilterEvent';
@@ -45,7 +46,7 @@ export default defineComponent({
default: () => undefined,
},
selectedLeafNodeIds: {
type: Array as PropType<ReadonlyArray<string>>,
type: Array as PropType<ReadonlyArray<TreeNodeId>>,
default: () => [],
},
},

View File

@@ -1,14 +1,17 @@
import type { Category } from '@/domain/Executables/Category/Category';
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
import type { Script } from '@/domain/Executables/Script/Script';
import type { ExecutableId } from '@/domain/Executables/Identifiable';
import type { Executable } from '@/domain/Executables/Executable';
import { type NodeMetadata, NodeType } from '../NodeContent/NodeMetadata';
import type { TreeNodeId } from '../TreeView/Node/TreeNode';
export function parseAllCategories(collection: ICategoryCollection): NodeMetadata[] {
return createCategoryNodes(collection.actions);
}
export function parseSingleCategory(
categoryId: number,
categoryId: ExecutableId,
collection: ICategoryCollection,
): NodeMetadata[] {
const category = collection.getCategory(categoryId);
@@ -16,27 +19,19 @@ export function parseSingleCategory(
return tree;
}
export function getScriptNodeId(script: Script): string {
return script.id;
export function createNodeIdForExecutable(executable: Executable): TreeNodeId {
return executable.executableId;
}
export function getScriptId(nodeId: string): string {
export function createExecutableIdFromNodeId(nodeId: TreeNodeId): ExecutableId {
return nodeId;
}
export function getCategoryId(nodeId: string): number {
return +nodeId;
}
export function getCategoryNodeId(category: Category): string {
return `${category.id}`;
}
function parseCategoryRecursively(
parentCategory: Category,
): NodeMetadata[] {
return [
...createCategoryNodes(parentCategory.subCategories),
...createCategoryNodes(parentCategory.subcategories),
...createScriptNodes(parentCategory.scripts),
];
}
@@ -57,7 +52,7 @@ function convertCategoryToNode(
children: readonly NodeMetadata[],
): NodeMetadata {
return {
id: getCategoryNodeId(category),
id: createNodeIdForExecutable(category),
type: NodeType.Category,
text: category.name,
children,
@@ -68,7 +63,7 @@ function convertCategoryToNode(
function convertScriptToNode(script: Script): NodeMetadata {
return {
id: getScriptNodeId(script),
id: createNodeIdForExecutable(script),
type: NodeType.Script,
text: script.name,
children: [],

View File

@@ -0,0 +1,3 @@
export function UseExecutableFromTreeNodeId(treeNodeId: string) {
}

View File

@@ -2,20 +2,21 @@ import {
computed, shallowReadonly,
} from 'vue';
import type { useUserSelectionState } from '@/presentation/components/Shared/Hooks/UseUserSelectionState';
import { getScriptNodeId } from './CategoryNodeMetadataConverter';
import { createNodeIdForExecutable } from './CategoryNodeMetadataConverter';
import type { TreeNodeId } from '../TreeView/Node/TreeNode';
export function useSelectedScriptNodeIds(
useSelectionStateHook: ReturnType<typeof useUserSelectionState>,
scriptNodeIdParser = getScriptNodeId,
convertToNodeId = createNodeIdForExecutable,
) {
const { currentSelection } = useSelectionStateHook;
const selectedNodeIds = computed<readonly string[]>(() => {
const selectedNodeIds = computed<readonly TreeNodeId[]>(() => {
return currentSelection
.value
.scripts
.selectedScripts
.map((selected) => scriptNodeIdParser(selected.script));
.map((selected) => convertToNodeId(selected.script));
});
return {

View File

@@ -1,16 +1,15 @@
import {
type Ref, shallowReadonly, shallowRef,
} from 'vue';
import type { Script } from '@/domain/Executables/Script/Script';
import type { Category } from '@/domain/Executables/Category/Category';
import { injectKey } from '@/presentation/injectionSymbols';
import type { ReadonlyFilterContext } from '@/application/Context/State/Filter/FilterContext';
import type { FilterResult } from '@/application/Context/State/Filter/Result/FilterResult';
import type { ExecutableId } from '@/domain/Executables/Identifiable';
import type { Executable } from '@/domain/Executables/Executable';
import { type TreeViewFilterEvent, createFilterRemovedEvent, createFilterTriggeredEvent } from '../TreeView/Bindings/TreeInputFilterEvent';
import { getNodeMetadata } from './TreeNodeMetadataConverter';
import { getCategoryNodeId, getScriptNodeId } from './CategoryNodeMetadataConverter';
import type { NodeMetadata } from '../NodeContent/NodeMetadata';
import type { ReadOnlyTreeNode } from '../TreeView/Node/TreeNode';
import { createExecutableIdFromNodeId } from './CategoryNodeMetadataConverter';
import type { ReadOnlyTreeNode, TreeNodeId } from '../TreeView/Node/TreeNode';
type TreeNodeFilterResultPredicate = (
node: ReadOnlyTreeNode,
@@ -24,7 +23,7 @@ export function useTreeViewFilterEvent() {
const latestFilterEvent = shallowRef<TreeViewFilterEvent | undefined>(undefined);
const treeNodePredicate: TreeNodeFilterResultPredicate = (node, filterResult) => filterMatches(
getNodeMetadata(node),
node.id,
filterResult,
);
@@ -71,15 +70,17 @@ function createFilterEvent(
);
}
function filterMatches(node: NodeMetadata, filter: FilterResult): boolean {
return containsScript(node, filter.scriptMatches)
|| containsCategory(node, filter.categoryMatches);
function filterMatches(nodeId: TreeNodeId, filter: FilterResult): boolean {
const executableId = createExecutableIdFromNodeId(nodeId);
return containsExecutable(executableId, filter.scriptMatches)
|| containsExecutable(executableId, filter.categoryMatches);
}
function containsScript(expected: NodeMetadata, scripts: readonly Script[]) {
return scripts.some((existing: Script) => expected.id === getScriptNodeId(existing));
}
function containsCategory(expected: NodeMetadata, categories: readonly Category[]) {
return categories.some((existing: Category) => expected.id === getCategoryNodeId(existing));
function containsExecutable(
expectedId: ExecutableId,
executables: readonly Executable[],
): boolean {
return executables.some(
(existing: Category) => existing.executableId === expectedId,
);
}

View File

@@ -1,15 +1,16 @@
import {
type Ref, computed, shallowReadonly,
} from 'vue';
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
import { injectKey } from '@/presentation/injectionSymbols';
import type { ExecutableId } from '@/domain/Executables/Identifiable';
import { parseSingleCategory, parseAllCategories } from './CategoryNodeMetadataConverter';
import { convertToNodeInput } from './TreeNodeMetadataConverter';
import type { TreeInputNodeData } from '../TreeView/Bindings/TreeInputNodeData';
import type { NodeMetadata } from '../NodeContent/NodeMetadata';
export function useTreeViewNodeInput(
categoryIdRef: Readonly<Ref<number | undefined>>,
categoryIdRef: Readonly<Ref<ExecutableId | undefined>>,
parser: CategoryNodeParser = {
parseSingle: parseSingleCategory,
parseAll: parseAllCategories,
@@ -30,7 +31,7 @@ export function useTreeViewNodeInput(
}
function parseNodes(
categoryId: number | undefined,
categoryId: ExecutableId | undefined,
categoryCollection: ICategoryCollection,
parser: CategoryNodeParser,
): NodeMetadata[] {