Fix code highlighting and optimize category select
This commit introduces a batched debounce mechanism for managing user
selection state changes. It effectively reduces unnecessary processing
during rapid script checking, preventing multiple triggers for code
compilation and UI rendering.
Key improvements include:
- Enhanced performance, especially noticeable when selecting large
categories. This update resolves minor UI freezes experienced when
selecting categories with numerous scripts.
- Correction of a bug where the code area only highlighted the last
selected script when multiple scripts were chosen.
Other changes include:
- Timing functions:
- Create a `Timing` folder for `throttle` and the new
`batchedDebounce` functions.
- Move these functions to the application layer from the presentation
layer, reflecting their application-wide use.
- Refactor existing code for improved clarity, naming consistency, and
adherence to new naming conventions.
- Add missing unit tests.
- `UserSelection`:
- State modifications in `UserSelection` now utilize a singular object
inspired by the CQRS pattern, enabling batch updates and flexible
change configurations, thereby simplifying change management.
- Remove the `I` prefix from related interfaces to align with new coding
standards.
- Refactor related code for better testability in isolation with
dependency injection.
- Repository:
- Move repository abstractions to the application layer.
- Improve repository abstraction to combine `ReadonlyRepository` and
`MutableRepository` interfaces.
- E2E testing:
- Introduce E2E tests to validate the correct batch selection
behavior.
- Add a specialized data attribute in `TheCodeArea.vue` for improved
testability.
- Reorganize shared Cypress functions for a more idiomatic Cypress
approach.
- Improve test documentation with related information.
- `SelectedScript`:
- Create an abstraction for simplified testability.
- Introduce `SelectedScriptStub` in tests as a substitute for the
actual object.
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
import { ICategory } from '@/domain/ICategory';
|
||||
import { CategorySelectionChangeCommand } from './CategorySelectionChange';
|
||||
|
||||
export interface ReadonlyCategorySelection {
|
||||
areAllScriptsSelected(category: ICategory): boolean;
|
||||
isAnyScriptSelected(category: ICategory): boolean;
|
||||
}
|
||||
|
||||
export interface CategorySelection extends ReadonlyCategorySelection {
|
||||
processChanges(action: CategorySelectionChangeCommand): void;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
type CategorySelectionStatus = {
|
||||
readonly isSelected: true;
|
||||
readonly isReverted: boolean;
|
||||
} | {
|
||||
readonly isSelected: false;
|
||||
};
|
||||
|
||||
export interface CategorySelectionChange {
|
||||
readonly categoryId: number;
|
||||
readonly newStatus: CategorySelectionStatus;
|
||||
}
|
||||
|
||||
export interface CategorySelectionChangeCommand {
|
||||
readonly changes: readonly CategorySelectionChange[];
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { ICategory } from '@/domain/ICategory';
|
||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
import { ScriptSelection } from '../Script/ScriptSelection';
|
||||
import { ScriptSelectionChange } from '../Script/ScriptSelectionChange';
|
||||
import { CategorySelection } from './CategorySelection';
|
||||
import { CategorySelectionChange, CategorySelectionChangeCommand } from './CategorySelectionChange';
|
||||
|
||||
export class ScriptToCategorySelectionMapper implements CategorySelection {
|
||||
constructor(
|
||||
private readonly scriptSelection: ScriptSelection,
|
||||
private readonly collection: ICategoryCollection,
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
public areAllScriptsSelected(category: ICategory): boolean {
|
||||
const { selectedScripts } = this.scriptSelection;
|
||||
if (selectedScripts.length === 0) {
|
||||
return false;
|
||||
}
|
||||
const scripts = category.getAllScriptsRecursively();
|
||||
if (selectedScripts.length < scripts.length) {
|
||||
return false;
|
||||
}
|
||||
return scripts.every(
|
||||
(script) => selectedScripts.some((selected) => selected.id === script.id),
|
||||
);
|
||||
}
|
||||
|
||||
public isAnyScriptSelected(category: ICategory): boolean {
|
||||
const { selectedScripts } = this.scriptSelection;
|
||||
if (selectedScripts.length === 0) {
|
||||
return false;
|
||||
}
|
||||
return selectedScripts.some((s) => category.includes(s.script));
|
||||
}
|
||||
|
||||
public processChanges(action: CategorySelectionChangeCommand): void {
|
||||
const scriptChanges = action.changes.reduce((changes, change) => {
|
||||
changes.push(...this.collectScriptChanges(change));
|
||||
return changes;
|
||||
}, new Array<ScriptSelectionChange>());
|
||||
this.scriptSelection.processChanges({
|
||||
changes: scriptChanges,
|
||||
});
|
||||
}
|
||||
|
||||
private collectScriptChanges(change: CategorySelectionChange): ScriptSelectionChange[] {
|
||||
const category = this.collection.getCategory(change.categoryId);
|
||||
const scripts = category.getAllScriptsRecursively();
|
||||
const scriptsChangesInCategory = scripts
|
||||
.map((script): ScriptSelectionChange => ({
|
||||
scriptId: script.id,
|
||||
newStatus: {
|
||||
...change.newStatus,
|
||||
},
|
||||
}));
|
||||
return scriptsChangesInCategory;
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import { IScript } from '@/domain/IScript';
|
||||
import { ICategory } from '@/domain/ICategory';
|
||||
import { IEventSource } from '@/infrastructure/Events/IEventSource';
|
||||
import { SelectedScript } from './SelectedScript';
|
||||
|
||||
export interface IReadOnlyUserSelection {
|
||||
readonly changed: IEventSource<ReadonlyArray<SelectedScript>>;
|
||||
readonly selectedScripts: ReadonlyArray<SelectedScript>;
|
||||
isSelected(scriptId: string): boolean;
|
||||
areAllSelected(category: ICategory): boolean;
|
||||
isAnySelected(category: ICategory): boolean;
|
||||
}
|
||||
|
||||
export interface IUserSelection extends IReadOnlyUserSelection {
|
||||
removeAllInCategory(categoryId: number): void;
|
||||
addOrUpdateAllInCategory(categoryId: number, revert: boolean): void;
|
||||
addSelectedScript(scriptId: string, revert: boolean): void;
|
||||
addOrUpdateSelectedScript(scriptId: string, revert: boolean): void;
|
||||
removeSelectedScript(scriptId: string): void;
|
||||
selectOnly(scripts: ReadonlyArray<IScript>): void;
|
||||
selectAll(): void;
|
||||
deselectAll(): void;
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryRepository';
|
||||
import { IScript } from '@/domain/IScript';
|
||||
import { EventSource } from '@/infrastructure/Events/EventSource';
|
||||
import { ReadonlyRepository, Repository } from '@/application/Repository/Repository';
|
||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
import { batchedDebounce } from '@/application/Common/Timing/BatchedDebounce';
|
||||
import { ScriptSelection } from './ScriptSelection';
|
||||
import { ScriptSelectionChange, ScriptSelectionChangeCommand } from './ScriptSelectionChange';
|
||||
import { SelectedScript } from './SelectedScript';
|
||||
import { UserSelectedScript } from './UserSelectedScript';
|
||||
|
||||
const DEBOUNCE_DELAY_IN_MS = 100;
|
||||
|
||||
export type DebounceFunction = typeof batchedDebounce<ScriptSelectionChangeCommand>;
|
||||
|
||||
export class DebouncedScriptSelection implements ScriptSelection {
|
||||
public readonly changed = new EventSource<ReadonlyArray<SelectedScript>>();
|
||||
|
||||
private readonly scripts: Repository<string, SelectedScript>;
|
||||
|
||||
public readonly processChanges: ScriptSelection['processChanges'];
|
||||
|
||||
constructor(
|
||||
private readonly collection: ICategoryCollection,
|
||||
selectedScripts: ReadonlyArray<SelectedScript>,
|
||||
debounce: DebounceFunction = batchedDebounce,
|
||||
) {
|
||||
this.scripts = new InMemoryRepository<string, SelectedScript>();
|
||||
for (const script of selectedScripts) {
|
||||
this.scripts.addItem(script);
|
||||
}
|
||||
this.processChanges = debounce(
|
||||
(batchedRequests: readonly ScriptSelectionChangeCommand[]) => {
|
||||
const consolidatedChanges = batchedRequests.flatMap((request) => request.changes);
|
||||
this.processScriptChanges(consolidatedChanges);
|
||||
},
|
||||
DEBOUNCE_DELAY_IN_MS,
|
||||
);
|
||||
}
|
||||
|
||||
public isSelected(scriptId: string): boolean {
|
||||
return this.scripts.exists(scriptId);
|
||||
}
|
||||
|
||||
public get selectedScripts(): readonly SelectedScript[] {
|
||||
return this.scripts.getItems();
|
||||
}
|
||||
|
||||
public selectAll(): void {
|
||||
const scriptsToSelect = this.collection
|
||||
.getAllScripts()
|
||||
.filter((script) => !this.scripts.exists(script.id))
|
||||
.map((script) => new UserSelectedScript(script, false));
|
||||
if (scriptsToSelect.length === 0) {
|
||||
return;
|
||||
}
|
||||
this.processChanges({
|
||||
changes: scriptsToSelect.map((script): ScriptSelectionChange => ({
|
||||
scriptId: script.id,
|
||||
newStatus: {
|
||||
isSelected: true,
|
||||
isReverted: false,
|
||||
},
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
public deselectAll(): void {
|
||||
if (this.scripts.length === 0) {
|
||||
return;
|
||||
}
|
||||
const selectedScriptIds = this.scripts.getItems().map((script) => script.id);
|
||||
this.processChanges({
|
||||
changes: selectedScriptIds.map((scriptId): ScriptSelectionChange => ({
|
||||
scriptId,
|
||||
newStatus: {
|
||||
isSelected: false,
|
||||
},
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
public selectOnly(scripts: readonly IScript[]): void {
|
||||
if (scripts.length === 0) {
|
||||
throw new Error('Provided script array is empty. To deselect all scripts, please use the deselectAll() method instead.');
|
||||
}
|
||||
this.processChanges({
|
||||
changes: [
|
||||
...getScriptIdsToBeDeselected(this.scripts, scripts)
|
||||
.map((scriptId): ScriptSelectionChange => ({
|
||||
scriptId,
|
||||
newStatus: {
|
||||
isSelected: false,
|
||||
},
|
||||
})),
|
||||
...getScriptIdsToBeSelected(this.scripts, scripts)
|
||||
.map((scriptId): ScriptSelectionChange => ({
|
||||
scriptId,
|
||||
newStatus: {
|
||||
isSelected: true,
|
||||
isReverted: false,
|
||||
},
|
||||
})),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
private processScriptChanges(changes: readonly ScriptSelectionChange[]): void {
|
||||
let totalChanged = 0;
|
||||
for (const change of changes) {
|
||||
totalChanged += this.applyChange(change);
|
||||
}
|
||||
if (totalChanged > 0) {
|
||||
this.changed.notify(this.scripts.getItems());
|
||||
}
|
||||
}
|
||||
|
||||
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.removeScript(script.id);
|
||||
}
|
||||
|
||||
private addOrUpdateScript(scriptId: string, revert: boolean): number {
|
||||
const script = this.collection.getScript(scriptId);
|
||||
const selectedScript = new UserSelectedScript(script, revert);
|
||||
if (!this.scripts.exists(selectedScript.id)) {
|
||||
this.scripts.addItem(selectedScript);
|
||||
return 1;
|
||||
}
|
||||
const existingSelectedScript = this.scripts.getById(selectedScript.id);
|
||||
if (equals(selectedScript, existingSelectedScript)) {
|
||||
return 0;
|
||||
}
|
||||
this.scripts.addOrUpdateItem(selectedScript);
|
||||
return 1;
|
||||
}
|
||||
|
||||
private removeScript(scriptId: string): number {
|
||||
if (!this.scripts.exists(scriptId)) {
|
||||
return 0;
|
||||
}
|
||||
this.scripts.removeItem(scriptId);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
function getScriptIdsToBeSelected(
|
||||
existingItems: ReadonlyRepository<string, SelectedScript>,
|
||||
desiredScripts: readonly IScript[],
|
||||
): string[] {
|
||||
return desiredScripts
|
||||
.filter((script) => !existingItems.exists(script.id))
|
||||
.map((script) => script.id);
|
||||
}
|
||||
|
||||
function getScriptIdsToBeDeselected(
|
||||
existingItems: ReadonlyRepository<string, SelectedScript>,
|
||||
desiredScripts: readonly IScript[],
|
||||
): string[] {
|
||||
return existingItems
|
||||
.getItems()
|
||||
.filter((existing) => !desiredScripts.some((script) => existing.id === script.id))
|
||||
.map((script) => script.id);
|
||||
}
|
||||
|
||||
function equals(a: SelectedScript, b: SelectedScript): boolean {
|
||||
return a.script.equals(b.script.id) && a.revert === b.revert;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { IEventSource } from '@/infrastructure/Events/IEventSource';
|
||||
import { IScript } from '@/domain/IScript';
|
||||
import { SelectedScript } from './SelectedScript';
|
||||
import { ScriptSelectionChangeCommand } from './ScriptSelectionChange';
|
||||
|
||||
export interface ReadonlyScriptSelection {
|
||||
readonly changed: IEventSource<readonly SelectedScript[]>;
|
||||
readonly selectedScripts: readonly SelectedScript[];
|
||||
isSelected(scriptId: string): boolean;
|
||||
}
|
||||
|
||||
export interface ScriptSelection extends ReadonlyScriptSelection {
|
||||
selectOnly(scripts: readonly IScript[]): void;
|
||||
selectAll(): void;
|
||||
deselectAll(): void;
|
||||
processChanges(action: ScriptSelectionChangeCommand): void;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
export type ScriptSelectionStatus = {
|
||||
readonly isSelected: true;
|
||||
readonly isReverted: boolean;
|
||||
} | {
|
||||
readonly isSelected: false;
|
||||
};
|
||||
|
||||
export interface ScriptSelectionChange {
|
||||
readonly scriptId: string;
|
||||
readonly newStatus: ScriptSelectionStatus;
|
||||
}
|
||||
|
||||
export interface ScriptSelectionChangeCommand {
|
||||
readonly changes: ReadonlyArray<ScriptSelectionChange>;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { IEntity } from '@/infrastructure/Entity/IEntity';
|
||||
import { IScript } from '@/domain/IScript';
|
||||
|
||||
type ScriptId = IScript['id'];
|
||||
|
||||
export interface SelectedScript extends IEntity<ScriptId> {
|
||||
readonly script: IScript;
|
||||
readonly revert: boolean;
|
||||
}
|
||||
@@ -1,14 +1,17 @@
|
||||
import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
|
||||
import { IScript } from '@/domain/IScript';
|
||||
import { SelectedScript } from './SelectedScript';
|
||||
|
||||
export class SelectedScript extends BaseEntity<string> {
|
||||
type SelectedScriptId = SelectedScript['id'];
|
||||
|
||||
export class UserSelectedScript extends BaseEntity<SelectedScriptId> {
|
||||
constructor(
|
||||
public readonly script: IScript,
|
||||
public readonly revert: boolean,
|
||||
) {
|
||||
super(script.id);
|
||||
if (revert && !script.canRevert()) {
|
||||
throw new Error('cannot revert an irreversible script');
|
||||
throw new Error(`The script with ID '${script.id}' is not reversible and cannot be reverted.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,164 +1,12 @@
|
||||
import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryRepository';
|
||||
import { IScript } from '@/domain/IScript';
|
||||
import { EventSource } from '@/infrastructure/Events/EventSource';
|
||||
import { IRepository } from '@/infrastructure/Repository/IRepository';
|
||||
import { ICategory } from '@/domain/ICategory';
|
||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
import { IUserSelection } from './IUserSelection';
|
||||
import { SelectedScript } from './SelectedScript';
|
||||
import { CategorySelection, ReadonlyCategorySelection } from './Category/CategorySelection';
|
||||
import { ReadonlyScriptSelection, ScriptSelection } from './Script/ScriptSelection';
|
||||
|
||||
export class UserSelection implements IUserSelection {
|
||||
public readonly changed = new EventSource<ReadonlyArray<SelectedScript>>();
|
||||
|
||||
private readonly scripts: IRepository<string, SelectedScript>;
|
||||
|
||||
constructor(
|
||||
private readonly collection: ICategoryCollection,
|
||||
selectedScripts: ReadonlyArray<SelectedScript>,
|
||||
) {
|
||||
this.scripts = new InMemoryRepository<string, SelectedScript>();
|
||||
for (const script of selectedScripts) {
|
||||
this.scripts.addItem(script);
|
||||
}
|
||||
}
|
||||
|
||||
public areAllSelected(category: ICategory): boolean {
|
||||
if (this.selectedScripts.length === 0) {
|
||||
return false;
|
||||
}
|
||||
const scripts = category.getAllScriptsRecursively();
|
||||
if (this.selectedScripts.length < scripts.length) {
|
||||
return false;
|
||||
}
|
||||
return scripts.every(
|
||||
(script) => this.selectedScripts.some((selected) => selected.id === script.id),
|
||||
);
|
||||
}
|
||||
|
||||
public isAnySelected(category: ICategory): boolean {
|
||||
if (this.selectedScripts.length === 0) {
|
||||
return false;
|
||||
}
|
||||
return this.selectedScripts.some((s) => category.includes(s.script));
|
||||
}
|
||||
|
||||
public removeAllInCategory(categoryId: number): void {
|
||||
const category = this.collection.getCategory(categoryId);
|
||||
const scriptsToRemove = category.getAllScriptsRecursively()
|
||||
.filter((script) => this.scripts.exists(script.id));
|
||||
if (!scriptsToRemove.length) {
|
||||
return;
|
||||
}
|
||||
for (const script of scriptsToRemove) {
|
||||
this.scripts.removeItem(script.id);
|
||||
}
|
||||
this.changed.notify(this.scripts.getItems());
|
||||
}
|
||||
|
||||
public addOrUpdateAllInCategory(categoryId: number, revert = false): void {
|
||||
const scriptsToAddOrUpdate = this.collection
|
||||
.getCategory(categoryId)
|
||||
.getAllScriptsRecursively()
|
||||
.filter(
|
||||
(script) => !this.scripts.exists(script.id)
|
||||
|| this.scripts.getById(script.id).revert !== revert,
|
||||
)
|
||||
.map((script) => new SelectedScript(script, revert));
|
||||
if (!scriptsToAddOrUpdate.length) {
|
||||
return;
|
||||
}
|
||||
for (const script of scriptsToAddOrUpdate) {
|
||||
this.scripts.addOrUpdateItem(script);
|
||||
}
|
||||
this.changed.notify(this.scripts.getItems());
|
||||
}
|
||||
|
||||
public addSelectedScript(scriptId: string, revert: boolean): void {
|
||||
const script = this.collection.getScript(scriptId);
|
||||
const selectedScript = new SelectedScript(script, revert);
|
||||
this.scripts.addItem(selectedScript);
|
||||
this.changed.notify(this.scripts.getItems());
|
||||
}
|
||||
|
||||
public addOrUpdateSelectedScript(scriptId: string, revert: boolean): void {
|
||||
const script = this.collection.getScript(scriptId);
|
||||
const selectedScript = new SelectedScript(script, revert);
|
||||
this.scripts.addOrUpdateItem(selectedScript);
|
||||
this.changed.notify(this.scripts.getItems());
|
||||
}
|
||||
|
||||
public removeSelectedScript(scriptId: string): void {
|
||||
this.scripts.removeItem(scriptId);
|
||||
this.changed.notify(this.scripts.getItems());
|
||||
}
|
||||
|
||||
public isSelected(scriptId: string): boolean {
|
||||
return this.scripts.exists(scriptId);
|
||||
}
|
||||
|
||||
/** Get users scripts based on his/her selections */
|
||||
public get selectedScripts(): ReadonlyArray<SelectedScript> {
|
||||
return this.scripts.getItems();
|
||||
}
|
||||
|
||||
public selectAll(): void {
|
||||
const scriptsToSelect = this.collection
|
||||
.getAllScripts()
|
||||
.filter((script) => !this.scripts.exists(script.id))
|
||||
.map((script) => new SelectedScript(script, false));
|
||||
if (scriptsToSelect.length === 0) {
|
||||
return;
|
||||
}
|
||||
for (const script of scriptsToSelect) {
|
||||
this.scripts.addItem(script);
|
||||
}
|
||||
this.changed.notify(this.scripts.getItems());
|
||||
}
|
||||
|
||||
public deselectAll(): void {
|
||||
if (this.scripts.length === 0) {
|
||||
return;
|
||||
}
|
||||
const selectedScriptIds = this.scripts.getItems().map((script) => script.id);
|
||||
for (const scriptId of selectedScriptIds) {
|
||||
this.scripts.removeItem(scriptId);
|
||||
}
|
||||
this.changed.notify([]);
|
||||
}
|
||||
|
||||
public selectOnly(scripts: readonly IScript[]): void {
|
||||
if (!scripts.length) {
|
||||
throw new Error('Scripts are empty. Use deselectAll() if you want to deselect everything');
|
||||
}
|
||||
let totalChanged = 0;
|
||||
totalChanged += this.unselectMissingWithoutNotifying(scripts);
|
||||
totalChanged += this.selectNewWithoutNotifying(scripts);
|
||||
if (totalChanged > 0) {
|
||||
this.changed.notify(this.scripts.getItems());
|
||||
}
|
||||
}
|
||||
|
||||
private unselectMissingWithoutNotifying(scripts: readonly IScript[]): number {
|
||||
if (this.scripts.length === 0 || scripts.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
const existingItems = this.scripts.getItems();
|
||||
const missingIds = existingItems
|
||||
.filter((existing) => !scripts.some((script) => existing.id === script.id))
|
||||
.map((script) => script.id);
|
||||
for (const id of missingIds) {
|
||||
this.scripts.removeItem(id);
|
||||
}
|
||||
return missingIds.length;
|
||||
}
|
||||
|
||||
private selectNewWithoutNotifying(scripts: readonly IScript[]): number {
|
||||
const unselectedScripts = scripts
|
||||
.filter((script) => !this.scripts.exists(script.id))
|
||||
.map((script) => new SelectedScript(script, false));
|
||||
for (const newScript of unselectedScripts) {
|
||||
this.scripts.addItem(newScript);
|
||||
}
|
||||
return unselectedScripts.length;
|
||||
}
|
||||
export interface ReadonlyUserSelection {
|
||||
readonly categories: ReadonlyCategorySelection;
|
||||
readonly scripts: ReadonlyScriptSelection;
|
||||
}
|
||||
|
||||
export interface UserSelection extends ReadonlyUserSelection {
|
||||
readonly categories: CategorySelection;
|
||||
readonly scripts: ScriptSelection;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
import { CategorySelection } from './Category/CategorySelection';
|
||||
import { ScriptToCategorySelectionMapper } from './Category/ScriptToCategorySelectionMapper';
|
||||
import { DebouncedScriptSelection } from './Script/DebouncedScriptSelection';
|
||||
import { ScriptSelection } from './Script/ScriptSelection';
|
||||
import { UserSelection } from './UserSelection';
|
||||
import { SelectedScript } from './Script/SelectedScript';
|
||||
|
||||
export class UserSelectionFacade implements UserSelection {
|
||||
public readonly categories: CategorySelection;
|
||||
|
||||
public readonly scripts: ScriptSelection;
|
||||
|
||||
constructor(
|
||||
collection: ICategoryCollection,
|
||||
selectedScripts: readonly SelectedScript[],
|
||||
scriptsFactory = DefaultScriptsFactory,
|
||||
categoriesFactory = DefaultCategoriesFactory,
|
||||
) {
|
||||
this.scripts = scriptsFactory(collection, selectedScripts);
|
||||
this.categories = categoriesFactory(this.scripts, collection);
|
||||
}
|
||||
}
|
||||
|
||||
export type ScriptsFactory = (
|
||||
...params: ConstructorParameters<typeof DebouncedScriptSelection>
|
||||
) => ScriptSelection;
|
||||
|
||||
const DefaultScriptsFactory: ScriptsFactory = (
|
||||
...params
|
||||
) => new DebouncedScriptSelection(...params);
|
||||
|
||||
export type CategoriesFactory = (
|
||||
...params: ConstructorParameters<typeof ScriptToCategorySelectionMapper>
|
||||
) => CategorySelection;
|
||||
|
||||
const DefaultCategoriesFactory: CategoriesFactory = (
|
||||
...params
|
||||
) => new ScriptToCategorySelectionMapper(...params);
|
||||
Reference in New Issue
Block a user