fix script revert activating recommendation level

Reverting any single of the scripts from standard recommendation pool
shows "Standard" selection as selected which is wrong. This commit fixes
it, refactors selection handling in a separate class and it also adds
missing tests. It removes UserSelection.totalSelected propertty in favor of using
UserSelection.selectedScripts.length to provide unified way of accessing
the information.
This commit is contained in:
undergroundwires
2021-04-17 14:34:29 +01:00
parent aea04e5f7c
commit a2f10857e2
13 changed files with 384 additions and 109 deletions

View File

@@ -0,0 +1,86 @@
import { IScript } from '@/domain/IScript';
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
import { RecommendationLevel } from '@/domain/RecommendationLevel';
import { scrambledEqual } from '@/application/Common/Array';
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
export enum SelectionType {
Standard,
Strict,
All,
None,
Custom,
}
export class SelectionTypeHandler {
constructor(private readonly state: ICategoryCollectionState) {
if (!state) { throw new Error('undefined state'); }
}
public selectType(type: SelectionType) {
if (type === SelectionType.Custom) {
throw new Error('cannot select custom type');
}
const selector = selectors.get(type);
selector.select(this.state);
}
public getCurrentSelectionType(): SelectionType {
for (const [type, selector] of Array.from(selectors.entries())) {
if (selector.isSelected(this.state)) {
return type;
}
}
return SelectionType.Custom;
}
}
interface ISingleTypeHandler {
isSelected: (state: ICategoryCollectionState) => boolean;
select: (state: ICategoryCollectionState) => void;
}
const selectors = new Map<SelectionType, ISingleTypeHandler>([
[SelectionType.None, {
select: (state) =>
state.selection.deselectAll(),
isSelected: (state) =>
state.selection.selectedScripts.length === 0,
}],
[SelectionType.Standard, getRecommendationLevelSelector(RecommendationLevel.Standard)],
[SelectionType.Strict, getRecommendationLevelSelector(RecommendationLevel.Strict)],
[SelectionType.All, {
select: (state) =>
state.selection.selectAll(),
isSelected: (state) =>
state.selection.selectedScripts.length === state.collection.totalScripts,
}],
]);
function getRecommendationLevelSelector(level: RecommendationLevel): ISingleTypeHandler {
return {
select: (state) => selectOnly(level, state),
isSelected: (state) => hasAllSelectedLevelOf(level, state),
};
}
function hasAllSelectedLevelOf(level: RecommendationLevel, state: ICategoryCollectionState) {
const scripts = state.collection.getScriptsByLevel(level);
const selectedScripts = state.selection.selectedScripts;
return areAllSelected(scripts, selectedScripts);
}
function selectOnly(level: RecommendationLevel, state: ICategoryCollectionState) {
const scripts = state.collection.getScriptsByLevel(level);
state.selection.selectOnly(scripts);
}
function areAllSelected(
expectedScripts: ReadonlyArray<IScript>,
selection: ReadonlyArray<SelectedScript>): boolean {
selection = selection.filter((selected) => !selected.revert);
if (expectedScripts.length < selection.length) {
return false;
}
const selectedScriptIds = selection.map((script) => script.id);
const expectedScriptIds = expectedScripts.map((script) => script.id);
return scrambledEqual(selectedScriptIds, expectedScriptIds);
}

View File

@@ -5,8 +5,8 @@
<div class="part">
<SelectableOption
label="None"
:enabled="this.currentSelection == SelectionState.None"
@click="selectAsync(SelectionState.None)"
:enabled="this.currentSelection == SelectionType.None"
@click="selectType(SelectionType.None)"
v-tooltip=" 'Deselect all selected scripts.<br/>' +
'💡 Good start to dive deeper into tweaks and select only what you want.'"
/>
@@ -15,8 +15,8 @@
<div class="part">
<SelectableOption
label="Standard"
:enabled="this.currentSelection == SelectionState.Standard"
@click="selectAsync(SelectionState.Standard)"
:enabled="this.currentSelection == SelectionType.Standard"
@click="selectType(SelectionType.Standard)"
v-tooltip=" '🛡️ Balanced for privacy and functionality.<br/>' +
'OS and applications will function normally.<br/>' +
'💡 Recommended for everyone'"
@@ -26,8 +26,8 @@
<div class="part">
<SelectableOption
label="Strict"
:enabled="this.currentSelection == SelectionState.Strict"
@click="selectAsync(SelectionState.Strict)"
:enabled="this.currentSelection == SelectionType.Strict"
@click="selectType(SelectionType.Strict)"
v-tooltip=" '🚫 Stronger privacy, disables risky functions that may leak your data.<br/>' +
'⚠️ Double check to remove sripts where you would trade functionality for privacy<br/>' +
'💡 Recommended for daily users that prefers more privacy over non-essential functions'"
@@ -37,8 +37,8 @@
<div class="part">
<SelectableOption
label="All"
:enabled="this.currentSelection == SelectionState.All"
@click="selectAsync(SelectionState.All)"
:enabled="this.currentSelection == SelectionType.All"
@click="selectType(SelectionType.All)"
v-tooltip=" '🔒 Strongest privacy, disabling any functionality that may leak your data.<br/>' +
'🛑 Not designed for daily users, it will break important functionalities.<br/>' +
'💡 Only recommended for extreme use-cases like crime labs where no leak is acceptable'"
@@ -52,112 +52,40 @@
import { Component } from 'vue-property-decorator';
import { StatefulVue } from '@/presentation/components/Shared/StatefulVue';
import SelectableOption from './SelectableOption.vue';
import { IScript } from '@/domain/IScript';
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
import { RecommendationLevel } from '@/domain/RecommendationLevel';
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
import { SelectionType, SelectionTypeHandler } from './SelectionTypeHandler';
enum SelectionState {
Standard,
Strict,
All,
None,
Custom,
}
@Component({
components: {
SelectableOption,
},
})
export default class TheSelector extends StatefulVue {
public SelectionState = SelectionState;
public currentSelection = SelectionState.None;
public SelectionType = SelectionType;
public currentSelection = SelectionType.None;
private selectionTypeHandler: SelectionTypeHandler;
public async selectAsync(type: SelectionState): Promise<void> {
public async selectType(type: SelectionType) {
if (this.currentSelection === type) {
return;
}
const context = await this.getCurrentContextAsync();
selectType(context.state, type);
this.selectionTypeHandler.selectType(type);
}
protected handleCollectionState(newState: ICategoryCollectionState, oldState: ICategoryCollectionState): void {
this.updateSelections(newState);
newState.selection.changed.on(() => this.updateSelections(newState));
this.selectionTypeHandler = new SelectionTypeHandler(newState);
this.updateSelections();
newState.selection.changed.on(() => this.updateSelections());
if (oldState) {
oldState.selection.changed.on(() => this.updateSelections(oldState));
oldState.selection.changed.on(() => this.updateSelections());
}
}
private updateSelections(state: ICategoryCollectionState) {
this.currentSelection = getCurrentSelectionState(state);
private updateSelections() {
this.currentSelection = this.selectionTypeHandler.getCurrentSelectionType();
}
}
interface ITypeSelector {
isSelected: (state: ICategoryCollectionState) => boolean;
select: (state: ICategoryCollectionState) => void;
}
const selectors = new Map<SelectionState, ITypeSelector>([
[SelectionState.None, {
select: (state) =>
state.selection.deselectAll(),
isSelected: (state) =>
state.selection.totalSelected === 0,
}],
[SelectionState.Standard, {
select: (state) =>
state.selection.selectOnly(
state.collection.getScriptsByLevel(RecommendationLevel.Standard)),
isSelected: (state) =>
hasAllSelectedLevelOf(RecommendationLevel.Standard, state),
}],
[SelectionState.Strict, {
select: (state) =>
state.selection.selectOnly(state.collection.getScriptsByLevel(RecommendationLevel.Strict)),
isSelected: (state) =>
hasAllSelectedLevelOf(RecommendationLevel.Strict, state),
}],
[SelectionState.All, {
select: (state) =>
state.selection.selectAll(),
isSelected: (state) =>
state.selection.totalSelected === state.collection.totalScripts,
}],
]);
function selectType(state: ICategoryCollectionState, type: SelectionState) {
const selector = selectors.get(type);
selector.select(state);
}
function getCurrentSelectionState(state: ICategoryCollectionState): SelectionState {
for (const [type, selector] of Array.from(selectors.entries())) {
if (selector.isSelected(state)) {
return type;
}
}
return SelectionState.Custom;
}
function hasAllSelectedLevelOf(level: RecommendationLevel, state: ICategoryCollectionState) {
const scripts = state.collection.getScriptsByLevel(level);
const selectedScripts = state.selection.selectedScripts;
return areAllSelected(scripts, selectedScripts);
}
function areAllSelected(
expectedScripts: ReadonlyArray<IScript>,
selection: ReadonlyArray<SelectedScript>): boolean {
selection = selection.filter((selected) => !selected.revert);
if (expectedScripts.length < selection.length) {
return false;
}
const selectedScriptIds = selection.map((script) => script.id).sort();
const expectedScriptIds = expectedScripts.map((script) => script.id).sort();
return selectedScriptIds.every((id, index) => id === expectedScriptIds[index]);
}
</script>
<style scoped lang="scss">