From 1072505219edc47d82a91f148d1f310f32869fea Mon Sep 17 00:00:00 2001 From: undergroundwires Date: Thu, 17 Sep 2020 21:46:20 +0100 Subject: [PATCH] show icons on cards during indeterminate and fully selected states --- .../State/Selection/IUserSelection.ts | 3 + .../State/Selection/UserSelection.ts | 20 ++++- src/domain/Category.ts | 4 + src/domain/ICategory.ts | 1 + .../Bootstrapping/Modules/IconBootstrapper.ts | 4 +- .../Scripts/Cards/CardListItem.vue | 55 ++++++++---- .../Scripts/Selector/TheSelector.vue | 2 +- .../State/Selection/UserSelection.spec.ts | 88 +++++++++++++++++++ tests/unit/domain/Category.spec.ts | 37 ++++++++ tests/unit/stubs/CategoryStub.ts | 4 + 10 files changed, 200 insertions(+), 18 deletions(-) diff --git a/src/application/State/Selection/IUserSelection.ts b/src/application/State/Selection/IUserSelection.ts index a97aada9..a817f6e9 100644 --- a/src/application/State/Selection/IUserSelection.ts +++ b/src/application/State/Selection/IUserSelection.ts @@ -1,11 +1,14 @@ import { SelectedScript } from './SelectedScript'; import { ISignal } from '@/infrastructure/Events/Signal'; import { IScript } from '@/domain/IScript'; +import { ICategory } from '@/domain/ICategory'; export interface IUserSelection { readonly changed: ISignal>; readonly selectedScripts: ReadonlyArray; readonly totalSelected: number; + areAllSelected(category: ICategory): boolean; + isAnySelected(category: ICategory): boolean; removeAllInCategory(categoryId: number): void; addOrUpdateAllInCategory(categoryId: number, revert: boolean): void; addSelectedScript(scriptId: string, revert: boolean): void; diff --git a/src/application/State/Selection/UserSelection.ts b/src/application/State/Selection/UserSelection.ts index bb750524..b987d0c8 100644 --- a/src/application/State/Selection/UserSelection.ts +++ b/src/application/State/Selection/UserSelection.ts @@ -1,5 +1,5 @@ import { SelectedScript } from './SelectedScript'; -import { IApplication } from '@/domain/IApplication'; +import { IApplication, ICategory } from '@/domain/IApplication'; import { IUserSelection } from './IUserSelection'; import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryRepository'; import { IScript } from '@/domain/IScript'; @@ -21,6 +21,24 @@ export class UserSelection implements IUserSelection { } } + 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.app.findCategory(categoryId); const scriptsToRemove = category.getAllScriptsRecursively() diff --git a/src/domain/Category.ts b/src/domain/Category.ts index 4fa96725..2afc55fa 100644 --- a/src/domain/Category.ts +++ b/src/domain/Category.ts @@ -15,6 +15,10 @@ export class Category extends BaseEntity implements ICategory { validateCategory(this); } + public includes(script: IScript): boolean { + return this.getAllScriptsRecursively().some((childScript) => childScript.id === script.id); + } + public getAllScriptsRecursively(): readonly IScript[] { return this.allSubScripts || (this.allSubScripts = parseScriptsRecursively(this)); } diff --git a/src/domain/ICategory.ts b/src/domain/ICategory.ts index e7833fcd..773eb91b 100644 --- a/src/domain/ICategory.ts +++ b/src/domain/ICategory.ts @@ -7,6 +7,7 @@ export interface ICategory extends IEntity, IDocumentable { readonly name: string; readonly subCategories?: ReadonlyArray; readonly scripts?: ReadonlyArray; + includes(script: IScript): boolean; getAllScriptsRecursively(): ReadonlyArray; } diff --git a/src/presentation/Bootstrapping/Modules/IconBootstrapper.ts b/src/presentation/Bootstrapping/Modules/IconBootstrapper.ts index dae631cd..4762bd5a 100644 --- a/src/presentation/Bootstrapping/Modules/IconBootstrapper.ts +++ b/src/presentation/Bootstrapping/Modules/IconBootstrapper.ts @@ -6,7 +6,8 @@ import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'; /** REGULAR ICONS (PREFIX: far) */ import { faFolderOpen, faFolder, faSmile } from '@fortawesome/free-regular-svg-icons'; /** SOLID ICONS (PREFIX: fas (default)) */ -import { faTimes, faFileDownload, faCopy, faSearch, faInfoCircle, faUserSecret, faDesktop, faTag, faGlobe, faSave } from '@fortawesome/free-solid-svg-icons'; +import { faTimes, faFileDownload, faCopy, faSearch, faInfoCircle, faUserSecret, faDesktop, + faTag, faGlobe, faSave, faBatteryFull, faBatteryHalf } from '@fortawesome/free-solid-svg-icons'; export class IconBootstrapper implements IVueBootstrapper { @@ -24,6 +25,7 @@ export class IconBootstrapper implements IVueBootstrapper { faFileDownload, faSave, faCopy, faSearch, + faBatteryFull, faBatteryHalf, faInfoCircle); vue.component('font-awesome-icon', FontAwesomeIcon); } diff --git a/src/presentation/Scripts/Cards/CardListItem.vue b/src/presentation/Scripts/Cards/CardListItem.vue index 5e30435b..d479bafe 100644 --- a/src/presentation/Scripts/Cards/CardListItem.vue +++ b/src/presentation/Scripts/Cards/CardListItem.vue @@ -8,9 +8,17 @@ }" ref="cardElement">
- {{cardTitle}} + + {{cardTitle}} + Oh no 😢 + + +
+ + +
@@ -27,6 +35,8 @@ import { Component, Prop, Watch, Emit } from 'vue-property-decorator'; import ScriptsTree from '@/presentation/Scripts/ScriptsTree/ScriptsTree.vue'; import { StatefulVue } from '@/presentation/StatefulVue'; +import { ICategory } from '@/domain/ICategory'; +import { IUserSelection } from '@/application/State/IApplicationState'; @Component({ components: { @@ -36,17 +46,21 @@ import { StatefulVue } from '@/presentation/StatefulVue'; export default class CardListItem extends StatefulVue { @Prop() public categoryId!: number; @Prop() public activeCategoryId!: number; - public cardTitle?: string = ''; - public isExpanded: boolean = false; + public cardTitle = ''; + public isExpanded = false; + public isAnyChildSelected = false; + public areAllChildrenSelected = false; - @Emit('selected') +@Emit('selected') public onSelected(isExpanded: boolean) { this.isExpanded = isExpanded; } + @Watch('activeCategoryId') public async onActiveCategoryChanged(value: |number) { this.isExpanded = value === this.categoryId; } + @Watch('isExpanded') public async onExpansionChangedAsync(newValue: number, oldValue: number) { if (!oldValue && newValue) { @@ -57,20 +71,23 @@ export default class CardListItem extends StatefulVue { } public async mounted() { - this.cardTitle = this.categoryId ? await this.getCardTitleAsync(this.categoryId) : undefined; + const state = await this.getCurrentStateAsync(); + state.selection.changed.on(() => { + this.updateStateAsync(this.categoryId); + }); + this.updateStateAsync(this.categoryId); } @Watch('categoryId') - public async onCategoryIdChanged(value: |number) { - this.cardTitle = value ? await this.getCardTitleAsync(value) : undefined; - } - - private async getCardTitleAsync(categoryId: number): Promise { - const state = await this.getCurrentStateAsync(); - const category = state.app.findCategory(this.categoryId); - return category ? category.name : undefined; + public async updateStateAsync(value: |number) { + const state = await this.getCurrentStateAsync(); + const category = !value ? undefined : state.app.findCategory(this.categoryId); + this.cardTitle = category ? category.name : undefined; + this.isAnyChildSelected = category ? state.selection.isAnySelected(category) : false; + this.areAllChildrenSelected = category ? state.selection.areAllSelected(category) : false; } } +