added support for grouping

This commit is contained in:
undergroundwires
2020-01-09 20:18:20 +01:00
parent 6825001c61
commit ec6b3c5407
16 changed files with 270 additions and 186 deletions

View File

@@ -4,11 +4,7 @@
<TheHeader class="row" <TheHeader class="row"
github-url="https://github.com/undergroundwires/privacy.sexy" /> github-url="https://github.com/undergroundwires/privacy.sexy" />
<!-- <TheSearchBar> </TheSearchBar> --> <!-- <TheSearchBar> </TheSearchBar> -->
<!-- <div style="display: flex; justify-content: space-between;"> --> <TheScripts class="row"/>
<!-- <TheGrouper></TheGrouper> -->
<TheSelector class="row" />
<!-- </div> -->
<CardList />
<TheCodeArea class="row" theme="xcode" /> <TheCodeArea class="row" theme="xcode" />
<TheCodeButtons class="row" /> <TheCodeButtons class="row" />
<TheFooter /> <TheFooter />
@@ -24,9 +20,7 @@ import TheFooter from './presentation/TheFooter.vue';
import TheCodeArea from './presentation/TheCodeArea.vue'; import TheCodeArea from './presentation/TheCodeArea.vue';
import TheCodeButtons from './presentation/TheCodeButtons.vue'; import TheCodeButtons from './presentation/TheCodeButtons.vue';
import TheSearchBar from './presentation/TheSearchBar.vue'; import TheSearchBar from './presentation/TheSearchBar.vue';
import TheSelector from './presentation/Scripts/Selector/TheSelector.vue'; import TheScripts from './presentation/Scripts/TheScripts.vue';
import TheGrouper from './presentation/Scripts/TheGrouper.vue';
import CardList from './presentation/Scripts/Cards/CardList.vue';
@Component({ @Component({
components: { components: {
@@ -34,9 +28,7 @@ import CardList from './presentation/Scripts/Cards/CardList.vue';
TheCodeArea, TheCodeArea,
TheCodeButtons, TheCodeButtons,
TheSearchBar, TheSearchBar,
TheGrouper, TheScripts,
CardList,
TheSelector,
TheFooter, TheFooter,
}, },
}) })

View File

@@ -50,9 +50,6 @@ export default class CardList extends StatefulVue {
display: flex; display: flex;
flex-flow: row wrap; flex-flow: row wrap;
font-family: $main-font; font-family: $main-font;
.card {
}
} }
.error { .error {
width: 100%; width: 100%;

View File

@@ -12,19 +12,19 @@
</div> </div>
<div class="card__expander" v-on:click.stop> <div class="card__expander" v-on:click.stop>
<font-awesome-icon :icon="['fas', 'times']" class="close-button" v-on:click="onSelected(false)"/> <font-awesome-icon :icon="['fas', 'times']" class="close-button" v-on:click="onSelected(false)"/>
<CardListItemScripts :categoryId="categoryId"></CardListItemScripts> <ScriptsTree :categoryId="categoryId"></ScriptsTree>
</div> </div>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Prop, Vue, Watch, Emit } from 'vue-property-decorator'; import { Component, Prop, Vue, Watch, Emit } from 'vue-property-decorator';
import CardListItemScripts from './CardListItemScripts.vue'; import ScriptsTree from '@/presentation/Scripts/ScriptsTree/ScriptsTree.vue';
import { StatefulVue } from '@/presentation/StatefulVue'; import { StatefulVue } from '@/presentation/StatefulVue';
@Component({ @Component({
components: { components: {
CardListItemScripts, ScriptsTree,
}, },
}) })
export default class CardListItem extends StatefulVue { export default class CardListItem extends StatefulVue {
@@ -124,10 +124,6 @@ export default class CardListItem extends StatefulVue {
justify-content: center; justify-content: center;
align-items: center; align-items: center;
text-transform: uppercase;
color: $light-gray;
font-size: 1.5em;
.close-button { .close-button {
font-size: 0.75em; font-size: 0.75em;
position: absolute; position: absolute;

View File

@@ -1,43 +0,0 @@
import { ICategory } from './../../../domain/ICategory';
import { IApplicationState, IUserSelection } from '@/application/State/IApplicationState';
import { INode } from './../SelectableTree/INode';
export class ScriptNodeParser {
public static parseNodes(categoryId: number, state: IApplicationState): INode[] | undefined {
const category = state.app.findCategory(categoryId);
if (!category) {
throw new Error(`Category with id ${categoryId} does not exist`);
}
const tree = this.parseNodesRecursively(category, state.selection);
return tree;
}
private static parseNodesRecursively(parentCategory: ICategory, selection: IUserSelection): INode[] {
const nodes = new Array<INode>();
if (parentCategory.subCategories && parentCategory.subCategories.length > 0) {
for (const subCategory of parentCategory.subCategories) {
const subCategoryNodes = this.parseNodesRecursively(subCategory, selection);
nodes.push(
{
id: `cat${subCategory.id}`,
text: subCategory.name,
selected: false,
children: subCategoryNodes,
documentationUrls: subCategory.documentationUrls,
});
}
}
if (parentCategory.scripts && parentCategory.scripts.length > 0) {
for (const script of parentCategory.scripts) {
nodes.push( {
id: script.id,
text: script.name,
selected: selection.isSelected(script),
children: undefined,
documentationUrls: script.documentationUrls,
});
}
}
return nodes;
}
}

View File

@@ -1,19 +0,0 @@
<template>
<div>
<CardList v-if="isGrouped">
</CardList>
<SelectableTree></SelectableTree>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue, Emit } from 'vue-property-decorator';
import { Category } from '@/domain/Category';
import { StatefulVue } from '@/presentation/StatefulVue';
/** Shows content of single category or many categories */
@Component
export default class CategoryTree extends StatefulVue {
@Prop() public data!: Category | Category[];
}
</script>

View File

@@ -0,0 +1,4 @@
export enum Grouping {
Cards = 1,
None = 0,
}

View File

@@ -0,0 +1,62 @@
<template>
<div class="container">
<span class="part">Group by:</span>
<span class="part">
<span
class="part"
v-bind:class="{ 'disabled': isGrouped, 'enabled': !isGrouped}"
@click="!isGrouped ? toggleGrouping() : undefined">Cards</span>
<span class="part">|</span>
<span class="part"
v-bind:class="{ 'disabled': !isGrouped, 'enabled': isGrouped}"
@click="isGrouped ? toggleGrouping() : undefined">None</span>
</span>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import { StatefulVue } from '@/presentation/StatefulVue';
import { IApplicationState } from '@/application/State/IApplicationState';
import { Grouping } from './Grouping';
@Component
export default class TheGrouper extends StatefulVue {
public currentGrouping: Grouping;
public isGrouped = true;
public toggleGrouping() {
this.currentGrouping = this.currentGrouping === Grouping.None ? Grouping.Cards : Grouping.None;
this.isGrouped = this.currentGrouping === Grouping.Cards;
this.$emit('groupingChanged', this.currentGrouping);
}
}
</script>
<style scoped lang="scss">
@import "@/presentation/styles/colors.scss";
@import "@/presentation/styles/fonts.scss";
.container {
display: flex;
flex-direction: row;
flex-wrap: wrap;
font-family: $normal-font;
.part {
display: flex;
margin-right:5px;
}
}
.enabled {
cursor: pointer;
&:hover {
font-weight:bold;
text-decoration:underline;
}
}
.disabled {
color:$gray;
}
</style>

View File

@@ -0,0 +1,63 @@
import { IApplicationState, IUserSelection } from '@/application/State/IApplicationState';
import { ICategory, IScript } from '@/domain/ICategory';
import { INode } from './SelectableTree/INode';
export function parseAllCategories(state: IApplicationState): INode[] | undefined {
const nodes = new Array<INode>();
for (const category of state.app.categories) {
const children = parseCategoryRecursively(category, state.selection);
nodes.push(convertCategoryToNode(category, children));
}
return nodes;
}
export function parseSingleCategory(categoryId: number, state: IApplicationState): INode[] | undefined {
const category = state.app.findCategory(categoryId);
if (!category) {
throw new Error(`Category with id ${categoryId} does not exist`);
}
const tree = parseCategoryRecursively(category, state.selection);
return tree;
}
function parseCategoryRecursively(
parentCategory: ICategory,
selection: IUserSelection): INode[] {
if (!parentCategory) { throw new Error('parentCategory is undefined'); }
if (!selection) { throw new Error('selection is undefined'); }
const nodes = new Array<INode>();
if (parentCategory.subCategories && parentCategory.subCategories.length > 0) {
for (const subCategory of parentCategory.subCategories) {
const subCategoryNodes = parseCategoryRecursively(subCategory, selection);
nodes.push(convertCategoryToNode(subCategory, subCategoryNodes));
}
}
if (parentCategory.scripts && parentCategory.scripts.length > 0) {
for (const script of parentCategory.scripts) {
nodes.push(convertScriptToNode(script, selection));
}
}
return nodes;
}
function convertCategoryToNode(
category: ICategory, children: readonly INode[]): INode {
return {
id: `${category.id}`,
text: category.name,
selected: false,
children,
documentationUrls: category.documentationUrls,
};
}
function convertScriptToNode(script: IScript, selection: IUserSelection): INode {
return {
id: `${script.id}`,
text: script.name,
selected: selection.isSelected(script),
children: undefined,
documentationUrls: script.documentationUrls,
};
}

View File

@@ -1,5 +1,5 @@
<template> <template>
<span> <span id="container">
<span v-if="nodes != null && nodes.length > 0"> <span v-if="nodes != null && nodes.length > 0">
<SelectableTree <SelectableTree
:nodes="nodes" :nodes="nodes"
@@ -21,17 +21,17 @@
import { IScript } from '@/domain/IScript'; import { IScript } from '@/domain/IScript';
import { IApplicationState, IUserSelection } from '@/application/State/IApplicationState'; import { IApplicationState, IUserSelection } from '@/application/State/IApplicationState';
import { IFilterMatches } from '@/application/State/Filter/IFilterMatches'; import { IFilterMatches } from '@/application/State/Filter/IFilterMatches';
import { ScriptNodeParser } from './ScriptNodeParser'; import { parseAllCategories, parseSingleCategory } from './ScriptNodeParser';
import SelectableTree, { FilterPredicate } from './../SelectableTree/SelectableTree.vue'; import SelectableTree, { FilterPredicate } from './SelectableTree/SelectableTree.vue';
import { INode } from './../SelectableTree/INode'; import { INode } from './SelectableTree/INode';
@Component({ @Component({
components: { components: {
SelectableTree, SelectableTree,
}, },
}) })
export default class CardListItemScripts extends StatefulVue { export default class ScriptsTree extends StatefulVue {
@Prop() public categoryId!: number; @Prop() public categoryId?: number;
public nodes?: INode[] = null; public nodes?: INode[] = null;
public selectedNodeIds?: string[] = null; public selectedNodeIds?: string[] = null;
@@ -42,9 +42,11 @@
public async mounted() { public async mounted() {
// React to state changes // React to state changes
const state = await this.getCurrentStateAsync(); const state = await this.getCurrentStateAsync();
this.reactToChanges(state); state.selection.changed.on(this.handleSelectionChanged);
state.filter.filterRemoved.on(this.handleFilterRemoved);
state.filter.filtered.on(this.handleFiltered);
// Update initial state // Update initial state
await this.updateNodesAsync(this.categoryId); await this.initializeNodesAsync(this.categoryId);
} }
public async checkNodeAsync(node: INode) { public async checkNodeAsync(node: INode) {
@@ -60,19 +62,34 @@
} }
@Watch('categoryId') @Watch('categoryId')
public async updateNodesAsync(categoryId: |number) { public async initializeNodesAsync(categoryId?: number) {
this.nodes = categoryId ? const state = await this.getCurrentStateAsync();
await ScriptNodeParser.parseNodes(categoryId, await this.getCurrentStateAsync()) if (categoryId) {
: undefined; this.nodes = parseSingleCategory(categoryId, state);
} else {
this.nodes = parseAllCategories(state);
}
} }
public filterPredicate(node: INode): boolean { public filterPredicate(node: INode): boolean {
return this.matches.scriptMatches.some((script: IScript) => script.id === node.id); return this.matches.scriptMatches.some((script: IScript) => script.id === node.id);
} }
private reactToChanges(state: IApplicationState) { private handleSelectionChanged(selectedScripts: ReadonlyArray<IScript>) {
// Update selection data this.nodes = this.nodes.map((node: INode) => updateNodeSelection(node, selectedScripts));
const updateNodeSelection = (node: INode, selectedScripts: ReadonlyArray<IScript>): INode => { }
private handleFilterRemoved() {
this.filterText = '';
}
private handleFiltered(matches: IFilterMatches) {
this.filterText = matches.query;
this.matches = matches;
}
}
function updateNodeSelection(node: INode, selectedScripts: ReadonlyArray<IScript>): INode {
return { return {
id: node.id, id: node.id,
text: node.text, text: node.text,
@@ -80,20 +97,8 @@
children: node.children ? node.children.map((child) => updateNodeSelection(child, selectedScripts)) : [], children: node.children ? node.children.map((child) => updateNodeSelection(child, selectedScripts)) : [],
documentationUrls: node.documentationUrls, documentationUrls: node.documentationUrls,
}; };
};
state.selection.changed.on(
(selectedScripts: ReadonlyArray<IScript>) =>
this.nodes = this.nodes.map((node: INode) => updateNodeSelection(node, selectedScripts)),
);
// Update search / filter data
state.filter.filterRemoved.on(() =>
this.filterText = '');
state.filter.filtered.on((matches: IFilterMatches) => {
this.filterText = matches.query;
this.matches = matches;
});
}
} }
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

View File

@@ -1,6 +1,7 @@
<template> <template>
<div class="container"> <div class="container">
<div class="part select">Select:</div> <div class="part select">Select:</div>
<div class="part">
<div class="part"> <div class="part">
<SelectableOption <SelectableOption
label="None" label="None"
@@ -23,6 +24,7 @@
@click="selectAllAsync()" /> @click="selectAllAsync()" />
</div> </div>
</div> </div>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
@@ -94,7 +96,6 @@ export default class TheSelector extends StatefulVue {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
flex-wrap: wrap; flex-wrap: wrap;
align-items:flex-start;
.part { .part {
display: flex; display: flex;
margin-right:5px; margin-right:5px;

View File

@@ -1,48 +0,0 @@
<template>
<div class="container">
Group by: <span
v-bind:class="{ 'disabled': isGrouped, 'enabled': !isGrouped}"
@click="changeGrouping()" >Cards</Span> |
<span class="action"
v-bind:class="{ 'disabled': !isGrouped, 'enabled': isGrouped}"
@click="changeGrouping()">None</span>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import { StatefulVue } from '@/presentation/StatefulVue';
import { IApplicationState } from '@/application/State/IApplicationState';
@Component
export default class TheGrouper extends StatefulVue {
public isGrouped = true;
public changeGrouping() {
this.isGrouped = !this.isGrouped;
}
}
</script>
<style scoped lang="scss">
@import "@/presentation/styles/colors.scss";
@import "@/presentation/styles/fonts.scss";
.container {
// text-align:left;
font:$normal-font;
}
.enabled {
cursor: pointer;
&:hover {
font-weight:bold;
text-decoration:underline;
}
}
.disabled {
color:$gray;
}
</style>

View File

@@ -0,0 +1,67 @@
<template>
<div>
<div class="help-container">
<TheSelector class="left" />
<TheGrouper class="right"
v-on:groupingChanged="onGroupingChanged($event)" />
</div>
<CardList v-if="showCards" />
<ScriptsTree v-if="showList" />
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue, Emit } from 'vue-property-decorator';
import { Category } from '@/domain/Category';
import { StatefulVue } from '@/presentation/StatefulVue';
import TheGrouper from '@/presentation/Scripts/Grouping/TheGrouper.vue';
import TheSelector from '@/presentation/Scripts/Selector/TheSelector.vue';
import ScriptsTree from '@/presentation/Scripts/ScriptsTree/ScriptsTree.vue';
import CardList from '@/presentation/Scripts/Cards/CardList.vue';
import { Grouping } from './Grouping/Grouping';
/** Shows content of single category or many categories */
@Component({
components: {
TheGrouper,
TheSelector,
ScriptsTree,
CardList,
},
})
export default class TheScripts extends StatefulVue {
public showCards = true;
public showList = false;
@Prop() public data!: Category | Category[];
public onGroupingChanged(group: Grouping) {
switch (group) {
case Grouping.Cards:
this.showCards = true;
this.showList = false;
break;
case Grouping.None:
this.showCards = false;
this.showList = true;
break;
default:
throw new Error('Unknown grouping');
}
}
}
</script>
<style scoped lang="scss">
.help-container {
display: flex;
justify-content: space-between;
.left {
justify-content: flex-start;
}
.right {
justify-content: flex-end;
}
}
</style>

View File

@@ -1,8 +1,15 @@
// Overrides base styling for LiquorTree // Overrides base styling for LiquorTree
@import "@/presentation/styles/colors.scss"; @import "@/presentation/styles/colors.scss";
.tree {
background-color: $slate;
}
.tree-node > .tree-content > .tree-anchor > span { .tree-node > .tree-content > .tree-anchor > span {
color: $white !important; color: $white !important;
text-transform: uppercase;
color: $light-gray;
font-size: 1.5em;
} }
.tree-node { .tree-node {