Refactor executable IDs to use strings #262
This commit unifies executable ID structure across categories and scripts, paving the way for more complex ID solutions for #262. It also refactors related code to adapt to the changes. Key changes: - Change numeric IDs to string IDs for categories - Use named types for string IDs to improve code clarity - Add unit tests to verify ID uniqueness Other supporting changes: - Separate concerns in entities for data access and executables by using separate abstractions (`Identifiable` and `RepositoryEntity`) - Simplify usage and construction of entities. - Remove `BaseEntity` for simplicity. - Move creation of categories/scripts to domain layer - Refactor CategoryCollection for better validation logic isolation - Rename some categories to keep the names (used as pseudo-IDs) unique on Windows.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import type { IApplication } from '@/domain/IApplication';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||
import { EventSource } from '@/infrastructure/Events/EventSource';
|
||||
import { assertInRange } from '@/application/Common/Enum';
|
||||
import { CategoryCollectionState } from './State/CategoryCollectionState';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { AdaptiveFilterContext } from './Filter/AdaptiveFilterContext';
|
||||
import { ApplicationCode } from './Code/ApplicationCode';
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { Script } from '@/domain/Executables/Script/Script';
|
||||
import type { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
||||
import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
|
||||
import { splitTextIntoLines } from '@/application/Common/Text/SplitTextIntoLines';
|
||||
import type { ExecutableId } from '@/domain/Executables/Identifiable';
|
||||
import type { ICodeChangedEvent } from './ICodeChangedEvent';
|
||||
|
||||
export class CodeChangedEvent implements ICodeChangedEvent {
|
||||
@@ -37,12 +38,12 @@ export class CodeChangedEvent implements ICodeChangedEvent {
|
||||
}
|
||||
|
||||
public getScriptPositionInCode(script: Script): ICodePosition {
|
||||
return this.getPositionById(script.id);
|
||||
return this.getPositionById(script.executableId);
|
||||
}
|
||||
|
||||
private getPositionById(scriptId: string): ICodePosition {
|
||||
private getPositionById(scriptId: ExecutableId): ICodePosition {
|
||||
const position = [...this.scripts.entries()]
|
||||
.filter(([s]) => s.id === scriptId)
|
||||
.filter(([s]) => s.executableId === scriptId)
|
||||
.map(([, pos]) => pos)
|
||||
.at(0);
|
||||
if (!position) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { EventSource } from '@/infrastructure/Events/EventSource';
|
||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||
import { FilterChange } from './Event/FilterChange';
|
||||
import { LinearFilterStrategy } from './Strategy/LinearFilterStrategy';
|
||||
import type { FilterResult } from './Result/FilterResult';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||
import type { FilterResult } from '../Result/FilterResult';
|
||||
|
||||
export interface FilterStrategy {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Category } from '@/domain/Executables/Category/Category';
|
||||
import type { ScriptCode } from '@/domain/Executables/Script/Code/ScriptCode';
|
||||
import type { Documentable } from '@/domain/Executables/Documentable';
|
||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||
import type { Script } from '@/domain/Executables/Script/Script';
|
||||
import { AppliedFilterResult } from '../Result/AppliedFilterResult';
|
||||
import type { FilterStrategy } from './FilterStrategy';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import type { IApplicationCode } from './Code/IApplicationCode';
|
||||
import type { ReadonlyFilterContext, FilterContext } from './Filter/FilterContext';
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { ExecutableId } from '@/domain/Executables/Identifiable';
|
||||
|
||||
type CategorySelectionStatus = {
|
||||
readonly isSelected: true;
|
||||
readonly isReverted: boolean;
|
||||
@@ -6,7 +8,7 @@ type CategorySelectionStatus = {
|
||||
};
|
||||
|
||||
export interface CategorySelectionChange {
|
||||
readonly categoryId: number;
|
||||
readonly categoryId: ExecutableId;
|
||||
readonly newStatus: CategorySelectionStatus;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Category } from '@/domain/Executables/Category/Category';
|
||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||
import type { CategorySelectionChange, CategorySelectionChangeCommand } from './CategorySelectionChange';
|
||||
import type { CategorySelection } from './CategorySelection';
|
||||
import type { ScriptSelection } from '../Script/ScriptSelection';
|
||||
@@ -23,7 +23,7 @@ export class ScriptToCategorySelectionMapper implements CategorySelection {
|
||||
return false;
|
||||
}
|
||||
return scripts.every(
|
||||
(script) => selectedScripts.some((selected) => selected.id === script.id),
|
||||
(script) => selectedScripts.some((selected) => selected.id === script.executableId),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ export class ScriptToCategorySelectionMapper implements CategorySelection {
|
||||
const scripts = category.getAllScriptsRecursively();
|
||||
const scriptsChangesInCategory = scripts
|
||||
.map((script): ScriptSelectionChange => ({
|
||||
scriptId: script.id,
|
||||
scriptId: script.executableId,
|
||||
newStatus: {
|
||||
...change.newStatus,
|
||||
},
|
||||
|
||||
@@ -2,8 +2,9 @@ import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryReposito
|
||||
import type { Script } from '@/domain/Executables/Script/Script';
|
||||
import { EventSource } from '@/infrastructure/Events/EventSource';
|
||||
import type { ReadonlyRepository, Repository } from '@/application/Repository/Repository';
|
||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||
import { batchedDebounce } from '@/application/Common/Timing/BatchedDebounce';
|
||||
import type { ExecutableId } from '@/domain/Executables/Identifiable';
|
||||
import { UserSelectedScript } from './UserSelectedScript';
|
||||
import type { ScriptSelection } from './ScriptSelection';
|
||||
import type { ScriptSelectionChange, ScriptSelectionChangeCommand } from './ScriptSelectionChange';
|
||||
@@ -16,7 +17,7 @@ export type DebounceFunction = typeof batchedDebounce<ScriptSelectionChangeComma
|
||||
export class DebouncedScriptSelection implements ScriptSelection {
|
||||
public readonly changed = new EventSource<ReadonlyArray<SelectedScript>>();
|
||||
|
||||
private readonly scripts: Repository<string, SelectedScript>;
|
||||
private readonly scripts: Repository<SelectedScript>;
|
||||
|
||||
public readonly processChanges: ScriptSelection['processChanges'];
|
||||
|
||||
@@ -25,7 +26,7 @@ export class DebouncedScriptSelection implements ScriptSelection {
|
||||
selectedScripts: ReadonlyArray<SelectedScript>,
|
||||
debounce: DebounceFunction = batchedDebounce,
|
||||
) {
|
||||
this.scripts = new InMemoryRepository<string, SelectedScript>();
|
||||
this.scripts = new InMemoryRepository<SelectedScript>();
|
||||
for (const script of selectedScripts) {
|
||||
this.scripts.addItem(script);
|
||||
}
|
||||
@@ -38,8 +39,8 @@ export class DebouncedScriptSelection implements ScriptSelection {
|
||||
);
|
||||
}
|
||||
|
||||
public isSelected(scriptId: string): boolean {
|
||||
return this.scripts.exists(scriptId);
|
||||
public isSelected(scriptExecutableId: ExecutableId): boolean {
|
||||
return this.scripts.exists(scriptExecutableId);
|
||||
}
|
||||
|
||||
public get selectedScripts(): readonly SelectedScript[] {
|
||||
@@ -49,7 +50,7 @@ export class DebouncedScriptSelection implements ScriptSelection {
|
||||
public selectAll(): void {
|
||||
const scriptsToSelect = this.collection
|
||||
.getAllScripts()
|
||||
.filter((script) => !this.scripts.exists(script.id))
|
||||
.filter((script) => !this.scripts.exists(script.executableId))
|
||||
.map((script) => new UserSelectedScript(script, false));
|
||||
if (scriptsToSelect.length === 0) {
|
||||
return;
|
||||
@@ -116,12 +117,12 @@ export class DebouncedScriptSelection implements ScriptSelection {
|
||||
private applyChange(change: ScriptSelectionChange): number {
|
||||
const script = this.collection.getScript(change.scriptId);
|
||||
if (change.newStatus.isSelected) {
|
||||
return this.addOrUpdateScript(script.id, change.newStatus.isReverted);
|
||||
return this.addOrUpdateScript(script.executableId, change.newStatus.isReverted);
|
||||
}
|
||||
return this.removeScript(script.id);
|
||||
return this.removeScript(script.executableId);
|
||||
}
|
||||
|
||||
private addOrUpdateScript(scriptId: string, revert: boolean): number {
|
||||
private addOrUpdateScript(scriptId: ExecutableId, revert: boolean): number {
|
||||
const script = this.collection.getScript(scriptId);
|
||||
const selectedScript = new UserSelectedScript(script, revert);
|
||||
if (!this.scripts.exists(selectedScript.id)) {
|
||||
@@ -136,7 +137,7 @@ export class DebouncedScriptSelection implements ScriptSelection {
|
||||
return 1;
|
||||
}
|
||||
|
||||
private removeScript(scriptId: string): number {
|
||||
private removeScript(scriptId: ExecutableId): number {
|
||||
if (!this.scripts.exists(scriptId)) {
|
||||
return 0;
|
||||
}
|
||||
@@ -152,24 +153,24 @@ function assertNonEmptyScriptSelection(selectedItems: readonly Script[]) {
|
||||
}
|
||||
|
||||
function getScriptIdsToBeSelected(
|
||||
existingItems: ReadonlyRepository<string, SelectedScript>,
|
||||
existingItems: ReadonlyRepository<SelectedScript>,
|
||||
desiredScripts: readonly Script[],
|
||||
): string[] {
|
||||
return desiredScripts
|
||||
.filter((script) => !existingItems.exists(script.id))
|
||||
.map((script) => script.id);
|
||||
.filter((script) => !existingItems.exists(script.executableId))
|
||||
.map((script) => script.executableId);
|
||||
}
|
||||
|
||||
function getScriptIdsToBeDeselected(
|
||||
existingItems: ReadonlyRepository<string, SelectedScript>,
|
||||
existingItems: ReadonlyRepository<SelectedScript>,
|
||||
desiredScripts: readonly Script[],
|
||||
): string[] {
|
||||
return existingItems
|
||||
.getItems()
|
||||
.filter((existing) => !desiredScripts.some((script) => existing.id === script.id))
|
||||
.filter((existing) => !desiredScripts.some((script) => existing.id === script.executableId))
|
||||
.map((script) => script.id);
|
||||
}
|
||||
|
||||
function equals(a: SelectedScript, b: SelectedScript): boolean {
|
||||
return a.script.equals(b.script.id) && a.revert === b.revert;
|
||||
return a.script.executableId === b.script.executableId && a.revert === b.revert;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import type { IEventSource } from '@/infrastructure/Events/IEventSource';
|
||||
import type { Script } from '@/domain/Executables/Script/Script';
|
||||
import type { ExecutableId } from '@/domain/Executables/Identifiable';
|
||||
import type { SelectedScript } from './SelectedScript';
|
||||
import type { ScriptSelectionChangeCommand } from './ScriptSelectionChange';
|
||||
|
||||
export interface ReadonlyScriptSelection {
|
||||
readonly changed: IEventSource<readonly SelectedScript[]>;
|
||||
readonly selectedScripts: readonly SelectedScript[];
|
||||
isSelected(scriptId: string): boolean;
|
||||
isSelected(scriptExecutableId: ExecutableId): boolean;
|
||||
}
|
||||
|
||||
export interface ScriptSelection extends ReadonlyScriptSelection {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { ExecutableId } from '@/domain/Executables/Identifiable';
|
||||
|
||||
export type ScriptSelectionStatus = {
|
||||
readonly isSelected: true;
|
||||
readonly isReverted: boolean;
|
||||
@@ -7,7 +9,7 @@ export type ScriptSelectionStatus = {
|
||||
};
|
||||
|
||||
export interface ScriptSelectionChange {
|
||||
readonly scriptId: string;
|
||||
readonly scriptId: ExecutableId;
|
||||
readonly newStatus: ScriptSelectionStatus;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import type { IEntity } from '@/infrastructure/Entity/IEntity';
|
||||
import type { Script } from '@/domain/Executables/Script/Script';
|
||||
import type { RepositoryEntity } from '@/application/Repository/RepositoryEntity';
|
||||
|
||||
type ScriptId = Script['id'];
|
||||
|
||||
export interface SelectedScript extends IEntity<ScriptId> {
|
||||
export interface SelectedScript extends RepositoryEntity {
|
||||
readonly script: Script;
|
||||
readonly revert: boolean;
|
||||
}
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
|
||||
import type { Script } from '@/domain/Executables/Script/Script';
|
||||
import type { SelectedScript } from './SelectedScript';
|
||||
import type { RepositoryEntity } from '@/application/Repository/RepositoryEntity';
|
||||
|
||||
type SelectedScriptId = SelectedScript['id'];
|
||||
export class UserSelectedScript implements RepositoryEntity {
|
||||
public readonly id: string;
|
||||
|
||||
export class UserSelectedScript extends BaseEntity<SelectedScriptId> {
|
||||
constructor(
|
||||
public readonly script: Script,
|
||||
public readonly revert: boolean,
|
||||
) {
|
||||
super(script.id);
|
||||
this.id = script.executableId;
|
||||
if (revert && !script.canRevert()) {
|
||||
throw new Error(`The script with ID '${script.id}' is not reversible and cannot be reverted.`);
|
||||
throw new Error(`The script with ID '${script.executableId}' is not reversible and cannot be reverted.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||
import { ScriptToCategorySelectionMapper } from './Category/ScriptToCategorySelectionMapper';
|
||||
import { DebouncedScriptSelection } from './Script/DebouncedScriptSelection';
|
||||
import type { CategorySelection } from './Category/CategorySelection';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { CollectionData } from '@/application/collections/';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
import { CategoryCollection } from '@/domain/CategoryCollection';
|
||||
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||
import { CategoryCollection } from '@/domain/Collection/CategoryCollection';
|
||||
import type { ProjectDetails } from '@/domain/Project/ProjectDetails';
|
||||
import { createEnumParser, type EnumParser } from '../Common/Enum';
|
||||
import { parseCategory, type CategoryParser } from './Executable/CategoryParser';
|
||||
|
||||
@@ -3,16 +3,14 @@ import type {
|
||||
} from '@/application/collections/';
|
||||
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError';
|
||||
import type { Category } from '@/domain/Executables/Category/Category';
|
||||
import { CollectionCategory } from '@/domain/Executables/Category/CollectionCategory';
|
||||
import type { Script } from '@/domain/Executables/Script/Script';
|
||||
import { createCategory, type CategoryFactory } from '@/domain/Executables/Category/CategoryFactory';
|
||||
import { parseDocs, type DocsParser } from './DocumentationParser';
|
||||
import { parseScript, type ScriptParser } from './Script/ScriptParser';
|
||||
import { createExecutableDataValidator, type ExecutableValidator, type ExecutableValidatorFactory } from './Validation/ExecutableValidator';
|
||||
import { ExecutableType } from './Validation/ExecutableType';
|
||||
import type { CategoryCollectionSpecificUtilities } from './CategoryCollectionSpecificUtilities';
|
||||
|
||||
let categoryIdCounter = 0;
|
||||
|
||||
export const parseCategory: CategoryParser = (
|
||||
category: CategoryData,
|
||||
collectionUtilities: CategoryCollectionSpecificUtilities,
|
||||
@@ -59,7 +57,7 @@ function parseCategoryRecursively(
|
||||
}
|
||||
try {
|
||||
return context.categoryUtilities.createCategory({
|
||||
id: categoryIdCounter++,
|
||||
executableId: context.categoryData.category, // Pseudo-ID for uniqueness until real ID support
|
||||
name: context.categoryData.category,
|
||||
docs: context.categoryUtilities.parseDocs(context.categoryData),
|
||||
subcategories: children.subcategories,
|
||||
@@ -166,10 +164,6 @@ function hasProperty(
|
||||
return Object.prototype.hasOwnProperty.call(object, propertyName);
|
||||
}
|
||||
|
||||
export type CategoryFactory = (
|
||||
...parameters: ConstructorParameters<typeof CollectionCategory>
|
||||
) => Category;
|
||||
|
||||
interface CategoryParserUtilities {
|
||||
readonly createCategory: CategoryFactory;
|
||||
readonly wrapError: ErrorWithContextWrapper;
|
||||
@@ -179,7 +173,7 @@ interface CategoryParserUtilities {
|
||||
}
|
||||
|
||||
const DefaultCategoryParserUtilities: CategoryParserUtilities = {
|
||||
createCategory: (...parameters) => new CollectionCategory(...parameters),
|
||||
createCategory,
|
||||
wrapError: wrapErrorWithAdditionalContext,
|
||||
createValidator: createExecutableDataValidator,
|
||||
parseScript,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { ScriptData, CodeScriptData, CallScriptData } from '@/application/collections/';
|
||||
import { NoEmptyLines } from '@/application/Parser/Executable/Script/Validation/Rules/NoEmptyLines';
|
||||
import type { ILanguageSyntax } from '@/application/Parser/Executable/Script/Validation/Syntax/ILanguageSyntax';
|
||||
import { CollectionScript } from '@/domain/Executables/Script/CollectionScript';
|
||||
import { RecommendationLevel } from '@/domain/Executables/Script/RecommendationLevel';
|
||||
import type { ScriptCode } from '@/domain/Executables/Script/Code/ScriptCode';
|
||||
import type { ICodeValidator } from '@/application/Parser/Executable/Script/Validation/ICodeValidator';
|
||||
@@ -11,6 +10,7 @@ import { createScriptCode } from '@/domain/Executables/Script/Code/ScriptCodeFac
|
||||
import type { Script } from '@/domain/Executables/Script/Script';
|
||||
import { createEnumParser, type EnumParser } from '@/application/Common/Enum';
|
||||
import { filterEmptyStrings } from '@/application/Common/Text/FilterEmptyStrings';
|
||||
import { createScript, type ScriptFactory } from '@/domain/Executables/Script/ScriptFactory';
|
||||
import { parseDocs, type DocsParser } from '../DocumentationParser';
|
||||
import { ExecutableType } from '../Validation/ExecutableType';
|
||||
import { createExecutableDataValidator, type ExecutableValidator, type ExecutableValidatorFactory } from '../Validation/ExecutableValidator';
|
||||
@@ -38,6 +38,7 @@ export const parseScript: ScriptParser = (
|
||||
validateScript(data, validator);
|
||||
try {
|
||||
const script = scriptUtilities.createScript({
|
||||
executableId: data.name, // Pseudo-ID for uniqueness until real ID support
|
||||
name: data.name,
|
||||
code: parseCode(
|
||||
data,
|
||||
@@ -132,14 +133,6 @@ interface ScriptParserUtilities {
|
||||
readonly parseDocs: DocsParser;
|
||||
}
|
||||
|
||||
export type ScriptFactory = (
|
||||
...parameters: ConstructorParameters<typeof CollectionScript>
|
||||
) => Script;
|
||||
|
||||
const createScript: ScriptFactory = (...parameters) => {
|
||||
return new CollectionScript(...parameters);
|
||||
};
|
||||
|
||||
const DefaultUtilities: ScriptParserUtilities = {
|
||||
levelParser: createEnumParser(RecommendationLevel),
|
||||
createScript,
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import type { IEntity } from '@/infrastructure/Entity/IEntity';
|
||||
import type { RepositoryEntity } from './RepositoryEntity';
|
||||
|
||||
export interface ReadonlyRepository<TKey, TEntity extends IEntity<TKey>> {
|
||||
type EntityId = RepositoryEntity['id'];
|
||||
|
||||
export interface ReadonlyRepository<TEntity extends RepositoryEntity> {
|
||||
readonly length: number;
|
||||
getItems(predicate?: (entity: TEntity) => boolean): readonly TEntity[];
|
||||
getById(id: TKey): TEntity;
|
||||
exists(id: TKey): boolean;
|
||||
getById(id: EntityId): TEntity;
|
||||
exists(id: EntityId): boolean;
|
||||
}
|
||||
|
||||
export interface MutableRepository<TKey, TEntity extends IEntity<TKey>> {
|
||||
export interface MutableRepository<TEntity extends RepositoryEntity> {
|
||||
addItem(item: TEntity): void;
|
||||
addOrUpdateItem(item: TEntity): void;
|
||||
removeItem(id: TKey): void;
|
||||
removeItem(id: EntityId): void;
|
||||
}
|
||||
|
||||
export interface Repository<TKey, TEntity extends IEntity<TKey>>
|
||||
extends ReadonlyRepository<TKey, TEntity>, MutableRepository<TKey, TEntity> { }
|
||||
export interface Repository<TEntity extends RepositoryEntity>
|
||||
extends ReadonlyRepository<TEntity>, MutableRepository<TEntity> { }
|
||||
|
||||
6
src/application/Repository/RepositoryEntity.ts
Normal file
6
src/application/Repository/RepositoryEntity.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/** Aggregate root */
|
||||
export type RepositoryEntityId = string;
|
||||
|
||||
export interface RepositoryEntity {
|
||||
readonly id: RepositoryEntityId;
|
||||
}
|
||||
@@ -3207,7 +3207,7 @@ actions:
|
||||
parameters:
|
||||
appCapability: bluetoothSync
|
||||
-
|
||||
category: Disable app access to voice activation
|
||||
category: Disable app voice activation
|
||||
docs: |- # refactor-with-variable: Same • App Access Caution
|
||||
This category safeguards against unauthorized app activation via voice commands.
|
||||
|
||||
@@ -15671,7 +15671,7 @@ actions:
|
||||
data: '1'
|
||||
deleteOnRevert: 'true' # Missing by default since Windows 10 Pro (≥ 22H2) and Windows 11 Pro (≥ 23H2)
|
||||
-
|
||||
category: Minimize CPU usage during scans
|
||||
category: Disable intensive CPU usage during Defender scans
|
||||
children:
|
||||
-
|
||||
name: Minimize CPU usage during scans
|
||||
@@ -15866,7 +15866,7 @@ actions:
|
||||
category: Disable scanning archive files
|
||||
children:
|
||||
-
|
||||
name: Disable scanning archive files
|
||||
name: Disable Defender archive file scanning
|
||||
docs:
|
||||
- https://admx.help/?Category=Windows_10_2016&Policy=Microsoft.Policies.WindowsDefender::Scan_DisableArchiveScanning
|
||||
# Managing with MpPreference module:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { OperatingSystem } from './OperatingSystem';
|
||||
import type { IApplication } from './IApplication';
|
||||
import type { ICategoryCollection } from './ICategoryCollection';
|
||||
import type { ICategoryCollection } from './Collection/ICategoryCollection';
|
||||
import type { ProjectDetails } from './Project/ProjectDetails';
|
||||
|
||||
export class Application implements IApplication {
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { getEnumValues, assertInRange } from '@/application/Common/Enum';
|
||||
import { RecommendationLevel } from './Executables/Script/RecommendationLevel';
|
||||
import { OperatingSystem } from './OperatingSystem';
|
||||
import type { IEntity } from '../infrastructure/Entity/IEntity';
|
||||
import type { Category } from './Executables/Category/Category';
|
||||
import type { Script } from './Executables/Script/Script';
|
||||
import type { IScriptingDefinition } from './IScriptingDefinition';
|
||||
import { RecommendationLevel } from '../Executables/Script/RecommendationLevel';
|
||||
import { OperatingSystem } from '../OperatingSystem';
|
||||
import { validateCategoryCollection } from './Validation/CompositeCategoryCollectionValidator';
|
||||
import type { ExecutableId } from '../Executables/Identifiable';
|
||||
import type { Category } from '../Executables/Category/Category';
|
||||
import type { Script } from '../Executables/Script/Script';
|
||||
import type { IScriptingDefinition } from '../IScriptingDefinition';
|
||||
import type { ICategoryCollection } from './ICategoryCollection';
|
||||
import type { CategoryCollectionValidator } from './Validation/CategoryCollectionValidator';
|
||||
|
||||
export class CategoryCollection implements ICategoryCollection {
|
||||
public readonly os: OperatingSystem;
|
||||
@@ -22,22 +24,24 @@ export class CategoryCollection implements ICategoryCollection {
|
||||
|
||||
constructor(
|
||||
parameters: CategoryCollectionInitParameters,
|
||||
validate: CategoryCollectionValidator = validateCategoryCollection,
|
||||
) {
|
||||
this.os = parameters.os;
|
||||
this.actions = parameters.actions;
|
||||
this.scripting = parameters.scripting;
|
||||
|
||||
this.queryable = makeQueryable(this.actions);
|
||||
assertInRange(this.os, OperatingSystem);
|
||||
ensureValid(this.queryable);
|
||||
ensureNoDuplicates(this.queryable.allCategories);
|
||||
ensureNoDuplicates(this.queryable.allScripts);
|
||||
validate({
|
||||
allScripts: this.queryable.allScripts,
|
||||
allCategories: this.queryable.allCategories,
|
||||
operatingSystem: this.os,
|
||||
});
|
||||
}
|
||||
|
||||
public getCategory(categoryId: number): Category {
|
||||
const category = this.queryable.allCategories.find((c) => c.id === categoryId);
|
||||
public getCategory(executableId: ExecutableId): Category {
|
||||
const category = this.queryable.allCategories.find((c) => c.executableId === executableId);
|
||||
if (!category) {
|
||||
throw new Error(`Missing category with ID: "${categoryId}"`);
|
||||
throw new Error(`Missing category with ID: "${executableId}"`);
|
||||
}
|
||||
return category;
|
||||
}
|
||||
@@ -48,10 +52,10 @@ export class CategoryCollection implements ICategoryCollection {
|
||||
return scripts ?? [];
|
||||
}
|
||||
|
||||
public getScript(scriptId: string): Script {
|
||||
const script = this.queryable.allScripts.find((s) => s.id === scriptId);
|
||||
public getScript(executableId: ExecutableId): Script {
|
||||
const script = this.queryable.allScripts.find((s) => s.executableId === executableId);
|
||||
if (!script) {
|
||||
throw new Error(`missing script: ${scriptId}`);
|
||||
throw new Error(`Missing script: ${executableId}`);
|
||||
}
|
||||
return script;
|
||||
}
|
||||
@@ -65,21 +69,6 @@ export class CategoryCollection implements ICategoryCollection {
|
||||
}
|
||||
}
|
||||
|
||||
function ensureNoDuplicates<TKey>(entities: ReadonlyArray<IEntity<TKey>>) {
|
||||
const isUniqueInArray = (id: TKey, index: number, array: readonly TKey[]) => array
|
||||
.findIndex((otherId) => otherId === id) !== index;
|
||||
const duplicatedIds = entities
|
||||
.map((entity) => entity.id)
|
||||
.filter((id, index, array) => !isUniqueInArray(id, index, array))
|
||||
.filter(isUniqueInArray);
|
||||
if (duplicatedIds.length > 0) {
|
||||
const duplicatedIdsText = duplicatedIds.map((id) => `"${id}"`).join(',');
|
||||
throw new Error(
|
||||
`Duplicate entities are detected with following id(s): ${duplicatedIdsText}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export interface CategoryCollectionInitParameters {
|
||||
readonly os: OperatingSystem;
|
||||
readonly actions: ReadonlyArray<Category>;
|
||||
@@ -92,35 +81,12 @@ interface QueryableCollection {
|
||||
readonly scriptsByLevel: Map<RecommendationLevel, readonly Script[]>;
|
||||
}
|
||||
|
||||
function ensureValid(application: QueryableCollection) {
|
||||
ensureValidCategories(application.allCategories);
|
||||
ensureValidScripts(application.allScripts);
|
||||
}
|
||||
|
||||
function ensureValidCategories(allCategories: readonly Category[]) {
|
||||
if (!allCategories.length) {
|
||||
throw new Error('must consist of at least one category');
|
||||
}
|
||||
}
|
||||
|
||||
function ensureValidScripts(allScripts: readonly Script[]) {
|
||||
if (!allScripts.length) {
|
||||
throw new Error('must consist of at least one script');
|
||||
}
|
||||
const missingRecommendationLevels = getEnumValues(RecommendationLevel)
|
||||
.filter((level) => allScripts.every((script) => script.level !== level));
|
||||
if (missingRecommendationLevels.length > 0) {
|
||||
throw new Error('none of the scripts are recommended as'
|
||||
+ ` "${missingRecommendationLevels.map((level) => RecommendationLevel[level]).join(', "')}".`);
|
||||
}
|
||||
}
|
||||
|
||||
function flattenApplication(
|
||||
function flattenCategoryHierarchy(
|
||||
categories: ReadonlyArray<Category>,
|
||||
): [Category[], Script[]] {
|
||||
const [subCategories, subScripts] = (categories || [])
|
||||
// Parse children
|
||||
.map((category) => flattenApplication(category.subCategories))
|
||||
.map((category) => flattenCategoryHierarchy(category.subcategories))
|
||||
// Flatten results
|
||||
.reduce(([previousCategories, previousScripts], [currentCategories, currentScripts]) => {
|
||||
return [
|
||||
@@ -143,7 +109,7 @@ function flattenApplication(
|
||||
function makeQueryable(
|
||||
actions: ReadonlyArray<Category>,
|
||||
): QueryableCollection {
|
||||
const flattened = flattenApplication(actions);
|
||||
const flattened = flattenCategoryHierarchy(actions);
|
||||
return {
|
||||
allCategories: flattened[0],
|
||||
allScripts: flattened[1],
|
||||
@@ -3,6 +3,7 @@ import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { RecommendationLevel } from '@/domain/Executables/Script/RecommendationLevel';
|
||||
import type { Script } from '@/domain/Executables/Script/Script';
|
||||
import type { Category } from '@/domain/Executables/Category/Category';
|
||||
import type { ExecutableId } from '../Executables/Identifiable';
|
||||
|
||||
export interface ICategoryCollection {
|
||||
readonly scripting: IScriptingDefinition;
|
||||
@@ -12,8 +13,8 @@ export interface ICategoryCollection {
|
||||
readonly actions: ReadonlyArray<Category>;
|
||||
|
||||
getScriptsByLevel(level: RecommendationLevel): ReadonlyArray<Script>;
|
||||
getCategory(categoryId: number): Category;
|
||||
getScript(scriptId: string): Script;
|
||||
getCategory(categoryId: ExecutableId): Category;
|
||||
getScript(scriptId: ExecutableId): Script;
|
||||
getAllScripts(): ReadonlyArray<Script>;
|
||||
getAllCategories(): ReadonlyArray<Category>;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { Category } from '@/domain/Executables/Category/Category';
|
||||
import type { Script } from '@/domain/Executables/Script/Script';
|
||||
import type { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
|
||||
export interface CategoryCollectionValidationContext {
|
||||
readonly allScripts: readonly Script[];
|
||||
readonly allCategories: readonly Category[];
|
||||
readonly operatingSystem: OperatingSystem;
|
||||
}
|
||||
|
||||
export interface CategoryCollectionValidator {
|
||||
(
|
||||
context: CategoryCollectionValidationContext,
|
||||
): void;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { ensurePresenceOfAtLeastOneScript } from './Rules/EnsurePresenceOfAtLeastOneScript';
|
||||
import { ensurePresenceOfAtLeastOneCategory } from './Rules/EnsurePresenceOfAtLeastOneCategory';
|
||||
import { ensureUniqueIdsAcrossExecutables } from './Rules/EnsureUniqueIdsAcrossExecutables';
|
||||
import { ensureKnownOperatingSystem } from './Rules/EnsureKnownOperatingSystem';
|
||||
import type { CategoryCollectionValidationContext, CategoryCollectionValidator } from './CategoryCollectionValidator';
|
||||
|
||||
export type CompositeCategoryCollectionValidator = CategoryCollectionValidator & {
|
||||
(
|
||||
...args: [
|
||||
...Parameters<CategoryCollectionValidator>,
|
||||
(readonly CategoryCollectionValidator[])?,
|
||||
]
|
||||
): void;
|
||||
};
|
||||
|
||||
export const validateCategoryCollection: CompositeCategoryCollectionValidator = (
|
||||
context: CategoryCollectionValidationContext,
|
||||
validators: readonly CategoryCollectionValidator[] = DefaultValidators,
|
||||
) => {
|
||||
if (!validators.length) {
|
||||
throw new Error('No validators provided.');
|
||||
}
|
||||
for (const validate of validators) {
|
||||
validate(context);
|
||||
}
|
||||
};
|
||||
|
||||
const DefaultValidators: readonly CategoryCollectionValidator[] = [
|
||||
ensureKnownOperatingSystem,
|
||||
ensurePresenceOfAtLeastOneScript,
|
||||
ensurePresenceOfAtLeastOneCategory,
|
||||
ensureUniqueIdsAcrossExecutables,
|
||||
];
|
||||
@@ -0,0 +1,9 @@
|
||||
import { assertInRange } from '@/application/Common/Enum';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import type { CategoryCollectionValidator } from '../CategoryCollectionValidator';
|
||||
|
||||
export const ensureKnownOperatingSystem: CategoryCollectionValidator = (
|
||||
context,
|
||||
) => {
|
||||
assertInRange(context.operatingSystem, OperatingSystem);
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
import { getEnumValues } from '@/application/Common/Enum';
|
||||
import { RecommendationLevel } from '@/domain/Executables/Script/RecommendationLevel';
|
||||
import type { Script } from '@/domain/Executables/Script/Script';
|
||||
import type { CategoryCollectionValidator } from '../CategoryCollectionValidator';
|
||||
|
||||
export const ensurePresenceOfAllRecommendationLevels: CategoryCollectionValidator = (
|
||||
context,
|
||||
) => {
|
||||
const unrepresentedRecommendationLevels = getUnrepresentedRecommendationLevels(
|
||||
context.allScripts,
|
||||
);
|
||||
if (unrepresentedRecommendationLevels.length === 0) {
|
||||
return;
|
||||
}
|
||||
const formattedRecommendationLevels = unrepresentedRecommendationLevels
|
||||
.map((level) => getDisplayName(level))
|
||||
.join(', ');
|
||||
throw new Error(`Missing recommendation levels: ${formattedRecommendationLevels}.`);
|
||||
};
|
||||
|
||||
function getUnrepresentedRecommendationLevels(
|
||||
scripts: readonly Script[],
|
||||
): (RecommendationLevel | undefined)[] {
|
||||
const expectedLevels = [
|
||||
undefined,
|
||||
...getEnumValues(RecommendationLevel),
|
||||
];
|
||||
return expectedLevels.filter(
|
||||
(level) => scripts.every((script) => script.level !== level),
|
||||
);
|
||||
}
|
||||
|
||||
function getDisplayName(level: RecommendationLevel | undefined): string {
|
||||
return level === undefined ? 'None' : RecommendationLevel[level];
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { CategoryCollectionValidator } from '../CategoryCollectionValidator';
|
||||
|
||||
export const ensurePresenceOfAtLeastOneCategory: CategoryCollectionValidator = (
|
||||
context,
|
||||
) => {
|
||||
if (!context.allCategories.length) {
|
||||
throw new Error('Collection must have at least one category');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { CategoryCollectionValidator } from '../CategoryCollectionValidator';
|
||||
|
||||
export const ensurePresenceOfAtLeastOneScript: CategoryCollectionValidator = (
|
||||
context,
|
||||
) => {
|
||||
if (!context.allScripts.length) {
|
||||
throw new Error('Collection must have at least one script');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { Identifiable } from '@/domain/Executables/Identifiable';
|
||||
import type { CategoryCollectionValidator } from '../CategoryCollectionValidator';
|
||||
|
||||
export const ensureUniqueIdsAcrossExecutables: CategoryCollectionValidator = (
|
||||
context,
|
||||
) => {
|
||||
const allExecutables: readonly Identifiable[] = [
|
||||
...context.allCategories,
|
||||
...context.allScripts,
|
||||
];
|
||||
ensureNoDuplicateIds(allExecutables);
|
||||
};
|
||||
|
||||
function ensureNoDuplicateIds(
|
||||
executables: readonly Identifiable[],
|
||||
) {
|
||||
const duplicateExecutables = getExecutablesWithDuplicateIds(executables);
|
||||
if (duplicateExecutables.length === 0) {
|
||||
return;
|
||||
}
|
||||
const formattedDuplicateIds = duplicateExecutables.map(
|
||||
(executable) => `"${executable.executableId}"`,
|
||||
).join(', ');
|
||||
throw new Error(`Duplicate executable IDs found: ${formattedDuplicateIds}`);
|
||||
}
|
||||
|
||||
function getExecutablesWithDuplicateIds(
|
||||
executables: readonly Identifiable[],
|
||||
): Identifiable[] {
|
||||
return executables
|
||||
.filter(
|
||||
(executable, index, array) => {
|
||||
const otherIndex = array.findIndex(
|
||||
(otherExecutable) => haveIdenticalIds(executable, otherExecutable),
|
||||
);
|
||||
return otherIndex !== index;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function haveIdenticalIds(first: Identifiable, second: Identifiable): boolean {
|
||||
return first.executableId === second.executableId;
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
import type { Script } from '../Script/Script';
|
||||
import type { Executable } from '../Executable';
|
||||
|
||||
export interface Category extends Executable<number> {
|
||||
readonly id: number;
|
||||
export interface Category extends Executable {
|
||||
readonly name: string;
|
||||
readonly subCategories: ReadonlyArray<Category>;
|
||||
readonly subcategories: ReadonlyArray<Category>;
|
||||
readonly scripts: ReadonlyArray<Script>;
|
||||
includes(script: Script): boolean;
|
||||
getAllScriptsRecursively(): ReadonlyArray<Script>;
|
||||
|
||||
@@ -1,29 +1,51 @@
|
||||
import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
|
||||
import type { Category } from './Category';
|
||||
import type { Script } from '../Script/Script';
|
||||
import type { Category } from '@/domain/Executables/Category/Category';
|
||||
import type { Script } from '@/domain/Executables/Script/Script';
|
||||
import type { ExecutableId } from '../Identifiable';
|
||||
|
||||
export class CollectionCategory extends BaseEntity<number> implements Category {
|
||||
private allSubScripts?: ReadonlyArray<Script> = undefined;
|
||||
export type CategoryFactory = (
|
||||
parameters: CategoryInitParameters,
|
||||
) => Category;
|
||||
|
||||
export interface CategoryInitParameters {
|
||||
readonly executableId: ExecutableId;
|
||||
readonly name: string;
|
||||
readonly docs: ReadonlyArray<string>;
|
||||
readonly subcategories: ReadonlyArray<Category>;
|
||||
readonly scripts: ReadonlyArray<Script>;
|
||||
}
|
||||
|
||||
export const createCategory: CategoryFactory = (
|
||||
parameters,
|
||||
) => {
|
||||
return new CollectionCategory(parameters);
|
||||
};
|
||||
|
||||
class CollectionCategory implements Category {
|
||||
public readonly executableId: ExecutableId;
|
||||
|
||||
public readonly name: string;
|
||||
|
||||
public readonly docs: ReadonlyArray<string>;
|
||||
|
||||
public readonly subCategories: ReadonlyArray<Category>;
|
||||
public readonly subcategories: ReadonlyArray<Category>;
|
||||
|
||||
public readonly scripts: ReadonlyArray<Script>;
|
||||
|
||||
private allSubScripts?: ReadonlyArray<Script> = undefined;
|
||||
|
||||
constructor(parameters: CategoryInitParameters) {
|
||||
super(parameters.id);
|
||||
validateParameters(parameters);
|
||||
this.executableId = parameters.executableId;
|
||||
this.name = parameters.name;
|
||||
this.docs = parameters.docs;
|
||||
this.subCategories = parameters.subcategories;
|
||||
this.subcategories = parameters.subcategories;
|
||||
this.scripts = parameters.scripts;
|
||||
}
|
||||
|
||||
public includes(script: Script): boolean {
|
||||
return this.getAllScriptsRecursively().some((childScript) => childScript.id === script.id);
|
||||
return this
|
||||
.getAllScriptsRecursively()
|
||||
.some((childScript) => childScript.executableId === script.executableId);
|
||||
}
|
||||
|
||||
public getAllScriptsRecursively(): readonly Script[] {
|
||||
@@ -34,22 +56,17 @@ export class CollectionCategory extends BaseEntity<number> implements Category {
|
||||
}
|
||||
}
|
||||
|
||||
export interface CategoryInitParameters {
|
||||
readonly id: number;
|
||||
readonly name: string;
|
||||
readonly docs: ReadonlyArray<string>;
|
||||
readonly subcategories: ReadonlyArray<Category>;
|
||||
readonly scripts: ReadonlyArray<Script>;
|
||||
}
|
||||
|
||||
function parseScriptsRecursively(category: Category): ReadonlyArray<Script> {
|
||||
return [
|
||||
...category.scripts,
|
||||
...category.subCategories.flatMap((c) => c.getAllScriptsRecursively()),
|
||||
...category.subcategories.flatMap((c) => c.getAllScriptsRecursively()),
|
||||
];
|
||||
}
|
||||
|
||||
function validateParameters(parameters: CategoryInitParameters) {
|
||||
if (!parameters.executableId) {
|
||||
throw new Error('missing ID');
|
||||
}
|
||||
if (!parameters.name) {
|
||||
throw new Error('missing name');
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { IEntity } from '@/infrastructure/Entity/IEntity';
|
||||
import type { Documentable } from './Documentable';
|
||||
import type { Identifiable } from './Identifiable';
|
||||
|
||||
export interface Executable<TExecutableKey>
|
||||
extends Documentable, IEntity<TExecutableKey> {
|
||||
export interface Executable
|
||||
extends Documentable, Identifiable {
|
||||
}
|
||||
|
||||
5
src/domain/Executables/Identifiable.ts
Normal file
5
src/domain/Executables/Identifiable.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export type ExecutableId = string;
|
||||
|
||||
export interface Identifiable {
|
||||
readonly executableId: ExecutableId;
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import type { Executable } from '../Executable';
|
||||
import type { Documentable } from '../Documentable';
|
||||
import type { ScriptCode } from './Code/ScriptCode';
|
||||
|
||||
export interface Script extends Executable<string>, Documentable {
|
||||
export interface Script extends Executable, Documentable {
|
||||
readonly name: string;
|
||||
readonly level?: RecommendationLevel;
|
||||
readonly code: ScriptCode;
|
||||
|
||||
@@ -1,9 +1,27 @@
|
||||
import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
|
||||
import { RecommendationLevel } from './RecommendationLevel';
|
||||
import type { Script } from './Script';
|
||||
import type { ScriptCode } from './Code/ScriptCode';
|
||||
import type { Script } from './Script';
|
||||
import type { ExecutableId } from '../Identifiable';
|
||||
|
||||
export interface ScriptInitParameters {
|
||||
readonly executableId: ExecutableId;
|
||||
readonly name: string;
|
||||
readonly code: ScriptCode;
|
||||
readonly docs: ReadonlyArray<string>;
|
||||
readonly level?: RecommendationLevel;
|
||||
}
|
||||
|
||||
export type ScriptFactory = (
|
||||
parameters: ScriptInitParameters,
|
||||
) => Script;
|
||||
|
||||
export const createScript: ScriptFactory = (parameters) => {
|
||||
return new CollectionScript(parameters);
|
||||
};
|
||||
|
||||
class CollectionScript implements Script {
|
||||
public readonly executableId: ExecutableId;
|
||||
|
||||
export class CollectionScript extends BaseEntity<string> implements Script {
|
||||
public readonly name: string;
|
||||
|
||||
public readonly code: ScriptCode;
|
||||
@@ -13,7 +31,7 @@ export class CollectionScript extends BaseEntity<string> implements Script {
|
||||
public readonly level?: RecommendationLevel;
|
||||
|
||||
constructor(parameters: ScriptInitParameters) {
|
||||
super(parameters.name);
|
||||
this.executableId = parameters.executableId;
|
||||
this.name = parameters.name;
|
||||
this.code = parameters.code;
|
||||
this.docs = parameters.docs;
|
||||
@@ -26,13 +44,6 @@ export class CollectionScript extends BaseEntity<string> implements Script {
|
||||
}
|
||||
}
|
||||
|
||||
export interface ScriptInitParameters {
|
||||
readonly name: string;
|
||||
readonly code: ScriptCode;
|
||||
readonly docs: ReadonlyArray<string>;
|
||||
readonly level?: RecommendationLevel;
|
||||
}
|
||||
|
||||
function validateLevel(level?: RecommendationLevel) {
|
||||
if (level !== undefined && !(level in RecommendationLevel)) {
|
||||
throw new Error(`invalid level: ${level}`);
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ICategoryCollection } from './ICategoryCollection';
|
||||
import type { ICategoryCollection } from './Collection/ICategoryCollection';
|
||||
import type { ProjectDetails } from './Project/ProjectDetails';
|
||||
import type { OperatingSystem } from './OperatingSystem';
|
||||
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import { isNumber } from '@/TypeHelpers';
|
||||
import type { IEntity } from './IEntity';
|
||||
|
||||
export abstract class BaseEntity<TId> implements IEntity<TId> {
|
||||
protected constructor(public id: TId) {
|
||||
if (!isNumber(id) && !id) {
|
||||
throw new Error('Id cannot be null or empty');
|
||||
}
|
||||
}
|
||||
|
||||
public equals(otherId: TId): boolean {
|
||||
return this.id === otherId;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
/** Aggregate root */
|
||||
export interface IEntity<TId> {
|
||||
id: TId;
|
||||
equals(other: TId): boolean;
|
||||
}
|
||||
@@ -1,12 +1,15 @@
|
||||
import type { Repository } from '../../application/Repository/Repository';
|
||||
import type { IEntity } from '../Entity/IEntity';
|
||||
import type { RepositoryEntity, RepositoryEntityId } from '../../application/Repository/RepositoryEntity';
|
||||
|
||||
export class InMemoryRepository<TKey, TEntity extends IEntity<TKey>>
|
||||
implements Repository<TKey, TEntity> {
|
||||
export class InMemoryRepository<TEntity extends RepositoryEntity>
|
||||
implements Repository<TEntity> {
|
||||
private readonly items: TEntity[];
|
||||
|
||||
constructor(items?: TEntity[]) {
|
||||
this.items = items ?? new Array<TEntity>();
|
||||
constructor(items?: readonly TEntity[]) {
|
||||
this.items = new Array<TEntity>();
|
||||
if (items) {
|
||||
this.items.push(...items);
|
||||
}
|
||||
}
|
||||
|
||||
public get length(): number {
|
||||
@@ -17,7 +20,7 @@ implements Repository<TKey, TEntity> {
|
||||
return predicate ? this.items.filter(predicate) : this.items;
|
||||
}
|
||||
|
||||
public getById(id: TKey): TEntity {
|
||||
public getById(id: RepositoryEntityId): TEntity {
|
||||
const items = this.getItems((entity) => entity.id === id);
|
||||
if (!items.length) {
|
||||
throw new Error(`missing item: ${id}`);
|
||||
@@ -39,7 +42,7 @@ implements Repository<TKey, TEntity> {
|
||||
this.items.push(item);
|
||||
}
|
||||
|
||||
public removeItem(id: TKey): void {
|
||||
public removeItem(id: RepositoryEntityId): void {
|
||||
const index = this.items.findIndex((item) => item.id === id);
|
||||
if (index === -1) {
|
||||
throw new Error(`Cannot remove (id: ${id}) as it does not exist`);
|
||||
@@ -47,7 +50,7 @@ implements Repository<TKey, TEntity> {
|
||||
this.items.splice(index, 1);
|
||||
}
|
||||
|
||||
public exists(id: TKey): boolean {
|
||||
public exists(id: RepositoryEntityId): boolean {
|
||||
const index = this.items.findIndex((item) => item.id === id);
|
||||
return index !== -1;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import type { ExecutableId } from '@/domain/Executables/Identifiable';
|
||||
|
||||
export enum NodeType {
|
||||
Script,
|
||||
Category,
|
||||
}
|
||||
|
||||
export interface NodeMetadata {
|
||||
readonly id: string;
|
||||
readonly executableId: ExecutableId;
|
||||
readonly text: string;
|
||||
readonly isReversible: boolean;
|
||||
readonly docs: ReadonlyArray<string>;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
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';
|
||||
import type { TreeNodeId } from '../../TreeView/Node/TreeNode';
|
||||
|
||||
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);
|
||||
constructor(nodeId: TreeNodeId, collection: ICategoryCollection) {
|
||||
this.categoryId = createExecutableIdFromNodeId(nodeId);
|
||||
this.scriptReverters = createScriptReverters(this.categoryId, collection);
|
||||
}
|
||||
|
||||
@@ -37,12 +39,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));
|
||||
}
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
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';
|
||||
import type { Reverter } from './Reverter';
|
||||
|
||||
export function getReverter(node: NodeMetadata, collection: ICategoryCollection): Reverter {
|
||||
export function getReverter(
|
||||
node: NodeMetadata,
|
||||
collection: ICategoryCollection,
|
||||
): Reverter {
|
||||
switch (node.type) {
|
||||
case NodeType.Category:
|
||||
return new CategoryReverter(node.id, collection);
|
||||
return new CategoryReverter(node.executableId, collection);
|
||||
case NodeType.Script:
|
||||
return new ScriptReverter(node.id);
|
||||
return new ScriptReverter(node.executableId);
|
||||
default:
|
||||
throw new Error('Unknown script type');
|
||||
}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
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 type { ExecutableId } from '@/domain/Executables/Identifiable';
|
||||
import { createExecutableIdFromNodeId } from '../../TreeViewAdapter/CategoryNodeMetadataConverter';
|
||||
import type { Reverter } from './Reverter';
|
||||
import type { TreeNodeId } from '../../TreeView/Node/TreeNode';
|
||||
|
||||
export class ScriptReverter implements Reverter {
|
||||
private readonly scriptId: string;
|
||||
private readonly scriptId: ExecutableId;
|
||||
|
||||
constructor(nodeId: string) {
|
||||
this.scriptId = getScriptId(nodeId);
|
||||
constructor(nodeId: TreeNodeId) {
|
||||
this.scriptId = createExecutableIdFromNodeId(nodeId);
|
||||
}
|
||||
|
||||
public getState(selectedScripts: ReadonlyArray<SelectedScript>): boolean {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import type { ReadOnlyTreeNode, TreeNode } from '../../../Node/TreeNode';
|
||||
import type { ReadOnlyTreeNode, TreeNode, TreeNodeId } from '../../../Node/TreeNode';
|
||||
|
||||
export interface ReadOnlyQueryableNodes {
|
||||
readonly rootNodes: readonly ReadOnlyTreeNode[];
|
||||
readonly flattenedNodes: readonly ReadOnlyTreeNode[];
|
||||
|
||||
getNodeById(id: string): ReadOnlyTreeNode;
|
||||
getNodeById(nodeId: TreeNodeId): ReadOnlyTreeNode;
|
||||
}
|
||||
|
||||
export interface QueryableNodes extends ReadOnlyQueryableNodes {
|
||||
readonly rootNodes: readonly TreeNode[];
|
||||
readonly flattenedNodes: readonly TreeNode[];
|
||||
|
||||
getNodeById(id: string): TreeNode;
|
||||
getNodeById(nodeId: TreeNodeId): TreeNode;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { QueryableNodes } from './QueryableNodes';
|
||||
import type { TreeNode } from '../../../Node/TreeNode';
|
||||
import type { TreeNode, TreeNodeId } from '../../../Node/TreeNode';
|
||||
|
||||
export class TreeNodeNavigator implements QueryableNodes {
|
||||
public readonly flattenedNodes: readonly TreeNode[];
|
||||
@@ -8,10 +8,10 @@ export class TreeNodeNavigator implements QueryableNodes {
|
||||
this.flattenedNodes = flattenNodes(rootNodes);
|
||||
}
|
||||
|
||||
public getNodeById(id: string): TreeNode {
|
||||
const foundNode = this.flattenedNodes.find((node) => node.id === id);
|
||||
public getNodeById(nodeId: TreeNodeId): TreeNode {
|
||||
const foundNode = this.flattenedNodes.find((node) => node.id === nodeId);
|
||||
if (!foundNode) {
|
||||
throw new Error(`Node could not be found: ${id}`);
|
||||
throw new Error(`Node could not be found: ${nodeId}`);
|
||||
}
|
||||
return foundNode;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: () => [],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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),
|
||||
executableId: createNodeIdForExecutable(category),
|
||||
type: NodeType.Category,
|
||||
text: category.name,
|
||||
children,
|
||||
@@ -68,7 +63,7 @@ function convertCategoryToNode(
|
||||
|
||||
function convertScriptToNode(script: Script): NodeMetadata {
|
||||
return {
|
||||
id: getScriptNodeId(script),
|
||||
executableId: createNodeIdForExecutable(script),
|
||||
type: NodeType.Script,
|
||||
text: script.name,
|
||||
children: [],
|
||||
|
||||
@@ -14,7 +14,7 @@ export function getNodeMetadata(
|
||||
|
||||
export function convertToNodeInput(metadata: NodeMetadata): TreeInputNodeData {
|
||||
return {
|
||||
id: metadata.id,
|
||||
id: metadata.executableId,
|
||||
children: convertChildren(metadata.children, convertToNodeInput),
|
||||
data: metadata,
|
||||
};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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[] {
|
||||
|
||||
Reference in New Issue
Block a user