diff --git a/docs/collection-files.md b/docs/collection-files.md index 414fd8ea..c4878397 100644 --- a/docs/collection-files.md +++ b/docs/collection-files.md @@ -30,6 +30,8 @@ Related documentation: ### Executables +They represent independently executable actions with documentation and reversibility. + An Executable is a logical entity that can - execute once compiled, diff --git a/src/application/Context/ApplicationContext.ts b/src/application/Context/ApplicationContext.ts index 7829737f..4828947a 100644 --- a/src/application/Context/ApplicationContext.ts +++ b/src/application/Context/ApplicationContext.ts @@ -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'; diff --git a/src/application/Context/State/CategoryCollectionState.ts b/src/application/Context/State/CategoryCollectionState.ts index 253a7133..6d37ed8f 100644 --- a/src/application/Context/State/CategoryCollectionState.ts +++ b/src/application/Context/State/CategoryCollectionState.ts @@ -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'; diff --git a/src/application/Context/State/Code/Event/CodeChangedEvent.ts b/src/application/Context/State/Code/Event/CodeChangedEvent.ts index 916483f2..41398093 100644 --- a/src/application/Context/State/Code/Event/CodeChangedEvent.ts +++ b/src/application/Context/State/Code/Event/CodeChangedEvent.ts @@ -37,12 +37,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 { const position = [...this.scripts.entries()] - .filter(([s]) => s.id === scriptId) + .filter(([s]) => s.executableId === scriptId) .map(([, pos]) => pos) .at(0); if (!position) { diff --git a/src/application/Context/State/Filter/AdaptiveFilterContext.ts b/src/application/Context/State/Filter/AdaptiveFilterContext.ts index 902ec523..6ecba461 100644 --- a/src/application/Context/State/Filter/AdaptiveFilterContext.ts +++ b/src/application/Context/State/Filter/AdaptiveFilterContext.ts @@ -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'; diff --git a/src/application/Context/State/Filter/Strategy/FilterStrategy.ts b/src/application/Context/State/Filter/Strategy/FilterStrategy.ts index 2bda8de2..2a4a7862 100644 --- a/src/application/Context/State/Filter/Strategy/FilterStrategy.ts +++ b/src/application/Context/State/Filter/Strategy/FilterStrategy.ts @@ -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 { diff --git a/src/application/Context/State/Filter/Strategy/LinearFilterStrategy.ts b/src/application/Context/State/Filter/Strategy/LinearFilterStrategy.ts index bb3bfb04..327bdf80 100644 --- a/src/application/Context/State/Filter/Strategy/LinearFilterStrategy.ts +++ b/src/application/Context/State/Filter/Strategy/LinearFilterStrategy.ts @@ -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'; diff --git a/src/application/Context/State/ICategoryCollectionState.ts b/src/application/Context/State/ICategoryCollectionState.ts index 8a485679..3d9bdaa5 100644 --- a/src/application/Context/State/ICategoryCollectionState.ts +++ b/src/application/Context/State/ICategoryCollectionState.ts @@ -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'; diff --git a/src/application/Context/State/Selection/Category/CategorySelectionChange.ts b/src/application/Context/State/Selection/Category/CategorySelectionChange.ts index a944a3f6..36b7de90 100644 --- a/src/application/Context/State/Selection/Category/CategorySelectionChange.ts +++ b/src/application/Context/State/Selection/Category/CategorySelectionChange.ts @@ -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; } diff --git a/src/application/Context/State/Selection/Category/ScriptToCategorySelectionMapper.ts b/src/application/Context/State/Selection/Category/ScriptToCategorySelectionMapper.ts index a856b0ea..a2916926 100644 --- a/src/application/Context/State/Selection/Category/ScriptToCategorySelectionMapper.ts +++ b/src/application/Context/State/Selection/Category/ScriptToCategorySelectionMapper.ts @@ -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, }, diff --git a/src/application/Context/State/Selection/Script/DebouncedScriptSelection.ts b/src/application/Context/State/Selection/Script/DebouncedScriptSelection.ts index 79a7dd46..23184f20 100644 --- a/src/application/Context/State/Selection/Script/DebouncedScriptSelection.ts +++ b/src/application/Context/State/Selection/Script/DebouncedScriptSelection.ts @@ -2,7 +2,7 @@ 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 { UserSelectedScript } from './UserSelectedScript'; import type { ScriptSelection } from './ScriptSelection'; @@ -16,7 +16,7 @@ export type DebounceFunction = typeof batchedDebounce>(); - private readonly scripts: Repository; + private readonly scripts: Repository; public readonly processChanges: ScriptSelection['processChanges']; @@ -25,7 +25,7 @@ export class DebouncedScriptSelection implements ScriptSelection { selectedScripts: ReadonlyArray, debounce: DebounceFunction = batchedDebounce, ) { - this.scripts = new InMemoryRepository(); + this.scripts = new InMemoryRepository(); for (const script of selectedScripts) { this.scripts.addItem(script); } @@ -49,7 +49,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,9 +116,9 @@ 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 { @@ -152,24 +152,24 @@ function assertNonEmptyScriptSelection(selectedItems: readonly Script[]) { } function getScriptIdsToBeSelected( - existingItems: ReadonlyRepository, + existingItems: ReadonlyRepository, 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, + existingItems: ReadonlyRepository, 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; } diff --git a/src/application/Context/State/Selection/Script/SelectedScript.ts b/src/application/Context/State/Selection/Script/SelectedScript.ts index c0059f6e..858dbbbb 100644 --- a/src/application/Context/State/Selection/Script/SelectedScript.ts +++ b/src/application/Context/State/Selection/Script/SelectedScript.ts @@ -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 { +export interface SelectedScript extends RepositoryEntity { readonly script: Script; readonly revert: boolean; } diff --git a/src/application/Context/State/Selection/Script/UserSelectedScript.ts b/src/application/Context/State/Selection/Script/UserSelectedScript.ts index 315da002..01b40b38 100644 --- a/src/application/Context/State/Selection/Script/UserSelectedScript.ts +++ b/src/application/Context/State/Selection/Script/UserSelectedScript.ts @@ -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 { 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.`); } } } diff --git a/src/application/Context/State/Selection/UserSelectionFacade.ts b/src/application/Context/State/Selection/UserSelectionFacade.ts index 7667d983..6fbc12bc 100644 --- a/src/application/Context/State/Selection/UserSelectionFacade.ts +++ b/src/application/Context/State/Selection/UserSelectionFacade.ts @@ -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'; diff --git a/src/application/Parser/CategoryCollectionParser.ts b/src/application/Parser/CategoryCollectionParser.ts index be10a9d0..b7e71420 100644 --- a/src/application/Parser/CategoryCollectionParser.ts +++ b/src/application/Parser/CategoryCollectionParser.ts @@ -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'; diff --git a/src/application/Parser/Executable/CategoryParser.ts b/src/application/Parser/Executable/CategoryParser.ts index 88d77a1b..9b4790e3 100644 --- a/src/application/Parser/Executable/CategoryParser.ts +++ b/src/application/Parser/Executable/CategoryParser.ts @@ -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, // arbitrary ID 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 -) => 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, diff --git a/src/application/Parser/Executable/Script/ScriptParser.ts b/src/application/Parser/Executable/Script/ScriptParser.ts index d7be9d0c..bad08345 100644 --- a/src/application/Parser/Executable/Script/ScriptParser.ts +++ b/src/application/Parser/Executable/Script/ScriptParser.ts @@ -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, // arbitrary ID name: data.name, code: parseCode( data, @@ -132,14 +133,6 @@ interface ScriptParserUtilities { readonly parseDocs: DocsParser; } -export type ScriptFactory = ( - ...parameters: ConstructorParameters -) => Script; - -const createScript: ScriptFactory = (...parameters) => { - return new CollectionScript(...parameters); -}; - const DefaultUtilities: ScriptParserUtilities = { levelParser: createEnumParser(RecommendationLevel), createScript, diff --git a/src/application/Repository/Repository.ts b/src/application/Repository/Repository.ts index 6232598a..0b2d83d7 100644 --- a/src/application/Repository/Repository.ts +++ b/src/application/Repository/Repository.ts @@ -1,17 +1,19 @@ -import type { IEntity } from '@/infrastructure/Entity/IEntity'; +import type { RepositoryEntity } from './RepositoryEntity'; -export interface ReadonlyRepository> { +type EntityId = RepositoryEntity['id']; + +export interface ReadonlyRepository { 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> { +export interface MutableRepository { addItem(item: TEntity): void; addOrUpdateItem(item: TEntity): void; - removeItem(id: TKey): void; + removeItem(id: EntityId): void; } -export interface Repository> - extends ReadonlyRepository, MutableRepository { } +export interface Repository + extends ReadonlyRepository, MutableRepository { } diff --git a/src/application/Repository/RepositoryEntity.ts b/src/application/Repository/RepositoryEntity.ts new file mode 100644 index 00000000..1603cd97 --- /dev/null +++ b/src/application/Repository/RepositoryEntity.ts @@ -0,0 +1,6 @@ +/** Aggregate root */ +export type RepositoryEntityId = string; + +export interface RepositoryEntity { + readonly id: RepositoryEntityId; +} diff --git a/src/application/collections/windows.yaml b/src/application/collections/windows.yaml index b1d7bf12..d582f2f5 100644 --- a/src/application/collections/windows.yaml +++ b/src/application/collections/windows.yaml @@ -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: diff --git a/src/domain/Application.ts b/src/domain/Application.ts index 2fc1230d..77e85796 100644 --- a/src/domain/Application.ts +++ b/src/domain/Application.ts @@ -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 { diff --git a/src/domain/CategoryCollection.ts b/src/domain/Collection/CategoryCollection.ts similarity index 54% rename from src/domain/CategoryCollection.ts rename to src/domain/Collection/CategoryCollection.ts index 71d8c61e..06527003 100644 --- a/src/domain/CategoryCollection.ts +++ b/src/domain/Collection/CategoryCollection.ts @@ -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: string): 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(entities: ReadonlyArray>) { - 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; @@ -92,35 +81,12 @@ interface QueryableCollection { readonly scriptsByLevel: Map; } -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[], 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, ): QueryableCollection { - const flattened = flattenApplication(actions); + const flattened = flattenCategoryHierarchy(actions); return { allCategories: flattened[0], allScripts: flattened[1], diff --git a/src/domain/ICategoryCollection.ts b/src/domain/Collection/ICategoryCollection.ts similarity index 82% rename from src/domain/ICategoryCollection.ts rename to src/domain/Collection/ICategoryCollection.ts index c7b7d799..ac7879fc 100644 --- a/src/domain/ICategoryCollection.ts +++ b/src/domain/Collection/ICategoryCollection.ts @@ -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; getScriptsByLevel(level: RecommendationLevel): ReadonlyArray +@/domain/Collection/ICategoryCollection diff --git a/src/presentation/components/Scripts/View/Cards/CardList.vue b/src/presentation/components/Scripts/View/Cards/CardList.vue index dd831327..f054265e 100644 --- a/src/presentation/components/Scripts/View/Cards/CardList.vue +++ b/src/presentation/components/Scripts/View/Cards/CardList.vue @@ -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(); - const categoryIds = computed( - () => currentState.value.collection.actions.map((category) => category.id), + const categoryIds = computed( + () => currentState.value.collection.actions.map((category) => category.executableId), ); - const activeCategoryId = ref(undefined); + const activeCategoryId = ref(undefined); - function onSelected(categoryId: number, isExpanded: boolean) { + function onSelected(categoryId: ExecutableId, isExpanded: boolean) { activeCategoryId.value = isExpanded ? categoryId : undefined; } diff --git a/src/presentation/components/Scripts/View/Cards/CardListItem.vue b/src/presentation/components/Scripts/View/Cards/CardListItem.vue index 454167f0..74c175f3 100644 --- a/src/presentation/components/Scripts/View/Cards/CardListItem.vue +++ b/src/presentation/components/Scripts/View/Cards/CardListItem.vue @@ -56,12 +56,14 @@ +@/domain/Collection/ICategoryCollection diff --git a/src/presentation/components/Scripts/View/Tree/NodeContent/Reverter/CategoryReverter.ts b/src/presentation/components/Scripts/View/Tree/NodeContent/Reverter/CategoryReverter.ts index 8175b400..e13affa4 100644 --- a/src/presentation/components/Scripts/View/Tree/NodeContent/Reverter/CategoryReverter.ts +++ b/src/presentation/components/Scripts/View/Tree/NodeContent/Reverter/CategoryReverter.ts @@ -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; - 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)); } diff --git a/src/presentation/components/Scripts/View/Tree/NodeContent/Reverter/ReverterFactory.ts b/src/presentation/components/Scripts/View/Tree/NodeContent/Reverter/ReverterFactory.ts index 1ea96de0..6c6e2bf0 100644 --- a/src/presentation/components/Scripts/View/Tree/NodeContent/Reverter/ReverterFactory.ts +++ b/src/presentation/components/Scripts/View/Tree/NodeContent/Reverter/ReverterFactory.ts @@ -1,10 +1,13 @@ -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); diff --git a/src/presentation/components/Scripts/View/Tree/NodeContent/Reverter/ScriptReverter.ts b/src/presentation/components/Scripts/View/Tree/NodeContent/Reverter/ScriptReverter.ts index ae4a5efd..d6f619df 100644 --- a/src/presentation/components/Scripts/View/Tree/NodeContent/Reverter/ScriptReverter.ts +++ b/src/presentation/components/Scripts/View/Tree/NodeContent/Reverter/ScriptReverter.ts @@ -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): boolean { diff --git a/src/presentation/components/Scripts/View/Tree/ScriptsTree.vue b/src/presentation/components/Scripts/View/Tree/ScriptsTree.vue index daf58d76..c082c829 100644 --- a/src/presentation/components/Scripts/View/Tree/ScriptsTree.vue +++ b/src/presentation/components/Scripts/View/Tree/ScriptsTree.vue @@ -24,8 +24,9 @@