From 55fa7eae71031357d6f03f0d349a09cd446270d3 Mon Sep 17 00:00:00 2001 From: undergroundwires Date: Sun, 11 Feb 2024 22:47:34 +0100 Subject: [PATCH] Add 'Revert All Selection' feature #68 This commit introduces 'Revert: None - Selected' toggle, enabling users to revert all reversible scripts with a single action, improving user safety and control over script effects. This feature addresses user-reported concerns about the ease of reverting script changes. This feature should enhance the user experience by streamlining the revert process along with providing essential information about script reversibility. Key changes: - Add buttons to revert all selected scripts or setting all selected scripts to non-revert state. - Add tooltips with detailed explanations about consequences of modifying revert states, includinginformation about irreversible script changes. Supporting changes: - Align items on top menu vertically for better visual consistency. - Rename `SelectionType` to `RecommendationStatusType` for more clarity. - Rename `IReverter` to `Reverter` to move away from `I` prefix convention. - The `.script` CSS class was duplicated in `TheScriptsView.vue` and `TheScriptsArea.vue`, leading to style collisions in the development environment. The class has been renamed to component-specific classes to avoid such issues in the future. --- .../Script/DebouncedScriptSelection.ts | 10 +- .../Selection/Script/ScriptSelectionChange.ts | 1 + src/presentation/assets/icons/rotate-left.svg | 1 + src/presentation/assets/icons/shield.svg | 1 + .../Scripts/Menu/MenuOptionList.vue | 1 - .../Rating/CircleRating.vue | 0 .../Rating/RatingCircle.vue | 0 .../RecommendationDocumentation.vue} | 0 .../RecommendationStatusHandler.ts} | 38 ++-- .../RecommendationStatusType.ts | 7 + .../TheRecommendationSelector.vue} | 51 ++--- .../Menu/Revert/RevertStatusDocumentation.vue | 87 ++++++++ .../Menu/Revert/RevertStatusHandler.ts | 79 +++++++ .../Scripts/Menu/Revert/RevertStatusType.ts | 6 + .../Scripts/Menu/Revert/TheRevertSelector.vue | 145 +++++++++++++ .../Scripts/Menu/TheScriptsMenu.vue | 16 +- .../components/Scripts/TheScriptArea.vue | 10 +- .../Scripts/View/TheScriptsView.vue | 4 +- .../View/Tree/NodeContent/RevertToggle.vue | 5 +- .../NodeContent/Reverter/CategoryReverter.ts | 19 +- .../Reverter/{IReverter.ts => Reverter.ts} | 2 +- .../NodeContent/Reverter/ReverterFactory.ts | 4 +- .../NodeContent/Reverter/ScriptReverter.ts | 4 +- .../components/Shared/Icon/IconName.ts | 2 + .../Scripts/Menu/MenuOptionList.spec.ts | 54 +++++ .../Rating/CircleRating.spec.ts | 4 +- .../Rating/RatingCircle.spec.ts | 2 +- .../RecommendationDocumentation.spec.ts} | 8 +- .../RecommendationStatusHandler.spec.ts} | 61 +++--- .../RecommendationStatusTestScenario.ts} | 2 +- .../Menu/Revert/RevertStatusHandler.spec.ts | 201 ++++++++++++++++++ .../Reverter/CategoryReverter.spec.ts | 165 ++++++++++---- .../unit/shared/Stubs/ScriptSelectionStub.ts | 73 +++++-- tests/unit/shared/Stubs/ScriptStub.ts | 12 +- 34 files changed, 906 insertions(+), 169 deletions(-) create mode 100644 src/presentation/assets/icons/rotate-left.svg create mode 100644 src/presentation/assets/icons/shield.svg rename src/presentation/components/Scripts/Menu/{Selector => Recommendation}/Rating/CircleRating.vue (100%) rename src/presentation/components/Scripts/Menu/{Selector => Recommendation}/Rating/RatingCircle.vue (100%) rename src/presentation/components/Scripts/Menu/{Selector/SelectionTypeDocumentation.vue => Recommendation/RecommendationDocumentation.vue} (100%) rename src/presentation/components/Scripts/Menu/{Selector/SelectionTypeHandler.ts => Recommendation/RecommendationStatusHandler.ts} (74%) create mode 100644 src/presentation/components/Scripts/Menu/Recommendation/RecommendationStatusType.ts rename src/presentation/components/Scripts/Menu/{Selector/TheSelector.vue => Recommendation/TheRecommendationSelector.vue} (71%) create mode 100644 src/presentation/components/Scripts/Menu/Revert/RevertStatusDocumentation.vue create mode 100644 src/presentation/components/Scripts/Menu/Revert/RevertStatusHandler.ts create mode 100644 src/presentation/components/Scripts/Menu/Revert/RevertStatusType.ts create mode 100644 src/presentation/components/Scripts/Menu/Revert/TheRevertSelector.vue rename src/presentation/components/Scripts/View/Tree/NodeContent/Reverter/{IReverter.ts => Reverter.ts} (91%) create mode 100644 tests/unit/presentation/components/Scripts/Menu/MenuOptionList.spec.ts rename tests/unit/presentation/components/Scripts/Menu/{Selector => Recommendation}/Rating/CircleRating.spec.ts (96%) rename tests/unit/presentation/components/Scripts/Menu/{Selector => Recommendation}/Rating/RatingCircle.spec.ts (98%) rename tests/unit/presentation/components/Scripts/Menu/{Selector/SelectionTypeDocumentation.spec.ts => Recommendation/RecommendationDocumentation.spec.ts} (93%) rename tests/unit/presentation/components/Scripts/Menu/{Selector/SelectionTypeHandler.spec.ts => Recommendation/RecommendationStatusHandler.spec.ts} (69%) rename tests/unit/presentation/components/Scripts/Menu/{Selector/SelectionStateTestScenario.ts => Recommendation/RecommendationStatusTestScenario.ts} (98%) create mode 100644 tests/unit/presentation/components/Scripts/Menu/Revert/RevertStatusHandler.spec.ts diff --git a/src/application/Context/State/Selection/Script/DebouncedScriptSelection.ts b/src/application/Context/State/Selection/Script/DebouncedScriptSelection.ts index ffe73762..319c01c0 100644 --- a/src/application/Context/State/Selection/Script/DebouncedScriptSelection.ts +++ b/src/application/Context/State/Selection/Script/DebouncedScriptSelection.ts @@ -81,9 +81,7 @@ export class DebouncedScriptSelection implements ScriptSelection { } 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.'); - } + assertNonEmptyScriptSelection(scripts); this.processChanges({ changes: [ ...getScriptIdsToBeDeselected(this.scripts, scripts) @@ -147,6 +145,12 @@ export class DebouncedScriptSelection implements ScriptSelection { } } +function assertNonEmptyScriptSelection(selectedItems: readonly IScript[]) { + if (selectedItems.length === 0) { + throw new Error('Provided script array is empty. To deselect all scripts, please use the deselectAll() method instead.'); + } +} + function getScriptIdsToBeSelected( existingItems: ReadonlyRepository, desiredScripts: readonly IScript[], diff --git a/src/application/Context/State/Selection/Script/ScriptSelectionChange.ts b/src/application/Context/State/Selection/Script/ScriptSelectionChange.ts index bc596d02..16f23a31 100644 --- a/src/application/Context/State/Selection/Script/ScriptSelectionChange.ts +++ b/src/application/Context/State/Selection/Script/ScriptSelectionChange.ts @@ -3,6 +3,7 @@ export type ScriptSelectionStatus = { readonly isReverted: boolean; } | { readonly isSelected: false; + readonly isReverted?: undefined; }; export interface ScriptSelectionChange { diff --git a/src/presentation/assets/icons/rotate-left.svg b/src/presentation/assets/icons/rotate-left.svg new file mode 100644 index 00000000..2b12f49d --- /dev/null +++ b/src/presentation/assets/icons/rotate-left.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/presentation/assets/icons/shield.svg b/src/presentation/assets/icons/shield.svg new file mode 100644 index 00000000..ee79950e --- /dev/null +++ b/src/presentation/assets/icons/shield.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/presentation/components/Scripts/Menu/MenuOptionList.vue b/src/presentation/components/Scripts/Menu/MenuOptionList.vue index 6981fbda..0a6eebb3 100644 --- a/src/presentation/components/Scripts/Menu/MenuOptionList.vue +++ b/src/presentation/components/Scripts/Menu/MenuOptionList.vue @@ -29,7 +29,6 @@ $gap: 0.25rem; .list { font-family: $font-normal; display: flex; - align-items: center; :deep(.items) { > * + *::before { content: '|'; diff --git a/src/presentation/components/Scripts/Menu/Selector/Rating/CircleRating.vue b/src/presentation/components/Scripts/Menu/Recommendation/Rating/CircleRating.vue similarity index 100% rename from src/presentation/components/Scripts/Menu/Selector/Rating/CircleRating.vue rename to src/presentation/components/Scripts/Menu/Recommendation/Rating/CircleRating.vue diff --git a/src/presentation/components/Scripts/Menu/Selector/Rating/RatingCircle.vue b/src/presentation/components/Scripts/Menu/Recommendation/Rating/RatingCircle.vue similarity index 100% rename from src/presentation/components/Scripts/Menu/Selector/Rating/RatingCircle.vue rename to src/presentation/components/Scripts/Menu/Recommendation/Rating/RatingCircle.vue diff --git a/src/presentation/components/Scripts/Menu/Selector/SelectionTypeDocumentation.vue b/src/presentation/components/Scripts/Menu/Recommendation/RecommendationDocumentation.vue similarity index 100% rename from src/presentation/components/Scripts/Menu/Selector/SelectionTypeDocumentation.vue rename to src/presentation/components/Scripts/Menu/Recommendation/RecommendationDocumentation.vue diff --git a/src/presentation/components/Scripts/Menu/Selector/SelectionTypeHandler.ts b/src/presentation/components/Scripts/Menu/Recommendation/RecommendationStatusHandler.ts similarity index 74% rename from src/presentation/components/Scripts/Menu/Selector/SelectionTypeHandler.ts rename to src/presentation/components/Scripts/Menu/Recommendation/RecommendationStatusHandler.ts index dc97f6a5..827b9806 100644 --- a/src/presentation/components/Scripts/Menu/Selector/SelectionTypeHandler.ts +++ b/src/presentation/components/Scripts/Menu/Recommendation/RecommendationStatusHandler.ts @@ -4,33 +4,31 @@ import { scrambledEqual } from '@/application/Common/Array'; import { ICategoryCollection } from '@/domain/ICategoryCollection'; import { ReadonlyScriptSelection, ScriptSelection } from '@/application/Context/State/Selection/Script/ScriptSelection'; import { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript'; +import { RecommendationStatusType } from './RecommendationStatusType'; -export enum SelectionType { - Standard, - Strict, - All, - None, - Custom, -} - -export function setCurrentSelectionType(type: SelectionType, context: SelectionMutationContext) { - if (type === SelectionType.Custom) { +export function setCurrentRecommendationStatus( + type: RecommendationStatusType, + context: SelectionMutationContext, +) { + if (type === RecommendationStatusType.Custom) { throw new Error('Cannot select custom type.'); } const selector = selectors.get(type); if (!selector) { - throw new Error(`Cannot handle the type: ${SelectionType[type]}`); + throw new Error(`Cannot handle the type: ${RecommendationStatusType[type]}`); } selector.select(context); } -export function getCurrentSelectionType(context: SelectionCheckContext): SelectionType { +export function getCurrentRecommendationStatus( + context: SelectionCheckContext, +): RecommendationStatusType { for (const [type, selector] of selectors.entries()) { if (selector.isSelected(context)) { return type; } } - return SelectionType.Custom; + return RecommendationStatusType.Custom; } export interface SelectionCheckContext { @@ -43,19 +41,19 @@ export interface SelectionMutationContext { readonly collection: ICategoryCollection, } -interface SelectionTypeHandler { +interface RecommendationStatusTypeHandler { isSelected: (context: SelectionCheckContext) => boolean; select: (context: SelectionMutationContext) => void; } -const selectors = new Map([ - [SelectionType.None, { +const selectors = new Map([ + [RecommendationStatusType.None, { select: ({ selection }) => selection.deselectAll(), isSelected: ({ selection }) => selection.selectedScripts.length === 0, }], - [SelectionType.Standard, getRecommendationLevelSelector(RecommendationLevel.Standard)], - [SelectionType.Strict, getRecommendationLevelSelector(RecommendationLevel.Strict)], - [SelectionType.All, { + [RecommendationStatusType.Standard, getRecommendationLevelSelector(RecommendationLevel.Standard)], + [RecommendationStatusType.Strict, getRecommendationLevelSelector(RecommendationLevel.Strict)], + [RecommendationStatusType.All, { select: ({ selection }) => selection.selectAll(), isSelected: ( { selection, collection }, @@ -65,7 +63,7 @@ const selectors = new Map([ function getRecommendationLevelSelector( level: RecommendationLevel, -): SelectionTypeHandler { +): RecommendationStatusTypeHandler { return { select: (context) => selectOnly(level, context), isSelected: (context) => hasAllSelectedLevelOf(level, context), diff --git a/src/presentation/components/Scripts/Menu/Recommendation/RecommendationStatusType.ts b/src/presentation/components/Scripts/Menu/Recommendation/RecommendationStatusType.ts new file mode 100644 index 00000000..eafdb0a1 --- /dev/null +++ b/src/presentation/components/Scripts/Menu/Recommendation/RecommendationStatusType.ts @@ -0,0 +1,7 @@ +export enum RecommendationStatusType { + Standard, + Strict, + All, + None, + Custom, +} diff --git a/src/presentation/components/Scripts/Menu/Selector/TheSelector.vue b/src/presentation/components/Scripts/Menu/Recommendation/TheRecommendationSelector.vue similarity index 71% rename from src/presentation/components/Scripts/Menu/Selector/TheSelector.vue rename to src/presentation/components/Scripts/Menu/Recommendation/TheRecommendationSelector.vue index a582d424..8e9446b1 100644 --- a/src/presentation/components/Scripts/Menu/Selector/TheSelector.vue +++ b/src/presentation/components/Scripts/Menu/Recommendation/TheRecommendationSelector.vue @@ -4,11 +4,11 @@